package appconfig import ( "context" "regexp" "testing" "time" "github.com/DATA-DOG/go-sqlmock" ) func TestRoomDiscoveryAllRegionsCachesPerAppUntilTTL(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("new sql mock failed: %v", err) } defer db.Close() mock.ExpectQuery(regexp.QuoteMeta(getRoomDiscoveryScopeSQL)). WithArgs("wide-app", roomDiscoveryGroup, roomDiscoveryScopeKey). WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow(roomDiscoveryScopeAllActiveRegions)) reader := &MySQLReader{ db: db, roomDiscoveryCache: make(map[string]roomDiscoveryCacheEntry), roomDiscoveryCacheTTL: time.Minute, } for attempt := 0; attempt < 2; attempt++ { allowed, err := reader.RoomDiscoveryAllRegions(context.Background(), " WIDE-APP ") if err != nil || !allowed { t.Fatalf("cached all-region scope attempt %d failed: allowed=%t err=%v", attempt, allowed, err) } } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("scope must query DB only once inside TTL: %v", err) } reader.roomDiscoveryMu.Lock() entry := reader.roomDiscoveryCache["wide-app"] entry.expiresAt = time.Now().Add(-time.Second) reader.roomDiscoveryCache["wide-app"] = entry reader.roomDiscoveryMu.Unlock() mock.ExpectQuery(regexp.QuoteMeta(getRoomDiscoveryScopeSQL)). WithArgs("wide-app", roomDiscoveryGroup, roomDiscoveryScopeKey). WillReturnRows(sqlmock.NewRows([]string{"value"}).AddRow("viewer_region")) allowed, err := reader.RoomDiscoveryAllRegions(context.Background(), "wide-app") if err != nil || allowed { t.Fatalf("expired scope did not refresh to viewer-region: allowed=%t err=%v", allowed, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("expired scope refresh mismatch: %v", err) } } func TestRuntimeCapabilitiesReadBothKeysOnceAndCachePerApp(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("new sql mock failed: %v", err) } defer db.Close() mock.ExpectQuery(regexp.QuoteMeta(getRuntimeCapabilitiesSQL)). WithArgs("tenant-a", runtimeCapabilitiesGroup, activityLeaderboardScopeKey, resourceEquipCommandIDPolicyKey). WillReturnRows(sqlmock.NewRows([]string{"key", "value"}). AddRow(activityLeaderboardScopeKey, ActivityLeaderboardScopeViewerRegion). AddRow(resourceEquipCommandIDPolicyKey, ResourceEquipCommandIDPolicyRequired)) reader := &MySQLReader{ db: db, runtimeCapabilitiesCache: make(map[string]runtimeCapabilitiesCacheEntry), } for attempt := 0; attempt < 2; attempt++ { capabilities, err := reader.GetRuntimeCapabilities(context.Background(), " TENANT-A ") if err != nil || capabilities.ActivityLeaderboardScope != ActivityLeaderboardScopeViewerRegion || capabilities.ResourceEquipCommandIDPolicy != ResourceEquipCommandIDPolicyRequired { t.Fatalf("runtime capabilities attempt %d mismatch: config=%+v err=%v", attempt, capabilities, err) } } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("runtime capabilities must query DB once inside TTL: %v", err) } } func TestRuntimeCapabilitiesMissingRowsUseGenericDefaults(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("new sql mock failed: %v", err) } defer db.Close() mock.ExpectQuery(regexp.QuoteMeta(getRuntimeCapabilitiesSQL)). WithArgs("future-app", runtimeCapabilitiesGroup, activityLeaderboardScopeKey, resourceEquipCommandIDPolicyKey). WillReturnRows(sqlmock.NewRows([]string{"key", "value"})) reader := &MySQLReader{db: db} capabilities, err := reader.GetRuntimeCapabilities(context.Background(), "future-app") if err != nil { t.Fatalf("missing runtime capabilities failed: %v", err) } if capabilities.ActivityLeaderboardScope != ActivityLeaderboardScopeAppGlobal || capabilities.ResourceEquipCommandIDPolicy != ResourceEquipCommandIDPolicyLegacyOptional { t.Fatalf("missing runtime capability defaults mismatch: %+v", capabilities) } } func TestListH5LinksReadsOnlyRequestedApp(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("new sql mock failed: %v", err) } defer db.Close() mock.ExpectQuery(regexp.QuoteMeta(listH5LinksSQL)). WithArgs("yumi", h5LinkGroup). WillReturnRows(sqlmock.NewRows([]string{"key", "description", "value", "updated_at_ms"}). AddRow("admin", "Yumi Admin", "https://yumi.example.com/admin", int64(200)). AddRow("public", "", "https://yumi.example.com/public", int64(300))) reader := &MySQLReader{db: db} items, err := reader.ListH5Links(context.Background(), H5LinkQuery{AppCode: " YUMI "}) if err != nil { t.Fatalf("list scoped h5 links failed: %v", err) } if len(items) != 2 { t.Fatalf("unexpected merged h5 links: %+v", items) } if items[0].Key != "admin" || items[0].Label != "Yumi Admin" || items[0].URL != "https://yumi.example.com/admin" { t.Fatalf("scoped override did not replace baseline: %+v", items[0]) } if items[1].Key != "public" || items[1].Label != "public" || items[1].URL != "https://yumi.example.com/public" { t.Fatalf("requested app config mismatch: %+v", items[1]) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations mismatch: %v", err) } } func TestNormalizeBannerDisplayScopeAcceptsChineseLabels(t *testing.T) { cases := map[string]string{ "": "home", "首页": "home", "房间内": "room", "充值页": "recharge", "我的页": "me", "ME": "me", "ROOM": "room", "bad": "", } for input, want := range cases { if got := NormalizeBannerDisplayScope(input); got != want { t.Fatalf("scope %q mismatch: got %q want %q", input, got, want) } } } func TestBannerDisplayScopeListParsesMultiScopeValue(t *testing.T) { got := bannerDisplayScopeList("room,home,recharge,me") want := []string{"home", "room", "recharge", "me"} if len(got) != len(want) { t.Fatalf("scope count mismatch: got %+v want %+v", got, want) } for index := range want { if got[index] != want[index] { t.Fatalf("scope %d mismatch: got %+v want %+v", index, got, want) } } } func TestParseRoomRegionWhitelistSupportsObjectAndArrayPayloads(t *testing.T) { cases := []string{ `{"user_ids":["325379237278126080","1001","1001","0","bad"]}`, `["325379237278126080","1001","1001","0","bad"]`, } for _, input := range cases { got, err := parseRoomRegionWhitelist(input) if err != nil { t.Fatalf("parse whitelist %s failed: %v", input, err) } if _, ok := got[325379237278126080]; !ok { t.Fatalf("long user id missing: %+v", got) } if _, ok := got[1001]; !ok { t.Fatalf("small user id missing: %+v", got) } if _, ok := got[0]; ok { t.Fatalf("zero user id must be ignored: %+v", got) } } }