package paymentguideser import ( "encoding/json" "strings" "testing" "time" "91porn-server/models/v/paymentguidemod" "91porn-server/models/v/sysconfmod" "91porn-server/models/v/usermod" "91porn-server/models/v/vidmod" "go.mongodb.org/mongo-driver/bson/primitive" ) func TestSegmentForUser(t *testing.T) { now := time.Now() tests := []struct { name string user *usermod.User want string }{ { name: "new unpaid", user: &usermod.User{ CreatedAt: now.Add(-time.Hour), WatchCount: 3, }, want: paymentguidemod.SegmentNewNeverPaid, }, { name: "new user with exhausted free views stays new unpaid", user: &usermod.User{ CreatedAt: now.Add(-time.Hour), WatchCount: 0, }, want: paymentguidemod.SegmentNewNeverPaid, }, { name: "expired is old unpaid", user: &usermod.User{ CreatedAt: now.AddDate(0, 0, -30), VipLevel: 2, VipExpireDate: now.Add(-time.Hour), }, want: paymentguidemod.SegmentOldNeverPaid, }, { name: "active paid upgrade", user: &usermod.User{ CreatedAt: now.AddDate(0, 0, -30), VipLevel: 2, VipExpireDate: now.AddDate(0, 1, 0), }, want: paymentguidemod.SegmentPaidUpgrade, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := SegmentForUser(tt.user); got != tt.want { t.Fatalf("SegmentForUser() = %q, want %q", got, tt.want) } }) } } func TestContentVersion(t *testing.T) { videos := []vidmod.PaymentGuideVideo{{ID: "video-a"}, {ID: "video-b"}} got := contentVersion(videos) if got == "" { t.Fatal("contentVersion must not be empty") } if got != contentVersion(videos) { t.Fatal("contentVersion must be stable for the same videos") } reordered := []vidmod.PaymentGuideVideo{{ID: "video-b"}, {ID: "video-a"}} if got == contentVersion(reordered) { t.Fatal("contentVersion must change when video order changes") } expanded := append(append([]vidmod.PaymentGuideVideo{}, videos...), vidmod.PaymentGuideVideo{ID: "video-c"}) if got == contentVersion(expanded) { t.Fatal("contentVersion must change when configured video count changes") } } func TestPaymentGuideSwitchesDefaultLegacyHomeToEnabled(t *testing.T) { legacy := paymentGuideSwitchesFromValues(map[sysconfmod.VCode]bool{ sysconfmod.VCodePaymentGuideEnabled: true, }) if !legacy.global || !legacy.home { t.Fatalf("legacy switches must keep the home scenes enabled: %+v", legacy) } disabled := paymentGuideSwitchesFromValues(map[sysconfmod.VCode]bool{ sysconfmod.VCodePaymentGuideEnabled: true, sysconfmod.VCodePaymentGuideHomeEnabled: false, }) if !disabled.global || disabled.home { t.Fatalf("configured home switch must be honored: %+v", disabled) } } func TestGuideRespUsesVideosWithoutVideoIDs(t *testing.T) { body, err := json.Marshal(GuideResp{ Show: true, Videos: []vidmod.PaymentGuideVideo{{ID: "video-a"}}, }) if err != nil { t.Fatal(err) } jsonBody := string(body) if !strings.Contains(jsonBody, `"videos"`) { t.Fatalf("response must contain videos: %s", jsonBody) } if strings.Contains(jsonBody, `"videoIds"`) { t.Fatalf("response must not contain videoIds: %s", jsonBody) } } func TestBuildPingGuide(t *testing.T) { freeTrialID := primitive.NewObjectID() homeID := primitive.NewObjectID() contentID := primitive.NewObjectID() configs := map[string]paymentguidemod.PaymentGuide{ paymentguidemod.SceneHomeNewUserFreeTrial: { ID: freeTrialID, Scene: paymentguidemod.SceneHomeNewUserFreeTrial, Style: "BOTTOM_SHEET", Title: "free trial", Action: paymentguidemod.Action{Type: "NONE"}, }, paymentguidemod.SceneHomeOldUser: { ID: homeID, Scene: paymentguidemod.SceneHomeOldUser, Style: "BOTTOM_SHEET", Title: "open vip", ProductID: "product-id", DurationSeconds: 60, Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"}, }, paymentguidemod.SceneVIPContentUpdate: { ID: contentID, Scene: paymentguidemod.SceneVIPContentUpdate, }, } resp := buildPingGuide(paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentOldNeverPaid, 3, true, configs, nil) if !resp.Enabled || resp.Segment != paymentguidemod.SegmentOldNeverPaid { t.Fatalf("unexpected response header: %+v", resp) } if len(resp.Scenes) != len(pingGuideScenes) { t.Fatalf("scene count = %d, want %d", len(resp.Scenes), len(pingGuideScenes)) } freeTrial := resp.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial] if !freeTrial.Enabled || freeTrial.TotalWatchCount == nil || *freeTrial.TotalWatchCount != 3 || freeTrial.ConfigID != freeTrialID.Hex() { t.Fatalf("unexpected new-user free-trial scene: %+v", freeTrial) } home := resp.Scenes[paymentguidemod.SceneHomeOldUser] if !home.Enabled || home.Show == nil || !*home.Show || home.ConfigID != homeID.Hex() { t.Fatalf("unexpected ordinary scene: %+v", home) } if home.Action == nil || home.Action.Value != "product-id" { t.Fatalf("ordinary scene action missing: %+v", home) } content := resp.Scenes[paymentguidemod.SceneVIPContentUpdate] if !content.Enabled || content.Show != nil || content.ConfigID != "" { t.Fatalf("content-update Ping must expose enabled only: %+v", content) } missing := resp.Scenes[paymentguidemod.SceneVideoBack] if missing.Enabled || missing.Show == nil || *missing.Show { t.Fatalf("missing ordinary scene must be disabled: %+v", missing) } } func TestBuildPingGuideSuppressesShownAndGlobalDisabled(t *testing.T) { configID := primitive.NewObjectID() configs := map[string]paymentguidemod.PaymentGuide{ paymentguidemod.SceneVIPCenter: { ID: configID, Scene: paymentguidemod.SceneVIPCenter, Style: "CENTER_POPUP", Title: "shown", Description: "description", Cover: "cover.png", ProductID: "product-id", DurationSeconds: 0, Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"}, }, } resp := buildPingGuide( paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentNormal, 3, true, configs, map[primitive.ObjectID]bool{configID: true}, ) vipCenter := resp.Scenes[paymentguidemod.SceneVIPCenter] if !vipCenter.Enabled || vipCenter.Show == nil || *vipCenter.Show || vipCenter.ConfigID != configID.Hex() { t.Fatalf("shown configuration must retain its display payload: %+v", vipCenter) } if vipCenter.Description == nil || *vipCenter.Description != "description" || vipCenter.Cover == nil || *vipCenter.Cover != "cover.png" || vipCenter.ProductID == nil || *vipCenter.ProductID != "product-id" || vipCenter.DurationSeconds == nil || *vipCenter.DurationSeconds != 0 || vipCenter.Action == nil || vipCenter.Action.Value != "product-id" { t.Fatalf("shown configuration payload is incomplete: %+v", vipCenter) } disabled := buildPingGuide(paymentGuideSwitchState{}, paymentguidemod.SegmentNormal, 3, true, configs, nil) if disabled.Enabled { t.Fatal("global-disabled response must be disabled") } for scene, item := range disabled.Scenes { if item.Enabled { t.Fatalf("scene %s must be disabled", scene) } if scene != paymentguidemod.SceneVIPContentUpdate && (item.Show == nil || *item.Show) { t.Fatalf("ordinary scene %s must explicitly return show=false", scene) } } } func TestBuildPingGuideUsesSharedHomeSwitch(t *testing.T) { freeTrialID := primitive.NewObjectID() homeOldID := primitive.NewObjectID() videoBackID := primitive.NewObjectID() configs := map[string]paymentguidemod.PaymentGuide{ paymentguidemod.SceneHomeNewUserFreeTrial: {ID: freeTrialID, Scene: paymentguidemod.SceneHomeNewUserFreeTrial}, paymentguidemod.SceneHomeOldUser: {ID: homeOldID, Scene: paymentguidemod.SceneHomeOldUser}, paymentguidemod.SceneVideoBack: {ID: videoBackID, Scene: paymentguidemod.SceneVideoBack}, } resp := buildPingGuide( paymentGuideSwitchState{global: true, home: false}, paymentguidemod.SegmentNormal, 3, true, configs, nil, ) for _, scene := range []string{paymentguidemod.SceneHomeNewUserFreeTrial, paymentguidemod.SceneHomeOldUser} { item := resp.Scenes[scene] if item.Enabled || item.Show == nil || *item.Show || item.ConfigID != "" { t.Fatalf("home scene %s must be disabled by the shared switch: %+v", scene, item) } } videoBack := resp.Scenes[paymentguidemod.SceneVideoBack] if !videoBack.Enabled || videoBack.Show == nil || !*videoBack.Show || videoBack.ConfigID != videoBackID.Hex() { t.Fatalf("non-home scene must not be affected by the home switch: %+v", videoBack) } } func TestBuildPingGuideCountdownIsNotSuppressedByImpression(t *testing.T) { configID := primitive.NewObjectID() configs := map[string]paymentguidemod.PaymentGuide{ paymentguidemod.SceneDiscountCountdown: { ID: configID, Scene: paymentguidemod.SceneDiscountCountdown, Style: "CENTER_POPUP", Title: "限时优惠", Description: "前端本地倒计时", Cover: "countdown.png", ProductID: "product-id", DurationSeconds: 0, Action: paymentguidemod.Action{Type: "VIP_PRODUCT", Value: "product-id"}, }, } resp := buildPingGuide( paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentNormal, 3, true, configs, map[primitive.ObjectID]bool{configID: true}, ) countdown := resp.Scenes[paymentguidemod.SceneDiscountCountdown] if !countdown.Enabled || countdown.Show == nil || !*countdown.Show || countdown.ConfigID != configID.Hex() { t.Fatalf("countdown must remain displayable on every App launch: %+v", countdown) } if countdown.DurationSeconds == nil || *countdown.DurationSeconds != 0 { t.Fatalf("countdown must return the complete configured card: %+v", countdown) } } func TestPaymentGuidePingJSONOmitsContentUpdateShow(t *testing.T) { body, err := json.Marshal(buildPingGuide(paymentGuideSwitchState{}, paymentguidemod.SegmentUnregistered, 3, false, nil, nil)) if err != nil { t.Fatal(err) } var decoded struct { Scenes map[string]map[string]interface{} `json:"scenes"` } var envelope struct { Enabled bool `json:"enabled"` Segment string `json:"segment"` Scenes map[string]map[string]interface{} `json:"scenes"` } if err = json.Unmarshal(body, &envelope); err != nil { t.Fatal(err) } decoded.Scenes = envelope.Scenes if _, exists := decoded.Scenes[paymentguidemod.SceneHomeNewUser]; exists { t.Fatalf("legacy HOME_NEW_USER must not be returned in Ping: %s", body) } freeTrial := decoded.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial] if count, exists := freeTrial["totalWatchCount"]; !exists || count != float64(3) { t.Fatalf("new-user free-trial Ping must include totalWatchCount=3: %s", body) } if _, exists := decoded.Scenes[paymentguidemod.SceneVIPContentUpdate]["show"]; exists { t.Fatalf("VIP_CONTENT_UPDATE Ping must omit show: %s", body) } if show, exists := decoded.Scenes[paymentguidemod.SceneHomeOldUser]["show"]; !exists || show != false { t.Fatalf("home Ping scene must include show=false: %s", body) } } func TestPaymentGuidePingJSONKeepsConfiguredFieldsWhenShowFalse(t *testing.T) { configID := primitive.NewObjectID() body, err := json.Marshal(buildPingGuide( paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentNormal, 3, true, map[string]paymentguidemod.PaymentGuide{ paymentguidemod.SceneVideoBack: { ID: configID, Scene: paymentguidemod.SceneVideoBack, Style: "CENTER_POPUP", Title: "返回引导", Description: "", Cover: "", ProductID: "", DurationSeconds: 0, Action: paymentguidemod.Action{Type: "NONE"}, }, }, map[primitive.ObjectID]bool{configID: true}, )) if err != nil { t.Fatal(err) } var envelope struct { Scenes map[string]map[string]interface{} `json:"scenes"` } if err = json.Unmarshal(body, &envelope); err != nil { t.Fatal(err) } item := envelope.Scenes[paymentguidemod.SceneVideoBack] for _, field := range []string{"configId", "style", "title", "description", "cover", "productId", "durationSeconds", "action"} { if _, exists := item[field]; !exists { t.Fatalf("configured show=false scene must include %s: %s", field, body) } } if show, ok := item["show"].(bool); !ok || show { t.Fatalf("configured scene must preserve show=false: %s", body) } } func TestBuildPingGuideSuppressesExhaustedNewUserFreeTrialOnly(t *testing.T) { freeTrialID := primitive.NewObjectID() videoBackID := primitive.NewObjectID() resp := buildPingGuide( paymentGuideSwitchState{global: true, home: true}, paymentguidemod.SegmentNewNeverPaid, 3, false, map[string]paymentguidemod.PaymentGuide{ paymentguidemod.SceneHomeNewUserFreeTrial: { ID: freeTrialID, Scene: paymentguidemod.SceneHomeNewUserFreeTrial, Style: "BOTTOM_SHEET", Title: "free trial", Action: paymentguidemod.Action{Type: "NONE"}, }, paymentguidemod.SceneVideoBack: { ID: videoBackID, Scene: paymentguidemod.SceneVideoBack, Style: "BOTTOM_SHEET", Title: "video back", Action: paymentguidemod.Action{Type: "NONE"}, }, }, nil, ) if resp.Segment != paymentguidemod.SegmentNewNeverPaid { t.Fatalf("segment = %q, want %q", resp.Segment, paymentguidemod.SegmentNewNeverPaid) } freeTrial := resp.Scenes[paymentguidemod.SceneHomeNewUserFreeTrial] if !freeTrial.Enabled || freeTrial.Show == nil || *freeTrial.Show || freeTrial.ConfigID != freeTrialID.Hex() { t.Fatalf("exhausted free-trial scene must retain config with show=false: %+v", freeTrial) } videoBack := resp.Scenes[paymentguidemod.SceneVideoBack] if !videoBack.Enabled || videoBack.Show == nil || !*videoBack.Show || videoBack.ConfigID != videoBackID.Hex() { t.Fatalf("free-trial exhaustion must not suppress other matching scenes: %+v", videoBack) } }