196 lines
6.8 KiB
Go
196 lines
6.8 KiB
Go
package portal
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.118im.com/im_team/im_workbench/internal/accounts"
|
|
"git.118im.com/im_team/im_workbench/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) 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 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: "风轻云淡", CompletedCount: 12},
|
|
{MemberID: 8, Username: "李明", CompletedCount: 9},
|
|
}})
|
|
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[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)
|
|
}
|
|
}
|