259 lines
8.9 KiB
Go
259 lines
8.9 KiB
Go
package repository
|
||
|
||
import (
|
||
"os"
|
||
"reflect"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"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 TestGiftRecordPermissionMigrationExtendsOperationsReadRoles(t *testing.T) {
|
||
content, err := os.ReadFile("../../migrations/095_gift_record_navigation.sql")
|
||
if err != nil {
|
||
t.Fatalf("read gift record permission migration: %v", err)
|
||
}
|
||
sqlText := string(content)
|
||
for _, token := range []string{
|
||
"'gift-record:view'",
|
||
"'ops-admin'",
|
||
"'operations-specialist'",
|
||
"'product-lead'",
|
||
"'product-specialist'",
|
||
} {
|
||
if !strings.Contains(sqlText, token) {
|
||
t.Fatalf("gift record migration missing %s", token)
|
||
}
|
||
}
|
||
}
|
||
|
||
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)...)
|
||
// 093 已在生产执行,不能为了新增页面回写旧迁移;095 以增量方式给同一批运营只读岗位补 gift-record:view。
|
||
// 这里仍校验 093 的原始精确矩阵,新权限由上面的 095 专项测试锁定。
|
||
expected = stringSetDifference(expected, []string{"gift-record:view"})
|
||
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
|
||
}
|