2026-07-14 20:23:12 +08:00

259 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}