2026-07-14 17:12:39 +08:00

236 lines
8.1 KiB
Go

package repository
import (
"os"
"reflect"
"regexp"
"sort"
"testing"
)
func TestDefaultPermissionSeedUsesFinancePermissions(t *testing.T) {
permissions := map[string]struct{}{}
for _, permission := range defaultPermissions {
permissions[permission.Code] = struct{}{}
}
for _, code := range []string{"money:view", "money:payment-link:create"} {
if _, ok := permissions[code]; ok {
t.Fatalf("legacy money permission %s must not be part of default seed", code)
}
}
for _, code := range []string{
"payment-temporary-link:create",
"finance:view",
"finance-withdrawal:view",
"host-withdrawal:view",
} {
if _, ok := permissions[code]; !ok {
t.Fatalf("finance permission %s missing from default seed", code)
}
}
for _, code := range []string{
"finance-application:create",
"finance-application:audit",
"finance-operation:user-coin-credit",
"finance-operation:user-coin-debit",
"finance-operation:user-wallet-credit",
"finance-operation:user-wallet-debit",
"finance-operation:coin-seller-coin-credit",
"finance-operation:coin-seller-coin-debit",
} {
if _, ok := permissions[code]; ok {
t.Fatalf("removed finance application permission %s must not be seeded", code)
}
}
}
func TestDefaultRoleFinancePermissionTemplates(t *testing.T) {
opsPermissions := seedTestPermissionSet(defaultRolePermissionCodes(roleCodeOperationsLead))
for _, code := range []string{
"payment-bill:export",
"host-withdrawal:view",
} {
if _, ok := opsPermissions[code]; !ok {
t.Fatalf("ops-admin default permissions missing %s", code)
}
}
for _, code := range []string{
"finance:view",
"payment-temporary-link:create",
"payment-third-party:update-rate",
"payment-third-party:sync-rates",
"finance-application:create",
"finance-application:audit",
"finance-operation:user-coin-credit",
"finance-operation:user-coin-debit",
"finance-operation:user-wallet-credit",
"finance-operation:user-wallet-debit",
"finance-operation:coin-seller-coin-credit",
"finance-operation:coin-seller-coin-debit",
} {
if _, ok := opsPermissions[code]; ok {
t.Fatalf("ops-admin must not receive removed permission %s", code)
}
}
}
func TestManagedDefaultRolePermissionMatrix(t *testing.T) {
if len(managedDefaultRoles) != 7 {
t.Fatalf("managed role count = %d, want 7", len(managedDefaultRoles))
}
roleNames := map[string]string{}
for _, role := range managedDefaultRoles {
if previous, ok := roleNames[role.Code]; ok {
t.Fatalf("duplicate role code %s for %s and %s", role.Code, previous, role.Name)
}
roleNames[role.Code] = role.Name
}
wantNames := map[string]string{
roleCodePlatformAdmin: "超级管理员",
roleCodeOperationsLead: "运营负责人",
roleCodeOperationsSpecialist: "运营专员",
roleCodeProductLead: "产品负责人",
roleCodeProductSpecialist: "产品专员",
roleCodeFinanceLead: "财务负责人",
roleCodeFinanceSpecialist: "财务专员",
}
if !reflect.DeepEqual(roleNames, wantNames) {
t.Fatalf("managed roles = %#v, want %#v", roleNames, wantNames)
}
defined := map[string]struct{}{}
for _, permission := range defaultPermissions {
defined[permission.Code] = struct{}{}
}
for _, role := range managedDefaultRoles {
if role.Code == roleCodePlatformAdmin {
continue
}
seen := map[string]struct{}{}
for _, code := range defaultRolePermissionCodes(role.Code) {
if _, ok := seen[code]; ok {
t.Fatalf("role %s contains duplicate permission %s", role.Code, code)
}
seen[code] = struct{}{}
if _, ok := defined[code]; !ok {
t.Fatalf("role %s references permission %s missing from defaultPermissions", role.Code, code)
}
}
}
}
func TestManagedDefaultRoleModuleBoundaries(t *testing.T) {
assertRolePermissions(t, roleCodeOperationsLead,
[]string{"app-config:update", "payment-third-party:sync-methods", "game-catalog:update", "country:update"},
[]string{"app-version:view", "finance:view", "payment-third-party:update-rate", "role:view"},
)
assertRolePermissions(t, roleCodeOperationsSpecialist,
[]string{"room:update", "game-robot:update", "room-rps-config:update", "payment-bill:export"},
[]string{"activity:update", "daily-task:update", "game-catalog:update", "self-game:update", "coin-adjustment:create"},
)
assertRolePermissions(t, roleCodeProductLead,
[]string{"app-version:update", "daily-task:update", "game-catalog:update", "resource:update"},
[]string{"bd:create", "coin-adjustment:create", "country:update", "finance:view"},
)
assertRolePermissions(t, roleCodeProductSpecialist,
[]string{"resource:update", "room-robot:update", "game-catalog:view", "daily-task:view"},
[]string{"app-config:update", "room:update", "game-catalog:update", "daily-task:update", "bd:view"},
)
financeLead := defaultRolePermissionCodes(roleCodeFinanceLead)
financeSpecialist := defaultRolePermissionCodes(roleCodeFinanceSpecialist)
if !reflect.DeepEqual(financeLead, financeSpecialist) {
t.Fatalf("finance lead and specialist permissions differ: lead=%v specialist=%v", financeLead, financeSpecialist)
}
assertRolePermissions(t, roleCodeFinanceLead,
[]string{"finance:view", "payment-bill:view", "payment-bill:export", "finance-withdrawal:audit", "room-rps-order:view"},
[]string{"game-catalog:view", "room-rps-config:view", "app-user:view", "daily-task:view"},
)
}
func TestRolePermissionMigrationMatchesManagedMatrix(t *testing.T) {
content, err := os.ReadFile("../../migrations/093_role_permission_matrix.sql")
if err != nil {
t.Fatalf("read role permission migration: %v", err)
}
sqlText := string(content)
for _, roleCode := range []string{
roleCodeOperationsLead,
roleCodeOperationsSpecialist,
roleCodeProductLead,
roleCodeProductSpecialist,
} {
pattern := regexp.MustCompile(`(?s)WHERE role\.code = '` + regexp.QuoteMeta(roleCode) + `'\s+AND permission\.code IN \((.*?)\);`)
matches := pattern.FindStringSubmatch(sqlText)
if len(matches) != 2 {
t.Fatalf("migration permission block missing for %s", roleCode)
}
assertMigrationPermissionCodes(t, roleCode, matches[1])
}
financePattern := regexp.MustCompile(`(?s)WHERE role\.code IN \('finance-lead', 'finance-specialist'\)\s+AND permission\.code IN \((.*?)\);`)
financeMatches := financePattern.FindStringSubmatch(sqlText)
if len(financeMatches) != 2 {
t.Fatal("migration permission block missing for finance roles")
}
assertMigrationPermissionCodes(t, roleCodeFinanceLead, financeMatches[1])
assertMigrationPermissionCodes(t, roleCodeFinanceSpecialist, financeMatches[1])
}
func assertMigrationPermissionCodes(t *testing.T, roleCode string, sqlList string) {
t.Helper()
quotedCode := regexp.MustCompile(`'([a-z0-9:-]+)'`)
matches := quotedCode.FindAllStringSubmatch(sqlList, -1)
actual := make([]string, 0, len(matches))
for _, match := range matches {
actual = append(actual, match[1])
}
expected := append([]string(nil), defaultRolePermissionCodes(roleCode)...)
sort.Strings(actual)
sort.Strings(expected)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("migration permissions for %s differ\nactual-only=%v\nexpected-only=%v", roleCode, stringSetDifference(actual, expected), stringSetDifference(expected, actual))
}
}
func stringSetDifference(left []string, right []string) []string {
rightSet := make(map[string]struct{}, len(right))
for _, item := range right {
rightSet[item] = struct{}{}
}
out := make([]string, 0)
for _, item := range left {
if _, ok := rightSet[item]; !ok {
out = append(out, item)
}
}
return out
}
func assertRolePermissions(t *testing.T, roleCode string, required []string, forbidden []string) {
t.Helper()
permissions := seedTestPermissionSet(defaultRolePermissionCodes(roleCode))
for _, code := range required {
if _, ok := permissions[code]; !ok {
t.Errorf("role %s missing required permission %s", roleCode, code)
}
}
for _, code := range forbidden {
if _, ok := permissions[code]; ok {
t.Errorf("role %s must not have permission %s", roleCode, code)
}
}
}
func seedTestPermissionSet(codes []string) map[string]struct{} {
out := make(map[string]struct{}, len(codes))
for _, code := range codes {
out[code] = struct{}{}
}
return out
}