package opscenter import ( "context" "net/http" "net/http/httptest" "strings" "testing" "hyapp-admin-server/internal/integration/luckygiftadmin" "hyapp-admin-server/internal/middleware" luckygiftmodule "hyapp-admin-server/internal/modules/luckygift" luckygiftv1 "hyapp.local/api/proto/luckygift/v1" "github.com/gin-gonic/gin" ) type poolPermissionClient struct { luckygiftadmin.Client } func (poolPermissionClient) AdjustLuckyGiftPoolBalance(_ context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) { return &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{ Adjustment: &luckygiftv1.LuckyGiftPoolAdjustment{ AdjustmentId: req.GetAdjustmentId(), AppCode: req.GetMeta().GetAppCode(), PoolId: req.GetPoolId(), StrategyVersion: req.GetStrategyVersion(), Direction: req.GetDirection(), AmountCoins: req.GetAmountCoins(), }, Pool: &luckygiftv1.LuckyGiftPoolBalance{ AppCode: req.GetMeta().GetAppCode(), PoolId: req.GetPoolId(), StrategyVersion: req.GetStrategyVersion(), Materialized: true, }, }, nil } func TestRegisterRoutesExposesStrategyPoolAdjustmentEndpoints(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() RegisterRoutes(engine.Group("/api/v1"), New(nil, luckygiftmodule.New(poolPermissionClient{}, 0, nil))) routes := make(map[string]struct{}) for _, route := range engine.Routes() { routes[route.Method+" "+route.Path] = struct{}{} } for _, expected := range []string{ "POST /api/v1/admin/ops-center/lucky-gifts/pools/credit", "POST /api/v1/admin/ops-center/lucky-gifts/pools/debit", "GET /api/v1/admin/ops-center/lucky-gifts/experiments", "POST /api/v1/admin/ops-center/lucky-gifts/experiments", "PATCH /api/v1/admin/ops-center/lucky-gifts/experiments/:experimentId", "PUT /api/v1/admin/ops-center/lucky-gifts/experiments/:experimentId/overrides", "POST /api/v1/admin/ops-center/lucky-gifts/experiments/:experimentId/end", } { if _, ok := routes[expected]; !ok { t.Fatalf("missing route %s; actual=%v", expected, routes) } } } type experimentPermissionClient struct { luckygiftadmin.Client } func (experimentPermissionClient) ListLuckyGiftExperiments(_ context.Context, _ *luckygiftv1.ListLuckyGiftExperimentsRequest) (*luckygiftv1.ListLuckyGiftExperimentsResponse, error) { return &luckygiftv1.ListLuckyGiftExperimentsResponse{}, nil } func (experimentPermissionClient) CreateLuckyGiftExperiment(_ context.Context, req *luckygiftv1.CreateLuckyGiftExperimentRequest) (*luckygiftv1.CreateLuckyGiftExperimentResponse, error) { return &luckygiftv1.CreateLuckyGiftExperimentResponse{ Experiment: &luckygiftv1.LuckyGiftExperiment{ExperimentId: "lucky_exp_route", Name: req.GetName()}, TreatmentConfig: req.GetTreatmentConfig(), }, nil } func (experimentPermissionClient) UpdateLuckyGiftExperiment(_ context.Context, req *luckygiftv1.UpdateLuckyGiftExperimentRequest) (*luckygiftv1.UpdateLuckyGiftExperimentResponse, error) { return &luckygiftv1.UpdateLuckyGiftExperimentResponse{ Experiment: &luckygiftv1.LuckyGiftExperiment{ExperimentId: req.GetExperimentId()}, }, nil } func TestExperimentRoutesRequireExperimentPermission(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string method string target string body string permission string wantStatus int }{ { name: "list allowed with view", method: http.MethodGet, target: "/api/v1/admin/ops-center/lucky-gifts/experiments?app_code=lalu", body: "", permission: "lucky-gift:view", wantStatus: http.StatusOK, }, { name: "create allowed with experiment", method: http.MethodPost, target: "/api/v1/admin/ops-center/lucky-gifts/experiments?app_code=lalu", body: `{"name":"exp","treatment_traffic_percent":5,"config":{"pool_id":"lucky"}}`, permission: "lucky-gift:experiment", wantStatus: http.StatusOK, }, { name: "create rejects update permission", method: http.MethodPost, target: "/api/v1/admin/ops-center/lucky-gifts/experiments?app_code=lalu", body: `{"name":"exp","treatment_traffic_percent":5,"config":{"pool_id":"lucky"}}`, permission: "lucky-gift:update", wantStatus: http.StatusForbidden, }, { name: "patch allowed with experiment", method: http.MethodPatch, target: "/api/v1/admin/ops-center/lucky-gifts/experiments/lucky_exp_route?app_code=lalu", body: `{"status":"paused"}`, permission: "lucky-gift:experiment", wantStatus: http.StatusOK, }, { name: "patch rejects view permission", method: http.MethodPatch, target: "/api/v1/admin/ops-center/lucky-gifts/experiments/lucky_exp_route?app_code=lalu", body: `{"status":"paused"}`, permission: "lucky-gift:view", wantStatus: http.StatusForbidden, }, { name: "end rejects view permission", method: http.MethodPost, target: "/api/v1/admin/ops-center/lucky-gifts/experiments/lucky_exp_route/end?app_code=lalu", body: `{"winner_arm":"control"}`, permission: "lucky-gift:view", wantStatus: http.StatusForbidden, }, { name: "overrides rejects update permission", method: http.MethodPut, target: "/api/v1/admin/ops-center/lucky-gifts/experiments/lucky_exp_route/overrides?app_code=lalu", body: `{"upserts":[]}`, permission: "lucky-gift:update", wantStatus: http.StatusForbidden, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine := gin.New() engine.Use(func(c *gin.Context) { c.Set(middleware.ContextPermissions, []string{tt.permission}) c.Set(middleware.ContextUserID, uint(7)) c.Next() }) RegisterRoutes(engine.Group("/api/v1"), New(nil, luckygiftmodule.New(experimentPermissionClient{}, 0, nil))) recorder := httptest.NewRecorder() request := httptest.NewRequest(tt.method, tt.target, strings.NewReader(tt.body)) request.Header.Set("Content-Type", "application/json") engine.ServeHTTP(recorder, request) if recorder.Code != tt.wantStatus { t.Fatalf("status=%d want=%d body=%s", recorder.Code, tt.wantStatus, recorder.Body.String()) } }) } } func TestPoolAdjustmentRoutesRequireDirectionSpecificPermission(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string path string permission string wantStatus int }{ {name: "credit allowed", path: "credit", permission: "lucky-gift:pool-credit", wantStatus: http.StatusOK}, {name: "credit rejects debit permission", path: "credit", permission: "lucky-gift:pool-debit", wantStatus: http.StatusForbidden}, {name: "debit allowed", path: "debit", permission: "lucky-gift:pool-debit", wantStatus: http.StatusOK}, {name: "debit rejects view permission", path: "debit", permission: "lucky-gift:view", wantStatus: http.StatusForbidden}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine := gin.New() engine.Use(func(c *gin.Context) { c.Set(middleware.ContextPermissions, []string{tt.permission}) c.Set(middleware.ContextUserID, uint(7)) c.Next() }) RegisterRoutes(engine.Group("/api/v1"), New(nil, luckygiftmodule.New(poolPermissionClient{}, 0, nil))) recorder := httptest.NewRecorder() request := httptest.NewRequest( http.MethodPost, "/api/v1/admin/ops-center/lucky-gifts/pools/"+tt.path+"?app_code=aslan&pool_id=lucky&strategy_version=dynamic_v3", strings.NewReader(`{"adjustment_id":"route-adjustment","amount_coins":1,"reason":"permission test"}`), ) request.Header.Set("Content-Type", "application/json") engine.ServeHTTP(recorder, request) if recorder.Code != tt.wantStatus { t.Fatalf("status=%d want=%d body=%s", recorder.Code, tt.wantStatus, recorder.Body.String()) } }) } }