package vipcardexperimentser import ( "testing" "time" "91porn-server/app/proto" "91porn-server/models/v/productmod" "91porn-server/models/v/vipcardexperimentmod" "go.mongodb.org/mongo-driver/bson/primitive" ) func TestApplyVariantOrdersFiltersAndBadgesProducts(t *testing.T) { first := primitive.NewObjectID() second := primitive.NewObjectID() unaffected := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{ { Position: "vip", List: []proto.VIPListRes{ {Product: productmod.Product{ID: first}}, {Product: productmod.Product{ID: second}}, }, }, { Position: "coin", List: []proto.VIPListRes{ {Product: productmod.Product{ID: unaffected}}, }, }, }} config := vipcardexperimentmod.VariantConfig{ DefaultProductID: second, ProductIDs: []primitive.ObjectID{second, first}, ProductBadges: []vipcardexperimentmod.ProductBadge{{ ProductID: second, BadgeType: vipcardexperimentmod.BadgeMostPopular, BadgeText: "最受欢迎", }}, } if !applyVariant(&response, config) { t.Fatal("applyVariant() = false, want true") } vipProducts := response.List[0].List if len(vipProducts) != 2 || vipProducts[0].ID != second || vipProducts[1].ID != first { t.Fatalf("VIP product order = %#v", vipProducts) } if vipProducts[0].Sort != 1 || vipProducts[1].Sort != 2 { t.Fatalf("VIP product sort values = %#v", vipProducts) } if vipProducts[0].BadgeType != vipcardexperimentmod.BadgeMostPopular || vipProducts[0].BadgeText != "最受欢迎" { t.Fatalf("badge not applied: %#v", vipProducts[0]) } if len(response.List[1].List) != 1 || response.List[1].List[0].ID != unaffected { t.Fatalf("unaffected position changed: %#v", response.List[1].List) } } func TestApplyVariantOrdersIndependentPositionsByFirstConfiguredProduct(t *testing.T) { firstVIP := primitive.NewObjectID() secondVIP := primitive.NewObjectID() presale := primitive.NewObjectID() unaffected := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{ { Position: "会员卡", PositionID: "vip", List: []proto.VIPListRes{ {Product: productmod.Product{ID: firstVIP}}, {Product: productmod.Product{ID: secondVIP}}, }, }, { Position: "预售卡", PositionID: "presale", List: []proto.VIPListRes{ {Product: productmod.Product{ID: presale}}, }, }, { Position: "金币", PositionID: "coin", List: []proto.VIPListRes{ {Product: productmod.Product{ID: unaffected}}, }, }, }} config := vipcardexperimentmod.VariantConfig{ ProductIDs: []primitive.ObjectID{presale, secondVIP, firstVIP}, } if !applyVariant(&response, config) { t.Fatal("applyVariant() = false, want true") } if len(response.List) != 3 { t.Fatalf("positions = %#v, want member, presale and coin positions", response.List) } if response.List[0].Position != "预售卡" || response.List[0].PositionID != "presale" || len(response.List[0].List) != 1 || response.List[0].List[0].ID != presale || response.List[0].List[0].Sort != 1 { t.Fatalf("presale position = %#v", response.List[0]) } wantVIPProducts := []primitive.ObjectID{secondVIP, firstVIP} if response.List[1].Position != "会员卡" || response.List[1].PositionID != "vip" { t.Fatalf("member position = %#v", response.List[1]) } if len(response.List[1].List) != len(wantVIPProducts) { t.Fatalf("ordered member products = %#v", response.List[1].List) } for index, productID := range wantVIPProducts { if response.List[1].List[index].ID != productID { t.Fatalf("ordered member product %d = %s, want %s", index, response.List[1].List[index].ID, productID) } if response.List[1].List[index].Sort != index+2 { t.Fatalf("ordered member product %d sort = %d, want %d", index, response.List[1].List[index].Sort, index+2) } } if response.List[2].Position != "金币" || len(response.List[2].List) != 1 || response.List[2].List[0].ID != unaffected { t.Fatalf("unaffected position = %#v", response.List[2]) } } func TestApplyVariantKeepsInterleavedProductsInOnePosition(t *testing.T) { firstVIP := primitive.NewObjectID() secondVIP := primitive.NewObjectID() presale := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{ { Position: "会员卡", PositionID: "vip", List: []proto.VIPListRes{ {Product: productmod.Product{ID: firstVIP}}, {Product: productmod.Product{ID: secondVIP}}, }, }, { Position: "预售卡", PositionID: "presale", List: []proto.VIPListRes{ {Product: productmod.Product{ID: presale}}, }, }, }} config := vipcardexperimentmod.VariantConfig{ ProductIDs: []primitive.ObjectID{secondVIP, presale, firstVIP}, } if !applyVariant(&response, config) { t.Fatal("applyVariant() = false, want true") } if len(response.List) != 2 { t.Fatalf("positions = %#v, want one member and one presale position", response.List) } if response.List[0].PositionID != "vip" || len(response.List[0].List) != 2 || response.List[0].List[0].ID != secondVIP || response.List[0].List[0].Sort != 1 || response.List[0].List[1].ID != firstVIP || response.List[0].List[1].Sort != 3 { t.Fatalf("member position = %#v", response.List[0]) } if response.List[1].PositionID != "presale" || len(response.List[1].List) != 1 || response.List[1].List[0].ID != presale || response.List[1].List[0].Sort != 2 { t.Fatalf("presale position = %#v", response.List[1]) } } func TestFilterOtherVariantProductsRemovesExclusivePresalePosition(t *testing.T) { shared := primitive.NewObjectID() currentOnly := primitive.NewObjectID() otherOnlyPresale := primitive.NewObjectID() unaffected := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{ { Position: "vip", List: []proto.VIPListRes{ {Product: productmod.Product{ID: shared}}, {Product: productmod.Product{ID: currentOnly}}, }, }, { Position: "advance-card", List: []proto.VIPListRes{ {Product: productmod.Product{ID: otherOnlyPresale}}, }, }, { Position: "coin", List: []proto.VIPListRes{ {Product: productmod.Product{ID: unaffected}}, }, }, }} filterOtherVariantProducts( &response, []primitive.ObjectID{shared, currentOnly}, []primitive.ObjectID{shared, otherOnlyPresale}, ) if len(response.List) != 2 { t.Fatalf("positions = %#v, want vip and unaffected coin positions", response.List) } if response.List[0].Position != "vip" || len(response.List[0].List) != 2 { t.Fatalf("current variant products changed: %#v", response.List[0]) } if response.List[1].Position != "coin" || len(response.List[1].List) != 1 || response.List[1].List[0].ID != unaffected { t.Fatalf("unaffected position changed: %#v", response.List[1]) } } func TestApplyVariantFallsBackWhenDefaultProductIsUnavailable(t *testing.T) { available := primitive.NewObjectID() missingDefault := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{{ Position: "vip", List: []proto.VIPListRes{{ Product: productmod.Product{ID: available}, }}, }}} original := response.List[0].List config := vipcardexperimentmod.VariantConfig{ DefaultProductID: missingDefault, ProductIDs: []primitive.ObjectID{available, missingDefault}, } if applyVariant(&response, config) { t.Fatal("applyVariant() = true, want false") } if len(response.List[0].List) != len(original) { t.Fatalf("legacy response must remain unchanged: %#v", response.List[0].List) } } func TestApplyVariantAllowsMissingDefaultProduct(t *testing.T) { first := primitive.NewObjectID() second := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{{ Position: "vip", List: []proto.VIPListRes{ {Product: productmod.Product{ID: first}}, {Product: productmod.Product{ID: second}}, }, }}} config := vipcardexperimentmod.VariantConfig{ ProductIDs: []primitive.ObjectID{second, first}, } if !applyVariant(&response, config) { t.Fatal("applyVariant() = false, want true") } products := response.List[0].List if len(products) != 2 || products[0].ID != second || products[1].ID != first { t.Fatalf("VIP product order = %#v", products) } } func TestApplyExperimentOmitsMissingDefaultProduct(t *testing.T) { productID := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{{ Position: "vip", List: []proto.VIPListRes{{ Product: productmod.Product{ID: productID}, }}, }}} config := vipcardexperimentmod.VariantConfig{ ProductIDs: []primitive.ObjectID{productID}, } if !applyExperiment( &response, "optional-default-product", vipcardexperimentmod.VariantA, config, ) { t.Fatal("applyExperiment() = false, want true") } if response.DefaultProductID != "" { t.Fatalf("DefaultProductID = %q, want empty", response.DefaultProductID) } } func TestApplyExperimentIncludesUIConfig(t *testing.T) { productID := primitive.NewObjectID() uiConfig := &vipcardexperimentmod.UIConfig{ BackgroundImage: "vip-card-skin-a.png", BadgeStyles: []vipcardexperimentmod.BadgeStyle{{ BadgeType: vipcardexperimentmod.BadgeNewUser, BackgroundColor: "#F04432", TextColor: "#FFFFFF", }}, } config := vipcardexperimentmod.VariantConfig{ SkinKey: "vip-card-skin-a", DefaultProductID: productID, ProductIDs: []primitive.ObjectID{productID}, UIConfig: uiConfig, } response := proto.ProductRes{List: []proto.ProductList{{ Position: "vip", List: []proto.VIPListRes{{ Product: productmod.Product{ID: productID}, }}, }}} if !applyExperiment( &response, "vip-card-test", vipcardexperimentmod.VariantA, config, ) { t.Fatal("applyExperiment() = false, want true") } if response.ExperimentID != "vip-card-test" || response.ExperimentStatus != vipcardexperimentmod.StatusActive || response.Variant != vipcardexperimentmod.VariantA || response.SkinKey != "vip-card-skin-a" || response.DefaultProductID != productID.Hex() { t.Fatalf("experiment metadata = %#v", response) } if response.UIConfig == nil || response.UIConfig.BackgroundImage != "vip-card-skin-a.png" || len(response.UIConfig.BadgeStyles) != 1 { t.Fatalf("UI config = %#v", response.UIConfig) } uiConfig.BackgroundImage = "changed.png" uiConfig.BadgeStyles[0].BackgroundColor = "#000000" if response.UIConfig.BackgroundImage != "vip-card-skin-a.png" || response.UIConfig.BadgeStyles[0].BackgroundColor != "#F04432" { t.Fatalf("response UI config aliases stored config: %#v", response.UIConfig) } } func TestApplyExperimentOmitsUIConfigForLegacyExperiment(t *testing.T) { productID := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{{ Position: "vip", List: []proto.VIPListRes{{ Product: productmod.Product{ID: productID}, }}, }}} config := vipcardexperimentmod.VariantConfig{ DefaultProductID: productID, ProductIDs: []primitive.ObjectID{productID}, } if !applyExperiment( &response, "legacy-experiment", vipcardexperimentmod.VariantA, config, ) { t.Fatal("applyExperiment() = false, want true") } if response.UIConfig != nil { t.Fatalf("legacy UI config = %#v, want nil", response.UIConfig) } } func TestApplyExperimentFailureDoesNotExposeExperimentUI(t *testing.T) { available := primitive.NewObjectID() missingDefault := primitive.NewObjectID() response := proto.ProductRes{List: []proto.ProductList{{ Position: "vip", List: []proto.VIPListRes{{ Product: productmod.Product{ID: available}, }}, }}} config := vipcardexperimentmod.VariantConfig{ SkinKey: "vip-card-skin-a", DefaultProductID: missingDefault, ProductIDs: []primitive.ObjectID{available, missingDefault}, UIConfig: &vipcardexperimentmod.UIConfig{ BackgroundImage: "vip-card-skin-a.png", }, } if applyExperiment( &response, "unusable-experiment", vipcardexperimentmod.VariantA, config, ) { t.Fatal("applyExperiment() = true, want false") } if response.ExperimentID != "" || response.Variant != "" || response.SkinKey != "" || response.DefaultProductID != "" || response.UIConfig != nil { t.Fatalf("failed experiment leaked metadata: %#v", response) } if len(response.List) != 1 || len(response.List[0].List) != 1 || response.List[0].List[0].ID != available { t.Fatalf("failed experiment changed product list: %#v", response.List) } } func TestApplyToProductResponseSkipsAnonymousUser(t *testing.T) { response := proto.ProductRes{} if err := ApplyToProductResponse(0, &response, time.Now()); err != nil { t.Fatalf("ApplyToProductResponse() error = %v", err) } if response.ExperimentStatus != vipcardexperimentmod.StatusDisabled || response.ExperimentID != "" || response.UIConfig != nil { t.Fatalf("anonymous response contains experiment config: %#v", response) } } func TestValidateEventChecksStableAssignmentAndProduct(t *testing.T) { productID := primitive.NewObjectID() experiment := &vipcardexperimentmod.Experiment{ ExperimentID: "experiment", TrafficA: 100, TrafficB: 0, VariantA: vipcardexperimentmod.VariantConfig{ DefaultProductID: productID, ProductIDs: []primitive.ObjectID{productID}, }, } now := time.Now() input := EventInput{ EventID: "event-1", EventName: vipcardexperimentmod.EventProductImpression, SessionID: "session-1", OccurredAt: now, ExperimentID: experiment.ExperimentID, Variant: vipcardexperimentmod.VariantA, ProductID: productID, } event, _, err := validateEvent( 123, input, now, map[string]*vipcardexperimentmod.Experiment{experiment.ExperimentID: experiment}, ) if err != nil { t.Fatalf("validateEvent() error = %v", err) } if event.UID != 123 || event.ProductID != productID { t.Fatalf("event = %#v", event) } input.Variant = vipcardexperimentmod.VariantB if _, _, err = validateEvent( 123, input, now, map[string]*vipcardexperimentmod.Experiment{experiment.ExperimentID: experiment}, ); err == nil { t.Fatal("expected mismatched assignment to fail") } } func TestEventOccurredWithinExperiment(t *testing.T) { now := time.Now() start := now.Add(-time.Hour) end := now.Add(time.Hour) experiment := &vipcardexperimentmod.Experiment{ PublishedAt: start, EndAt: &end, } if !eventOccurredWithinExperiment(experiment, now) { t.Fatal("event during experiment must be accepted") } if eventOccurredWithinExperiment(experiment, start.Add(-10*time.Minute)) { t.Fatal("event before experiment must be rejected") } if eventOccurredWithinExperiment(experiment, end.Add(10*time.Minute)) { t.Fatal("event after experiment must be rejected") } disabled := now.Add(-time.Minute) experiment.DisabledAt = &disabled if eventOccurredWithinExperiment(experiment, now.Add(10*time.Minute)) { t.Fatal("event after disable must be rejected") } }