2026-07-10 20:30:53 +08:00

366 lines
12 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 appconfig
import (
"os"
"strings"
"testing"
"time"
"hyapp-admin-server/internal/model"
)
func TestH5AppScopeMigrationKeepsAppAgnosticLegacyBaseline(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",
"DROP INDEX uk_admin_app_configs_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)
}
}
// 迁移不能枚举当前 App空基线 + runtime override 才能让未来 app_code 自动继承旧配置。
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 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")
}
}