package middleware import ( "context" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/store" ) // fakeRoleResolver 按 (tenant,user) 返回预置角色,用于隔离测试门控逻辑(不碰 DB)。 type fakeRoleResolver struct{ role string } func (f fakeRoleResolver) MemberRole(context.Context, string, string) string { return f.role } // 组装一条 /tasks 路由,模拟 RequireAuth+TenantContext 已注入 uid/tenant_id,再挂门控。 func roleGateEngine(uid, tenant, memberRole, minRole string) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { if uid != "" { c.Set(CtxUserID, uid) } if tenant != "" { c.Set(CtxTenantID, tenant) } c.Next() }) r.POST("/tasks", RequireTenantRole(fakeRoleResolver{role: memberRole}, minRole), func(c *gin.Context) { c.String(http.StatusOK, "submitted") }) return r } func TestRequireTenantRole_Gate(t *testing.T) { cases := []struct { name string uid string tenant string memberRole string wantCode int }{ {"owner 放行", "u1", "t1", store.RoleOwner, http.StatusOK}, {"admin 放行", "u1", "t1", store.RoleAdmin, http.StatusOK}, {"member 放行", "u1", "t1", store.RoleMember, http.StatusOK}, {"billing_admin 拦下(财务只读不能提交)", "u1", "t1", store.RoleBillingAdmin, http.StatusForbidden}, {"viewer 拦下(只读不能提交)", "u1", "t1", store.RoleViewer, http.StatusForbidden}, {"非成员(空角色) 拦下", "u1", "t1", "", http.StatusForbidden}, {"未登录 401", "", "t1", store.RoleOwner, http.StatusUnauthorized}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { r := roleGateEngine(tc.uid, tc.tenant, tc.memberRole, store.RoleMember) w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/tasks", nil)) if w.Code != tc.wantCode { t.Errorf("状态码=%d, 期望 %d(body=%s)", w.Code, tc.wantCode, w.Body.String()) } }) } }