package portal import ( "context" "database/sql" "encoding/json" "errors" "strings" "testing" "git.118im.com/im_team/olons_server/internal/accounts" "git.118im.com/im_team/olons_server/internal/store" ) type fakeRepository struct { config json.RawMessage banners []store.Banner content *store.Content levels []store.MemberLevel notices []store.MemberNotice summary *store.TaskSummary leaders []store.LeaderboardMember services []store.CustomerService noticePage *store.MemberNoticePage } func (r fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) { return r.config, nil } func (r fakeRepository) ActiveBanners(context.Context, int64, string) ([]store.Banner, error) { return r.banners, nil } func (r fakeRepository) PublishedContent(context.Context, int64, string) (*store.Content, error) { if r.content == nil { return nil, store.ErrResourceNotFound } return r.content, nil } func (r fakeRepository) ActiveMemberLevels(context.Context, int64) ([]store.MemberLevel, error) { return r.levels, nil } func (r fakeRepository) ActiveMemberNotices(context.Context, int64, int64, int) ([]store.MemberNotice, error) { return r.notices, nil } func (r fakeRepository) LatestPopupNotice(context.Context, int64, int64) (*store.MemberNotice, error) { if len(r.notices) == 0 { return nil, nil } return &r.notices[0], nil } func (r fakeRepository) MarkMemberNoticeRead(context.Context, int64, int64, int64) error { return nil } func (r fakeRepository) MemberNotices(context.Context, int64, int64, int, int) (*store.MemberNoticePage, error) { if r.noticePage == nil { return &store.MemberNoticePage{Items: []store.MemberNotice{}}, nil } return r.noticePage, nil } func (r fakeRepository) MemberTaskSummary(context.Context, int64, int64) (*store.TaskSummary, error) { return r.summary, nil } func (r fakeRepository) TaskLeaderboard(context.Context, int64, int) ([]store.LeaderboardMember, error) { return r.leaders, nil } func (r fakeRepository) ActiveCustomerServices(context.Context, int64) ([]store.CustomerService, error) { return r.services, nil } func TestParseFeaturesSupportsNestedAndLegacyKeys(t *testing.T) { features := parseFeatures(json.RawMessage(`{ "features":{"task_enabled":true,"rechargeEnabled":1}, "WITHDRAWAL":"on","SHOP_STATUS":"false" }`)) if !features.TaskEnabled || !features.RechargeEnabled || !features.WithdrawalEnabled || features.ManualMallEnabled { t.Fatalf("unexpected features: %+v", features) } } func TestParseFeaturesDefaultsMissingSwitchesToEnabled(t *testing.T) { features := parseFeatures(json.RawMessage(`{}`)) if !features.TaskEnabled || !features.RechargeEnabled || !features.WithdrawalEnabled || !features.ManualMallEnabled { t.Fatalf("missing switches should default to enabled: %+v", features) } features = parseFeatures(json.RawMessage(`{"features":{"task_enabled":false,"withdrawal_enabled":false}}`)) if features.TaskEnabled || features.WithdrawalEnabled || !features.RechargeEnabled || !features.ManualMallEnabled { t.Fatalf("explicitly disabled switches must remain disabled: %+v", features) } } func TestBannersSanitizeTargets(t *testing.T) { service := New(fakeRepository{banners: []store.Banner{ {ID: 1, TargetType: 2, TargetValue: "/receive"}, {ID: 2, TargetType: 2, TargetValue: "//evil.example"}, {ID: 3, TargetType: 3, TargetValue: "https://example.com/path"}, {ID: 4, TargetType: 3, TargetValue: "javascript:alert(1)"}, }}) items, err := service.Banners(context.Background(), 0, "home_top") if err != nil { t.Fatal(err) } wantTypes := []string{"internal_route", "none", "external_url", "none"} for i, want := range wantTypes { if items[i].TargetType != want { t.Errorf("item %d target type = %q, want %q", i, items[i].TargetType, want) } } } func TestBannersRejectInvalidPosition(t *testing.T) { service := New(fakeRepository{}) if _, err := service.Banners(context.Background(), 0, "../home"); !errors.Is(err, ErrPositionInvalid) { t.Fatalf("error = %v, want %v", err, ErrPositionInvalid) } } func TestContentMapsAssets(t *testing.T) { service := New(fakeRepository{content: &store.Content{ ContentKey: "privacy_policy", Title: "Policy", AssetIDs: sql.NullString{String: `[1,2]`, Valid: true}, }}) content, err := service.Content(context.Background(), 0, "privacy_policy") if err != nil { t.Fatal(err) } if len(content.AssetIDs) != 2 || content.AssetIDs[0] != 1 || content.AssetIDs[1] != 2 { t.Fatalf("unexpected asset IDs: %v", content.AssetIDs) } } func TestMemberLevelsAlwaysReturnJSONObjectRules(t *testing.T) { service := New(fakeRepository{levels: []store.MemberLevel{{ID: 1, Code: "VIP1", Name: "Normal"}}}) levels, err := service.MemberLevels(context.Background(), 0) if err != nil { t.Fatal(err) } if string(levels[0].Rules) != `{}` { t.Fatalf("rules = %s, want {}", levels[0].Rules) } } func TestHomeUsesSettledSummaryAndSnakeCaseJSON(t *testing.T) { service := New(fakeRepository{ config: json.RawMessage(`{"features":{"task_enabled":true}}`), summary: &store.TaskSummary{TodayCount: 2, TodayCommissionAmount: 150}, }) home, err := service.Home(context.Background(), 0, 7, &accounts.Profile{ Username: "alice", Level: accounts.ProfileLevel{Name: "VIP1"}, Wallet: accounts.ProfileWallet{Currency: "CNY"}, }) if err != nil { t.Fatal(err) } encoded, err := json.Marshal(home) if err != nil { t.Fatal(err) } text := string(encoded) for _, field := range []string{`"today_count":2`, `"today_commission_amount":150`, `"task_enabled":true`} { if !strings.Contains(text, field) { t.Errorf("home JSON %s does not contain %s", text, field) } } } func TestLeaderboardMasksUsernameAndAddsRank(t *testing.T) { service := New(fakeRepository{leaders: []store.LeaderboardMember{ {MemberID: 7, Username: "风轻云淡", ClaimedPoints: 125800}, {MemberID: 8, Username: "李明", ClaimedPoints: 96000}, }}) items, err := service.Leaderboard(context.Background(), 1) if err != nil { t.Fatal(err) } if len(items) != 2 || items[0].Rank != 1 || items[0].Username != "风***淡" || items[0].ClaimedPoints != 125800 || items[1].Username != "李*" || items[1].Status != "领取成功" { t.Fatalf("unexpected leaderboard: %+v", items) } } func TestCustomerServicesSanitizeURLsAndMapStatus(t *testing.T) { service := New(fakeRepository{services: []store.CustomerService{ {ID: 1, Name: "在线客服", AvatarURL: "https://example.com/avatar.png", ServiceURL: "https://example.com/chat", OnlineStatus: 1}, {ID: 2, Name: "离线客服", AvatarURL: "javascript:alert(1)", ServiceURL: "http://example.com/chat", OnlineStatus: 2}, }}) items, err := service.CustomerServices(context.Background(), 0) if err != nil { t.Fatal(err) } if len(items) != 2 || items[0].OnlineStatus != "online" || items[0].ServiceURL == "" || items[1].OnlineStatus != "offline" || items[1].AvatarURL != "" || items[1].ServiceURL != "" { t.Fatalf("unexpected customer services: %+v", items) } } func TestNoticesMapsPageAndSourceType(t *testing.T) { service := New(fakeRepository{noticePage: &store.MemberNoticePage{ Items: []store.MemberNotice{{ID: 3, Title: "活动通知", SourceType: 2, CreatedAt: "2026-10-13T08:00:00Z"}}, Total: 21, }}) page, err := service.Notices(context.Background(), 1, 7, 2, 20) if err != nil { t.Fatal(err) } if page.Page != 2 || page.PageSize != 20 || page.Total != 21 || len(page.Items) != 1 || page.Items[0].SourceType != "operational_simulation" || page.Items[0].CreatedAt == "" { t.Fatalf("unexpected notice page: %+v", page) } } func TestNoticesRejectsOversizedPage(t *testing.T) { service := New(fakeRepository{}) if _, err := service.Notices(context.Background(), 1, 7, 1, 101); !errors.Is(err, ErrPageSizeInvalid) { t.Fatalf("error = %v, want %v", err, ErrPageSizeInvalid) } } func TestPopupNoticeUsesManagedContent(t *testing.T) { service := New(fakeRepository{content: &store.Content{ ContentKey: "home_popup_content", Title: "公告通知", BodyText: "新活动已上线", PublishedAt: "2026-10-16T08:00:00Z", }}) notice, err := service.PopupNotice(context.Background(), 1, 7) if err != nil { t.Fatal(err) } if notice == nil || notice.ID != 0 || notice.Content != "新活动已上线" || notice.SourceType != "managed_content" { t.Fatalf("unexpected popup notice: %+v", notice) } } func TestContentAliasesPublicAgreementRoutesToManagedText(t *testing.T) { service := New(fakeRepository{content: &store.Content{ ContentKey: "platform_introduction", Title: "平台简介", BodyText: "服务条款正文", }}) content, err := service.Content(context.Background(), 1, "user_agreement") if err != nil { t.Fatal(err) } if content.ContentKey != "user_agreement" || content.Title != "用户协议" || content.BodyText != "服务条款正文" { t.Fatalf("unexpected aliased content: %+v", content) } }