532 lines
20 KiB
Go
532 lines
20 KiB
Go
package appconfig
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/integration/userclient"
|
||
"hyapp-admin-server/internal/model"
|
||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||
)
|
||
|
||
func TestH5AppScopeSchemaMigrationCreatesScopedKeys(t *testing.T) {
|
||
body, err := os.ReadFile("../../../migrations/086_app_config_app_scope.sql")
|
||
if err != nil {
|
||
t.Fatalf("read h5 app scope migration failed: %v", err)
|
||
}
|
||
sqlText := string(body)
|
||
for _, snippet := range []string{
|
||
"app_code VARCHAR(32) NOT NULL DEFAULT ''",
|
||
"is_deleted BOOLEAN NOT NULL DEFAULT FALSE",
|
||
"INDEX_NAME = 'uk_admin_app_configs_group_key'",
|
||
"INDEX_NAME = 'idx_admin_app_config_group_key'",
|
||
"uk_admin_app_config_app_group_key (app_code, `group`, `key`)",
|
||
} {
|
||
if !strings.Contains(sqlText, snippet) {
|
||
t.Fatalf("h5 app scope migration missing %q", snippet)
|
||
}
|
||
}
|
||
// 086 只负责补齐通用 scoped 存储结构;091 再按 H5 的强隔离边界迁走历史空作用域数据。
|
||
for _, appCode := range []string{"huwaa", "fami", "yumi", "aslan"} {
|
||
if strings.Contains(sqlText, "'"+appCode+"'") {
|
||
t.Fatalf("migration must not hard-code app_code %q", appCode)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestH5StrictAppScopeMigrationMovesLegacyLinksToDefaultApp(t *testing.T) {
|
||
body, err := os.ReadFile("../../../migrations/091_h5_config_strict_app_scope.sql")
|
||
if err != nil {
|
||
t.Fatalf("read strict h5 app scope migration failed: %v", err)
|
||
}
|
||
sqlText := string(body)
|
||
for _, snippet := range []string{
|
||
"scoped.app_code = 'lalu'",
|
||
"legacy.app_code = ''",
|
||
"legacy.`group` = 'h5-links'",
|
||
"SET app_code = 'lalu'",
|
||
"`group` = 'h5-links'",
|
||
} {
|
||
if !strings.Contains(sqlText, snippet) {
|
||
t.Fatalf("strict h5 app scope migration missing %q", snippet)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFloatingScreenConfigMapsActivityOwnerContract(t *testing.T) {
|
||
item := floatingScreenConfigFromProto(" FAMI ", &activityv1.GameWinBroadcastConfig{
|
||
Enabled: true, MinWinCoins: 9000, UpdatedByAdminId: 7, CreatedAtMs: 100, UpdatedAtMs: 200,
|
||
})
|
||
if item.AppCode != "fami" || !item.Enabled || item.GameWinMinCoins != 9000 || item.UpdatedByAdminID != 7 || item.UpdatedAtMS != 200 {
|
||
t.Fatalf("floating screen config mapping mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestFloatingScreenNavigationUsesAppConfigChildMenu(t *testing.T) {
|
||
body, err := os.ReadFile("../../../migrations/114_game_win_floating_screen_navigation.sql")
|
||
if err != nil {
|
||
t.Fatalf("read floating screen navigation migration: %v", err)
|
||
}
|
||
sqlText := string(body)
|
||
for _, snippet := range []string{"'飘屏配置'", "'app-config-floating-screen'", "'/app-config/floating-screen'", "parent.code = 'app-config'", "'app-config:view'"} {
|
||
if !strings.Contains(sqlText, snippet) {
|
||
t.Fatalf("floating screen navigation migration missing %q", snippet)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRoomDiscoveryNavigationMigratesLegacyScopeWithoutRuntimeBranch(t *testing.T) {
|
||
body, err := os.ReadFile("../../../migrations/117_room_discovery_scope_config.sql")
|
||
if err != nil {
|
||
t.Fatalf("read room discovery navigation migration: %v", err)
|
||
}
|
||
sqlText := string(body)
|
||
for _, snippet := range []string{"'房间发现范围'", "'app-config-room-discovery'", "'/app-config/room-discovery'", "parent.code = 'app-config'", "'app-config:view'"} {
|
||
if !strings.Contains(sqlText, snippet) {
|
||
t.Fatalf("room discovery navigation migration missing %q", snippet)
|
||
}
|
||
}
|
||
// 旧 Huwaa 分支必须一次性落成配置数据,否则发布新代码后会错误退回 viewer_region;运行时仍只读 App 配置。
|
||
for _, snippet := range []string{"'huwaa'", "'room-discovery'", "'scope'", "'all_active_regions'"} {
|
||
if !strings.Contains(strings.ToLower(sqlText), snippet) {
|
||
t.Fatalf("room discovery legacy capability migration missing %q", snippet)
|
||
}
|
||
}
|
||
for _, appCode := range []string{"fami", "lalu", "yumi", "aslan"} {
|
||
if strings.Contains(strings.ToLower(sqlText), "'"+appCode+"'") {
|
||
t.Fatalf("room discovery migration must not seed unrelated app_code %q", appCode)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRoomDiscoveryConfigModelAcceptsOnlySupportedScopes(t *testing.T) {
|
||
item, err := roomDiscoveryConfigModel(" FUTURE-APP ", roomDiscoveryConfigRequest{Scope: " ALL_ACTIVE_REGIONS "})
|
||
if err != nil {
|
||
t.Fatalf("valid room discovery scope failed: %v", err)
|
||
}
|
||
if item.AppCode != "future-app" || item.Group != roomDiscoveryGroup || item.Key != roomDiscoveryScopeKey || item.Value != roomDiscoveryScopeAllActiveRegions {
|
||
t.Fatalf("room discovery model mismatch: %+v", item)
|
||
}
|
||
if _, err := roomDiscoveryConfigModel("future-app", roomDiscoveryConfigRequest{Scope: "global"}); err == nil {
|
||
t.Fatal("unsupported room discovery scope must fail")
|
||
}
|
||
legacy := roomDiscoveryConfigFromModel(model.AppConfig{AppCode: "future-app", Value: "global", UpdatedAtMS: 99})
|
||
if legacy.Scope != roomDiscoveryScopeViewerRegion || legacy.UpdatedAtMS != 99 {
|
||
t.Fatalf("invalid stored scope must fail closed: %+v", legacy)
|
||
}
|
||
}
|
||
|
||
func TestRuntimeCapabilitiesConfigUsesGenericDefaultsAndValidatesEnums(t *testing.T) {
|
||
defaults, err := runtimeCapabilitiesConfigFromModels(" FUTURE-APP ", nil)
|
||
if err != nil {
|
||
t.Fatalf("runtime capability defaults failed: %v", err)
|
||
}
|
||
if defaults.AppCode != "future-app" || defaults.ActivityLeaderboardScope != activityLeaderboardScopeAppGlobal || defaults.ResourceEquipCommandIDPolicy != resourceEquipCommandIDPolicyLegacyOptional {
|
||
t.Fatalf("runtime capability defaults mismatch: %+v", defaults)
|
||
}
|
||
|
||
items, err := runtimeCapabilitiesConfigModels(" FUTURE-APP ", runtimeCapabilitiesConfigRequest{
|
||
ActivityLeaderboardScope: " VIEWER_REGION ",
|
||
ResourceEquipCommandIDPolicy: " REQUIRED ",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("valid runtime capabilities failed: %v", err)
|
||
}
|
||
if len(items) != 2 || items[0].AppCode != "future-app" || items[0].Value != activityLeaderboardScopeViewerRegion || items[1].Value != resourceEquipCommandIDPolicyRequired {
|
||
t.Fatalf("runtime capability models mismatch: %+v", items)
|
||
}
|
||
if _, err := runtimeCapabilitiesConfigModels("future-app", runtimeCapabilitiesConfigRequest{ActivityLeaderboardScope: "all", ResourceEquipCommandIDPolicy: "required"}); err == nil {
|
||
t.Fatal("unsupported leaderboard scope must fail")
|
||
}
|
||
if _, err := runtimeCapabilitiesConfigModels("future-app", runtimeCapabilitiesConfigRequest{ActivityLeaderboardScope: "app_global", ResourceEquipCommandIDPolicy: "optional"}); err == nil {
|
||
t.Fatal("unsupported equip policy must fail")
|
||
}
|
||
}
|
||
|
||
func TestRuntimeCapabilitiesMigrationSeedsOnlyCompatibilityRows(t *testing.T) {
|
||
body, err := os.ReadFile("../../../migrations/118_runtime_capabilities_config.sql")
|
||
if err != nil {
|
||
t.Fatalf("read runtime capabilities migration: %v", err)
|
||
}
|
||
sqlText := strings.ToLower(string(body))
|
||
for _, snippet := range []string{
|
||
"'lalu', 'runtime-capabilities', 'activity_leaderboard_scope', 'viewer_region'",
|
||
"'fami', 'runtime-capabilities', 'activity_leaderboard_scope', 'viewer_region'",
|
||
"'fami', 'runtime-capabilities', 'resource_equip_command_id_policy', 'required'",
|
||
"'app-config-runtime-capabilities'",
|
||
"'/app-config/runtime-capabilities'",
|
||
} {
|
||
if !strings.Contains(sqlText, snippet) {
|
||
t.Fatalf("runtime capabilities migration missing %q", snippet)
|
||
}
|
||
}
|
||
if strings.Contains(sqlText, "'lalu', 'runtime-capabilities', 'resource_equip_command_id_policy'") {
|
||
t.Fatal("lalu must retain the missing-key legacy_optional default")
|
||
}
|
||
}
|
||
|
||
func TestRoleScopePoliciesConfigMapsOnlyValidatedOwnerSnapshot(t *testing.T) {
|
||
policies, err := roleScopePoliciesFromRequest(roleScopePoliciesConfigRequest{
|
||
OrganizationScope: " GLOBAL ",
|
||
ManagerBaseScope: "country",
|
||
ManagerExpandedScope: "region",
|
||
ManagerRegionExpansionConfigurable: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("valid role scope config failed: %v", err)
|
||
}
|
||
policies[0].UpdatedAtMS = 100
|
||
policies[1].UpdatedAtMS = 200
|
||
config, err := roleScopePoliciesConfigFromPolicies(" TENANT-A ", policies)
|
||
if err != nil {
|
||
t.Fatalf("map role scope owner response: %v", err)
|
||
}
|
||
if config.AppCode != "tenant-a" || config.OrganizationScope != roleScopeGlobal || config.ManagerBaseScope != roleScopeCountry || config.ManagerExpandedScope != roleScopeRegion || !config.ManagerRegionExpansionConfigurable || config.UpdatedAtMS != 200 {
|
||
t.Fatalf("role scope config mismatch: %+v", config)
|
||
}
|
||
|
||
if _, err := roleScopePoliciesFromRequest(roleScopePoliciesConfigRequest{
|
||
OrganizationScope: roleScopeRegion,
|
||
ManagerBaseScope: roleScopeCountry,
|
||
ManagerExpandedScope: roleScopeGlobal,
|
||
}); err == nil {
|
||
t.Fatal("disabled manager expansion must keep one scope")
|
||
}
|
||
if _, err := roleScopePoliciesConfigFromPolicies("tenant-a", []userclient.RoleScopePolicy{policies[0]}); err == nil {
|
||
t.Fatal("partial owner response must fail closed")
|
||
}
|
||
}
|
||
|
||
func TestH5LinkModelFromPayloadValidatesDynamicConfig(t *testing.T) {
|
||
item, err := h5LinkModelFromPayload(" YUMI ", h5LinkPayload{
|
||
Key: "host-center.v2",
|
||
Label: "Host Center",
|
||
URL: "https://h5.example.com/host",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("h5 config should be valid: %v", err)
|
||
}
|
||
if item.AppCode != "yumi" || item.Group != h5LinkGroup || item.Key != "host-center.v2" || item.Description != "Host Center" || item.Value != "https://h5.example.com/host" {
|
||
t.Fatalf("h5 config model mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestH5LinkModelFromPayloadRejectsInvalidKey(t *testing.T) {
|
||
_, err := h5LinkModelFromPayload("lalu", h5LinkPayload{
|
||
Key: "host center",
|
||
Label: "Host Center",
|
||
URL: "https://h5.example.com/host",
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected invalid key to fail")
|
||
}
|
||
}
|
||
|
||
func TestBannerModelFromRequestRequiresRoomSmallImageForRoomScope(t *testing.T) {
|
||
_, err := bannerModelFromRequest("lalu", bannerRequest{
|
||
CoverURL: "https://cdn.example.com/banner.png",
|
||
BannerType: "h5",
|
||
DisplayScope: bannerDisplayScopeRoom,
|
||
Param: "https://h5.example.com/activity",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
EndsAtMs: time.Now().UTC().Add(time.Hour).UnixMilli(),
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected room scope without room small image to fail")
|
||
}
|
||
}
|
||
|
||
func TestBannerModelFromRequestKeepsRoomScopeScheduleFields(t *testing.T) {
|
||
startsAtMs := time.Now().UTC().Add(time.Hour).UnixMilli()
|
||
endsAtMs := startsAtMs + int64(time.Hour/time.Millisecond)
|
||
item, err := bannerModelFromRequest("lalu", bannerRequest{
|
||
CoverURL: "https://cdn.example.com/banner.png",
|
||
RoomSmallImageURL: "https://cdn.example.com/banner-small.png",
|
||
BannerType: "h5",
|
||
DisplayScopes: []string{bannerDisplayScopeHome, bannerDisplayScopeRoom},
|
||
Param: "https://h5.example.com/activity",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
StartsAtMs: startsAtMs,
|
||
EndsAtMs: endsAtMs,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("banner model should be valid: %v", err)
|
||
}
|
||
if item.DisplayScope != "home,room" || item.RoomSmallImageURL == "" {
|
||
t.Fatalf("room display fields mismatch: %+v", item)
|
||
}
|
||
if item.StartsAtMS != startsAtMs || item.EndsAtMS != endsAtMs || item.Status != bannerStatusActive {
|
||
t.Fatalf("schedule fields mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestBannerModelFromRequestExpiresEndedActiveBanner(t *testing.T) {
|
||
item, err := bannerModelFromRequest("lalu", bannerRequest{
|
||
CoverURL: "https://cdn.example.com/banner.png",
|
||
RoomSmallImageURL: "https://cdn.example.com/ignored.png",
|
||
BannerType: "app",
|
||
DisplayScope: bannerDisplayScopeHome,
|
||
Status: bannerStatusActive,
|
||
Platform: "ios",
|
||
EndsAtMs: 1,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("banner model should accept ended active banner: %v", err)
|
||
}
|
||
if item.Status != bannerStatusExpired {
|
||
t.Fatalf("ended active banner must become expired: %+v", item)
|
||
}
|
||
if item.RoomSmallImageURL != "" {
|
||
t.Fatalf("non-room banner must not retain room small image: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestAppBannerFromModelReturnsDisplayScopes(t *testing.T) {
|
||
item := appBannerFromModel(bannerModel("home,room,recharge,me"))
|
||
|
||
if item.DisplayScope != "home,room,recharge,me" {
|
||
t.Fatalf("display scope should keep stored value: %+v", item)
|
||
}
|
||
if len(item.DisplayScopes) != 4 || item.DisplayScopes[0] != bannerDisplayScopeHome || item.DisplayScopes[1] != bannerDisplayScopeRoom || item.DisplayScopes[2] != bannerDisplayScopeRecharge || item.DisplayScopes[3] != bannerDisplayScopeMe {
|
||
t.Fatalf("display scopes mismatch: %+v", item.DisplayScopes)
|
||
}
|
||
}
|
||
|
||
func TestSplashScreenModelFromRequestValidatesH5Config(t *testing.T) {
|
||
startsAtMs := time.Now().UTC().Add(time.Hour).UnixMilli()
|
||
endsAtMs := startsAtMs + int64(time.Hour/time.Millisecond)
|
||
item, err := splashScreenModelFromRequest("lalu", splashScreenRequest{
|
||
CoverURL: "https://cdn.example.com/splash.png",
|
||
SplashType: "h5",
|
||
Param: "https://h5.example.com/splash",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
SortOrder: 7,
|
||
RegionID: 1001,
|
||
CountryCode: "us",
|
||
Description: "launch campaign",
|
||
DisplayDurationMs: 4200,
|
||
StartsAtMs: startsAtMs,
|
||
EndsAtMs: endsAtMs,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("splash screen model should be valid: %v", err)
|
||
}
|
||
if item.AppCode != "lalu" || item.SplashType != "h5" || item.CountryCode != "US" || item.DisplayDurationMS != 4200 || item.StartsAtMS != startsAtMs || item.EndsAtMS != endsAtMs {
|
||
t.Fatalf("splash screen model mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestSplashScreenModelFromRequestDefaultsDisplayDuration(t *testing.T) {
|
||
item, err := splashScreenModelFromRequest("lalu", splashScreenRequest{
|
||
CoverURL: "https://cdn.example.com/splash.png",
|
||
SplashType: "app",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("splash screen model should accept missing display duration: %v", err)
|
||
}
|
||
if item.DisplayDurationMS != splashDefaultDisplayDurationMS {
|
||
t.Fatalf("display duration should default to %d, got %+v", splashDefaultDisplayDurationMS, item)
|
||
}
|
||
}
|
||
|
||
func TestSplashScreenModelFromRequestRejectsNegativeDisplayDuration(t *testing.T) {
|
||
_, err := splashScreenModelFromRequest("lalu", splashScreenRequest{
|
||
CoverURL: "https://cdn.example.com/splash.png",
|
||
SplashType: "app",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
DisplayDurationMs: -1,
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected negative display duration to fail")
|
||
}
|
||
}
|
||
|
||
func TestSplashScreenModelFromRequestRequiresH5Link(t *testing.T) {
|
||
_, err := splashScreenModelFromRequest("lalu", splashScreenRequest{
|
||
CoverURL: "https://cdn.example.com/splash.png",
|
||
SplashType: "h5",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected h5 splash screen without link to fail")
|
||
}
|
||
}
|
||
|
||
func TestSplashScreenModelFromRequestExpiresEndedActiveConfig(t *testing.T) {
|
||
item, err := splashScreenModelFromRequest("lalu", splashScreenRequest{
|
||
CoverURL: "https://cdn.example.com/splash.png",
|
||
SplashType: "app",
|
||
Status: bannerStatusActive,
|
||
Platform: "ios",
|
||
EndsAtMs: 1,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ended app splash screen should be accepted: %v", err)
|
||
}
|
||
if item.Status != bannerStatusExpired {
|
||
t.Fatalf("ended active splash screen must become expired: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestAppSplashScreenFromModelMapsDTOFields(t *testing.T) {
|
||
item := appSplashScreenFromModel(model.AppSplashScreen{
|
||
ID: 9,
|
||
AppCode: "lalu",
|
||
CoverURL: "https://cdn.example.com/splash.png",
|
||
SplashType: "app",
|
||
Param: "profile",
|
||
Status: bannerStatusDisabled,
|
||
Platform: "ios",
|
||
SortOrder: 3,
|
||
RegionID: 2002,
|
||
CountryCode: "BR",
|
||
Description: "disabled campaign",
|
||
DisplayDurationMS: 5000,
|
||
StartsAtMS: 1700000000000,
|
||
EndsAtMS: 1800000000000,
|
||
CreatedAtMS: 1690000000000,
|
||
UpdatedAtMS: 1700000002000,
|
||
})
|
||
|
||
if item.ID != 9 || item.SplashType != "app" || item.Param != "profile" || item.RegionID != 2002 || item.DisplayDurationMs != 5000 || item.UpdatedAtMs != 1700000002000 {
|
||
t.Fatalf("splash screen dto mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestPopupModelFromRequestValidatesOptionalImageAndPeriod(t *testing.T) {
|
||
startsAtMs := time.Now().UTC().Add(time.Hour).UnixMilli()
|
||
endsAtMs := startsAtMs + int64(time.Hour/time.Millisecond)
|
||
item, err := popupModelFromRequest("lalu", popupRequest{
|
||
Code: "new-user-popup",
|
||
Name: "新人活动",
|
||
ImageURL: "",
|
||
JumpType: "h5",
|
||
JumpURL: "https://h5.example.com/new-user",
|
||
DisplayPeriodDays: 2,
|
||
Status: bannerStatusActive,
|
||
SortOrder: 4,
|
||
StartsAtMs: startsAtMs,
|
||
EndsAtMs: endsAtMs,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("popup model should be valid: %v", err)
|
||
}
|
||
if item.AppCode != "lalu" || item.Code != "new-user-popup" || item.Name != "新人活动" || item.ImageURL != "" || item.JumpType != "h5" || item.DisplayPeriodDays != 2 || item.StartsAtMS != startsAtMs || item.EndsAtMS != endsAtMs {
|
||
t.Fatalf("popup model mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestPopupModelFromRequestRejectsInvalidJumpURL(t *testing.T) {
|
||
_, err := popupModelFromRequest("lalu", popupRequest{
|
||
Code: "activity-popup",
|
||
Name: "活动弹窗",
|
||
JumpType: "h5",
|
||
JumpURL: "https://h5.example.com/new user",
|
||
Status: bannerStatusActive,
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected popup h5 jump url with whitespace to fail")
|
||
}
|
||
}
|
||
|
||
func TestPopupModelFromRequestRejectsInvalidCode(t *testing.T) {
|
||
_, err := popupModelFromRequest("lalu", popupRequest{
|
||
Code: "activity popup",
|
||
Name: "活动弹窗",
|
||
JumpType: "app",
|
||
JumpURL: "activity/detail",
|
||
Status: bannerStatusActive,
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected popup code with whitespace to fail")
|
||
}
|
||
}
|
||
|
||
func TestPopupModelFromRequestExpiresEndedActivePopup(t *testing.T) {
|
||
item, err := popupModelFromRequest("lalu", popupRequest{
|
||
Code: "history-popup",
|
||
Name: "历史活动",
|
||
JumpType: "app",
|
||
JumpURL: "activity/detail",
|
||
Status: bannerStatusActive,
|
||
EndsAtMs: 1,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ended popup should be accepted: %v", err)
|
||
}
|
||
if item.Status != bannerStatusExpired {
|
||
t.Fatalf("ended active popup must become expired: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestAppPopupFromModelMapsDTOFields(t *testing.T) {
|
||
item := appPopupFromModel(model.AppPopup{
|
||
ID: 10,
|
||
AppCode: "lalu",
|
||
Code: "recharge-popup",
|
||
Name: "充值活动",
|
||
ImageURL: "https://cdn.example.com/popup.png",
|
||
JumpType: "app",
|
||
JumpURL: "recharge",
|
||
DisplayPeriodDays: 0,
|
||
Status: bannerStatusDisabled,
|
||
SortOrder: 8,
|
||
StartsAtMS: 1700000000000,
|
||
EndsAtMS: 1800000000000,
|
||
CreatedAtMS: 1690000000000,
|
||
UpdatedAtMS: 1700000002000,
|
||
})
|
||
|
||
if item.ID != 10 || item.Code != "recharge-popup" || item.Name != "充值活动" || item.JumpType != "app" || item.JumpURL != "recharge" || item.DisplayPeriodDays != 0 || item.UpdatedAtMs != 1700000002000 {
|
||
t.Fatalf("popup dto mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func bannerModel(displayScope string) model.AppBanner {
|
||
return model.AppBanner{
|
||
ID: 1,
|
||
AppCode: "lalu",
|
||
CoverURL: "https://cdn.example.com/banner.png",
|
||
BannerType: "h5",
|
||
DisplayScope: displayScope,
|
||
Param: "https://h5.example.com/activity",
|
||
Status: bannerStatusActive,
|
||
Platform: "android",
|
||
}
|
||
}
|
||
|
||
func TestExploreTabModelFromRequestValidatesRequiredFields(t *testing.T) {
|
||
item, err := exploreTabModelFromRequest("lalu", exploreTabRequest{
|
||
Enabled: true,
|
||
H5URL: "https://h5.example.com/explore/live",
|
||
SortOrder: 3,
|
||
Tab: "Live",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("explore tab should be valid: %v", err)
|
||
}
|
||
if item.AppCode != "lalu" || item.Tab != "Live" || item.H5URL != "https://h5.example.com/explore/live" || !item.Enabled || item.SortOrder != 3 {
|
||
t.Fatalf("explore tab model mismatch: %+v", item)
|
||
}
|
||
}
|
||
|
||
func TestExploreTabModelFromRequestRejectsWhitespaceURL(t *testing.T) {
|
||
_, err := exploreTabModelFromRequest("lalu", exploreTabRequest{
|
||
Enabled: true,
|
||
H5URL: "https://h5.example.com/explore live",
|
||
Tab: "Live",
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected whitespace h5 url to fail")
|
||
}
|
||
}
|