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 }