package apppopup import ( "context" "encoding/json" "strconv" "testing" "time" "chatapp3-golang/internal/config" "chatapp3-golang/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func TestQueryEntryPopupsReturnsConfigWithoutUserCooldown(t *testing.T) { db := newTestDB(t) now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1001, SysOrigin: "LIKEI", PopupKey: "invite", Name: "邀请活动弹窗", Scene: defaultScene, Enabled: true, LimitDays: 7, Priority: 100, Version: 3, ContentJSON: `{"title":"Invite friends","image":"https://cdn.example.com/invite.png"}`, JumpType: "H5", JumpURL: "/h5/app-invite/landing.html", }) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1002, SysOrigin: "LIKEI", PopupKey: "game", Name: "游戏引导弹窗", Scene: defaultScene, Enabled: true, LimitDays: 0, Priority: 90, Version: 1, ContentJSON: `{"title":"Play games","image":"https://cdn.example.com/game.png"}`, JumpType: "APP_ROUTE", JumpURL: "/game-center", }) service := NewService(config.Config{}, db, nil) service.now = func() time.Time { return now } resp, err := service.QueryEntryPopups(context.Background(), AuthUser{ UserID: 123, SysOrigin: "LIKEI", }, EntryQueryRequest{Scene: defaultScene, Platform: "ios", AppVersion: "3.12.0"}) if err != nil { t.Fatalf("QueryEntryPopups() error = %v", err) } if len(resp.Queue) != 2 || resp.Queue[0] != "invite" || resp.Queue[1] != "game" { t.Fatalf("queue = %#v, want [invite game]", resp.Queue) } invite := resp.Items["invite"] if !invite.Value || invite.Limit != 7 || invite.PopupID != 1001 || invite.Version != 3 { t.Fatalf("invite = %+v, want enabled limit 7 id 1001 version 3", invite) } if invite.Title != "Invite friends" || invite.Image != "https://cdn.example.com/invite.png" { t.Fatalf("invite content = title %q image %q", invite.Title, invite.Image) } game := resp.Items["game"] if !game.Value || game.Limit != 0 { t.Fatalf("game = %+v, want enabled limit 0", game) } } func TestQueryEntryPopupsFiltersByConfigRulesOnly(t *testing.T) { db := newTestDB(t) now := time.Date(2026, 5, 7, 12, 0, 0, 0, time.UTC) future := now.Add(time.Hour) past := now.Add(-time.Hour) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1001, SysOrigin: "LIKEI", PopupKey: "invite", Scene: defaultScene, Enabled: false, LimitDays: 7, Priority: 100, Version: 1, UserScopeType: "ALL", }) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1002, SysOrigin: "LIKEI", PopupKey: "game", Scene: defaultScene, Enabled: true, LimitDays: 7, Priority: 90, Version: 1, PlatformsJSON: `["android"]`, UserScopeType: "ALL", }) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1003, SysOrigin: "LIKEI", PopupKey: "future", Scene: defaultScene, Enabled: true, LimitDays: 7, Priority: 80, Version: 1, StartTime: &future, UserScopeType: "ALL", }) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1004, SysOrigin: "LIKEI", PopupKey: "expired", Scene: defaultScene, Enabled: true, LimitDays: 7, Priority: 70, Version: 1, EndTime: &past, UserScopeType: "ALL", }) service := NewService(config.Config{}, db, nil) service.now = func() time.Time { return now } resp, err := service.QueryEntryPopups(context.Background(), AuthUser{ UserID: 123, SysOrigin: "LIKEI", }, EntryQueryRequest{Platform: "ios", AppVersion: "3.12.0"}) if err != nil { t.Fatalf("QueryEntryPopups() error = %v", err) } if len(resp.Queue) != 0 { t.Fatalf("queue = %#v, want empty", resp.Queue) } assertReason(t, resp.Items["invite"], reasonDisabled) assertReason(t, resp.Items["game"], reasonPlatform) assertReason(t, resp.Items["future"], reasonNotStarted) assertReason(t, resp.Items["expired"], reasonExpired) } func TestQueryEntryPopupsVersionBoundaries(t *testing.T) { db := newTestDB(t) mustCreatePopupConfig(t, db, model.AppPopupConfig{ ID: 1001, SysOrigin: "LIKEI", PopupKey: "invite", Scene: defaultScene, Enabled: true, LimitDays: 7, Priority: 100, Version: 1, MinAppVersion: "3.9.8", MaxAppVersion: "3.13.0", UserScopeType: "ALL", }) service := NewService(config.Config{}, db, nil) resp, err := service.QueryEntryPopups(context.Background(), AuthUser{ UserID: 123, SysOrigin: "LIKEI", }, EntryQueryRequest{AppVersion: "3.12.0"}) if err != nil { t.Fatalf("QueryEntryPopups() error = %v", err) } if !resp.Items["invite"].Value { t.Fatalf("invite = %+v, want value true", resp.Items["invite"]) } resp, err = service.QueryEntryPopups(context.Background(), AuthUser{ UserID: 123, SysOrigin: "LIKEI", }, EntryQueryRequest{AppVersion: "3.8.9"}) if err != nil { t.Fatalf("QueryEntryPopups() error = %v", err) } assertReason(t, resp.Items["invite"], reasonMinVersion) resp, err = service.QueryEntryPopups(context.Background(), AuthUser{ UserID: 123, SysOrigin: "LIKEI", }, EntryQueryRequest{AppVersion: "3.14.0"}) if err != nil { t.Fatalf("QueryEntryPopups() error = %v", err) } assertReason(t, resp.Items["invite"], reasonMaxVersion) } func TestEntryQueryResponseMarshalFlattensPopupKeys(t *testing.T) { resp := EntryQueryResponse{ RequestID: "req-1", Queue: []string{"invite"}, Items: map[string]PopupItem{ "invite": {Value: true, Limit: 7}, }, } raw, err := json.Marshal(resp) if err != nil { t.Fatalf("MarshalJSON() error = %v", err) } var decoded map[string]any if err := json.Unmarshal(raw, &decoded); err != nil { t.Fatalf("Unmarshal response error = %v", err) } if decoded["requestId"] != "req-1" { t.Fatalf("requestId = %v, want req-1", decoded["requestId"]) } invite, ok := decoded["invite"].(map[string]any) if !ok { t.Fatalf("invite field missing in %s", raw) } if invite["value"] != true || invite["limit"] != float64(7) { t.Fatalf("invite = %#v, want value true limit 7", invite) } } func TestAdminConfigCRUD(t *testing.T) { db := newTestDB(t) service := NewService(config.Config{}, db, nil) saved, err := service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{ SysOrigin: "LIKEI", Scene: defaultScene, PopupKey: "invite", Name: "邀请弹窗", Enabled: true, LimitDays: 7, Priority: 100, Version: 1, Title: "Invite friends", Image: "https://cdn.example.com/invite.png", JumpType: "H5", JumpURL: "/h5/app-invite/landing.html", Description: "邀请活动首页弹窗", }) if err != nil { t.Fatalf("SaveAdminConfig() error = %v", err) } if saved.ID == "" || saved.PopupKey != "invite" || saved.LimitDays != 7 || saved.Description != "邀请活动首页弹窗" { t.Fatalf("saved = %+v", saved) } savedID, err := strconv.ParseInt(saved.ID, 10, 64) if err != nil { t.Fatalf("parse saved id: %v", err) } page, err := service.PageAdminConfigs(context.Background(), "LIKEI", defaultScene, "inv", 1, 20) if err != nil { t.Fatalf("PageAdminConfigs() error = %v", err) } if page.Total != 1 || len(page.Records) != 1 { t.Fatalf("page = %+v, want one record", page) } updated, err := service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{ ID: flexibleInt64{value: savedID}, SysOrigin: "LIKEI", Scene: defaultScene, PopupKey: "invite", Name: "邀请弹窗", Enabled: false, LimitDays: 0, Priority: 90, Version: 2, Description: "每次进入都弹", }) if err != nil { t.Fatalf("SaveAdminConfig(update) error = %v", err) } if updated.Enabled || updated.LimitDays != 0 || updated.Version != 2 { t.Fatalf("updated = %+v, want disabled limit 0 version 2", updated) } if err := service.DeleteAdminConfig(context.Background(), savedID); err != nil { t.Fatalf("DeleteAdminConfig() error = %v", err) } page, err = service.PageAdminConfigs(context.Background(), "LIKEI", defaultScene, "", 1, 20) if err != nil { t.Fatalf("PageAdminConfigs(after delete) error = %v", err) } if page.Total != 0 { t.Fatalf("total = %d, want 0", page.Total) } } func TestAdminConfigRejectsDuplicateKey(t *testing.T) { db := newTestDB(t) service := NewService(config.Config{}, db, nil) _, err := service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{ SysOrigin: "LIKEI", Scene: defaultScene, PopupKey: "invite", }) if err != nil { t.Fatalf("SaveAdminConfig(first) error = %v", err) } _, err = service.SaveAdminConfig(context.Background(), SaveAdminConfigRequest{ SysOrigin: "LIKEI", Scene: defaultScene, PopupKey: "invite", }) if err == nil { t.Fatalf("SaveAdminConfig(duplicate) error = nil, want error") } } func newTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.AppPopupConfig{}); err != nil { t.Fatalf("migrate app popup config: %v", err) } return db } func mustCreatePopupConfig(t *testing.T, db *gorm.DB, row model.AppPopupConfig) { t.Helper() if row.Scene == "" { row.Scene = defaultScene } if row.SysOrigin == "" { row.SysOrigin = defaultSysOrigin } if row.Version == 0 { row.Version = 1 } if row.UserScopeType == "" { row.UserScopeType = "ALL" } if err := db.Create(&row).Error; err != nil { t.Fatalf("create popup config %s: %v", row.PopupKey, err) } } func assertReason(t *testing.T, item PopupItem, want string) { t.Helper() if item.Value { t.Fatalf("item = %+v, want value false", item) } if item.Reason != want { t.Fatalf("reason = %q, want %q", item.Reason, want) } }