package vipconfig import ( "context" "net/http" "net/http/httptest" "strings" "testing" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/walletclient" "hyapp-admin-server/internal/middleware" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/gin-gonic/gin" ) func TestGetProgramReturnsCamelCaseCompleteConfigAndBenefits(t *testing.T) { wallet := &fakeVIPConfigWallet{ programResponse: &walletv1.GetVipProgramConfigResponse{ Config: &walletv1.VipProgramConfig{ AppCode: "fami", ProgramType: "tiered_privilege_v1", LevelCount: 9, SameLevelExpiryPolicy: "extend_remaining", UpgradeExpiryPolicy: "replace_from_now", DowngradePurchasePolicy: "reject", BenefitInheritancePolicy: "target_only", GrantMode: "trial_card", TrialCardEnabled: true, Status: "active", ConfigVersion: 3, UpdatedByAdminId: 77, CreatedAtMs: 100, UpdatedAtMs: 200, }, Benefits: []*walletv1.VipBenefit{{ BenefitCode: "anti_kick", Name: "防踢", BenefitType: "function", UnlockLevel: 9, Status: "active", TrialEnabled: true, ExecutionScope: "room", MetadataJson: "{}", CreatedAtMs: 101, UpdatedAtMs: 201, }}, ServerTimeMs: 300, }, } recorder := serveVIPConfigRequest(t, wallet, http.MethodGet, "/admin/activity/vip-program", "") if recorder.Code != http.StatusOK { t.Fatalf("get program status = %d, body=%s", recorder.Code, recorder.Body.String()) } body := recorder.Body.String() for _, fragment := range []string{ `"programType":"tiered_privilege_v1"`, `"upgradeExpiryPolicy":"replace_from_now"`, `"updatedByAdminId":77`, `"benefitCode":"anti_kick"`, `"executionScope":"room"`, `"serverTimeMs":300`, } { if !strings.Contains(body, fragment) { t.Fatalf("get program response missing %s: %s", fragment, body) } } if strings.Contains(body, "program_type") || strings.Contains(body, "benefit_code") { t.Fatalf("admin contract must stay camelCase: %s", body) } if wallet.getProgramRequest.GetAppCode() != "fami" || wallet.getProgramRequest.GetRequestId() != "vip-config-test" { t.Fatalf("get program request mismatch: %+v", wallet.getProgramRequest) } } func TestUpdateProgramUsesContextAppAndAuthenticatedOperator(t *testing.T) { wallet := &fakeVIPConfigWallet{updateProgramResponse: &walletv1.UpdateAdminVipProgramConfigResponse{ Config: &walletv1.VipProgramConfig{AppCode: "fami", ProgramType: "tiered_privilege_v1", LevelCount: 9, Status: "active", ConfigVersion: 4}, ServerTimeMs: 400, }} body := `{"config":{"appCode":"lalu","programType":"tiered_privilege_v1","levelCount":9,"sameLevelExpiryPolicy":"extend_remaining","upgradeExpiryPolicy":"replace_from_now","downgradePurchasePolicy":"reject","benefitInheritancePolicy":"target_only","grantMode":"trial_card","trialCardEnabled":true,"status":"active","configVersion":999,"updatedByAdminId":999}}` recorder := serveVIPConfigRequest(t, wallet, http.MethodPut, "/admin/activity/vip-program", body) if recorder.Code != http.StatusOK { t.Fatalf("update program status = %d, body=%s", recorder.Code, recorder.Body.String()) } req := wallet.updateProgramRequest if req.GetAppCode() != "fami" || req.GetOperatorUserId() != 77 { t.Fatalf("program scope/operator mismatch: %+v", req) } if req.GetConfig().GetAppCode() != "fami" || req.GetConfig().GetConfigVersion() != 0 || req.GetConfig().GetUpdatedByAdminId() != 0 { t.Fatalf("client-controlled program audit fields leaked through: %+v", req.GetConfig()) } if !strings.Contains(recorder.Body.String(), `"configVersion":4`) || !strings.Contains(recorder.Body.String(), `"benefits":[]`) { t.Fatalf("update program response mismatch: %s", recorder.Body.String()) } } func TestUpdateLevelsForwardsExplicitBenefitsAndReturnsProgram(t *testing.T) { wallet := &fakeVIPConfigWallet{updateLevelsResponse: &walletv1.UpdateAdminVipLevelsResponse{ Levels: []*walletv1.VipLevel{{ Level: 2, Name: "VIP2", Status: "active", DurationMs: 2_592_000_000, Benefits: []*walletv1.VipBenefit{{BenefitCode: "room_entry_notice", Name: "进房通知", Status: "active", ExecutionScope: "room", MetadataJson: "{}"}}, }}, ProgramConfig: &walletv1.VipProgramConfig{AppCode: "fami", ProgramType: "tiered_privilege_v1", LevelCount: 9}, ServerTimeMs: 500, }} body := `{"levels":[{"level":2,"name":"VIP2","status":"active","priceCoin":2000,"durationMs":2592000000,"rewardResourceGroupId":22,"sortOrder":20,"requiredRechargeCoinAmount":123,"benefits":[{"benefitCode":"room_entry_notice","name":"进房通知","benefitType":"function","unlockLevel":2,"status":"active","trialEnabled":true,"resourceId":0,"resourceType":"","executionScope":"room","autoEquip":false,"sortOrder":10,"metadataJson":"{}"}]}]}` recorder := serveVIPConfigRequest(t, wallet, http.MethodPut, "/admin/activity/vip-levels", body) if recorder.Code != http.StatusOK { t.Fatalf("update levels status = %d, body=%s", recorder.Code, recorder.Body.String()) } req := wallet.updateLevelsRequest if req.GetAppCode() != "fami" || req.GetOperatorUserId() != 77 || len(req.GetLevels()) != 1 { t.Fatalf("update levels request mismatch: %+v", req) } level := req.GetLevels()[0] if level.GetRequiredRechargeCoinAmount() != 0 || len(level.GetBenefits()) != 1 || level.GetBenefits()[0].GetBenefitCode() != "room_entry_notice" { t.Fatalf("explicit level benefits mismatch: %+v", level) } if !strings.Contains(recorder.Body.String(), `"programConfig":{"appCode":"fami","programType":"tiered_privilege_v1","levelCount":9`) { t.Fatalf("level response must include program config: %s", recorder.Body.String()) } } func TestGrantTrialCardLeavesOptionalResourceUnsetAndUsesAuthenticatedOperator(t *testing.T) { wallet := &fakeVIPConfigWallet{grantTrialResponse: &walletv1.GrantVipTrialCardResponse{ TrialCard: &walletv1.VipTrialCard{TrialCardId: "trial-1", EntitlementId: "ent-1", UserId: 318705991371722752, Level: 5, DurationMs: 1_728_000_000, ExpiresAtMs: 2_000}, State: &walletv1.VipState{EffectiveSource: "none", EffectiveBenefits: []*walletv1.VipBenefit{}}, ServerTimeMs: 1_000, }} body := `{"targetUserId":"318705991371722752","level":5,"durationMs":1728000000,"reason":"manual"}` recorder := serveVIPConfigRequest(t, wallet, http.MethodPost, "/admin/activity/vip-trial-card-grants", body) if recorder.Code != http.StatusCreated { t.Fatalf("grant trial card status = %d, body=%s", recorder.Code, recorder.Body.String()) } req := wallet.grantTrialRequest if req.GetCommandId() != "admin_vip_trial_card_grant:vip-config-test" || req.GetAppCode() != "fami" || req.GetOperatorUserId() != 77 { t.Fatalf("trial grant command context mismatch: %+v", req) } if req.ResourceId != nil || req.GetTargetUserId() != 318705991371722752 || req.GetDurationMs() != 1_728_000_000 { t.Fatalf("trial grant payload mismatch: %+v", req) } if !strings.Contains(recorder.Body.String(), `"trialCardId":"trial-1"`) || !strings.Contains(recorder.Body.String(), `"userId":"318705991371722752"`) { t.Fatalf("trial grant response mismatch: %s", recorder.Body.String()) } } func serveVIPConfigRequest(t *testing.T, wallet walletclient.Client, method string, path string, body string) *httptest.ResponseRecorder { t.Helper() gin.SetMode(gin.TestMode) router := gin.New() router.Use(func(c *gin.Context) { c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "fami")) c.Set(middleware.ContextRequestID, "vip-config-test") c.Set(middleware.ContextUserID, uint(77)) c.Set(middleware.ContextUsername, "vip-operator") c.Set(middleware.ContextPermissions, []string{"vip-config:view", "vip-config:update", "vip-config:grant"}) c.Next() }) RegisterRoutes(router.Group(""), New(wallet, nil)) recorder := httptest.NewRecorder() request := httptest.NewRequest(method, path, strings.NewReader(body)) if body != "" { request.Header.Set("Content-Type", "application/json") } router.ServeHTTP(recorder, request) return recorder } type fakeVIPConfigWallet struct { walletclient.Client programResponse *walletv1.GetVipProgramConfigResponse updateProgramResponse *walletv1.UpdateAdminVipProgramConfigResponse updateLevelsResponse *walletv1.UpdateAdminVipLevelsResponse grantTrialResponse *walletv1.GrantVipTrialCardResponse getProgramRequest *walletv1.GetVipProgramConfigRequest updateProgramRequest *walletv1.UpdateAdminVipProgramConfigRequest updateLevelsRequest *walletv1.UpdateAdminVipLevelsRequest grantTrialRequest *walletv1.GrantVipTrialCardRequest } func (f *fakeVIPConfigWallet) GetVipProgramConfig(_ context.Context, req *walletv1.GetVipProgramConfigRequest) (*walletv1.GetVipProgramConfigResponse, error) { f.getProgramRequest = req return f.programResponse, nil } func (f *fakeVIPConfigWallet) UpdateAdminVipProgramConfig(_ context.Context, req *walletv1.UpdateAdminVipProgramConfigRequest) (*walletv1.UpdateAdminVipProgramConfigResponse, error) { f.updateProgramRequest = req return f.updateProgramResponse, nil } func (f *fakeVIPConfigWallet) UpdateAdminVipLevels(_ context.Context, req *walletv1.UpdateAdminVipLevelsRequest) (*walletv1.UpdateAdminVipLevelsResponse, error) { f.updateLevelsRequest = req return f.updateLevelsResponse, nil } func (f *fakeVIPConfigWallet) GrantVipTrialCard(_ context.Context, req *walletv1.GrantVipTrialCardRequest) (*walletv1.GrantVipTrialCardResponse, error) { f.grantTrialRequest = req return f.grantTrialResponse, nil }