Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
@@ -0,0 +1,397 @@
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)
}
}