package handler import ( "fmt" "net/http" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "gorm.io/gorm" "github.com/wangjia/jiu/backend/internal/middleware" "github.com/wangjia/jiu/backend/internal/model" "github.com/wangjia/jiu/backend/testutil" ) // setupGuardedRouter 复刻 router.go:98 的真实链路: // JWT → ReadOnly → LicenseGuard,后接真实 product handler。 // 用于「写权限鉴权」的全链路集成测试(后端侧前后端契约)。 func setupGuardedRouter(db *gorm.DB) *gin.Engine { productH := NewProductHandler(db) r := gin.New() r.Use(gin.Recovery()) api := r.Group("/api/v1") api.Use(middleware.JWT(db), middleware.ReadOnly(), middleware.LicenseGuard(db)) products := api.Group("/products") products.GET("", productH.List) products.POST("", productH.Create) return r } // seedLicense 给店铺写入一条有效授权,daysAgo>0 表示已过期天数,<=0 表示未过期(剩余天数)。 func seedLicense(t *testing.T, db *gorm.DB, shopID uint64, daysAgo int) { exp := time.Now().Add(-time.Duration(daysAgo) * 24 * time.Hour) assert.NoError(t, db.Create(&model.License{ ShopID: shopID, IsActive: true, Type: "trial", LicenseKey: fmt.Sprintf("KEY-%d-%d", shopID, time.Now().UnixNano()), ExpiresAt: &exp, }).Error) } // TestWriteAccessMatrix 钉死「角色 × 授权阶段」对写/读操作的 403/200 契约。 // 这是前端唯一依赖的契约,任何回归都会被这张表抓到。 func TestWriteAccessMatrix(t *testing.T) { gin.SetMode(gin.TestMode) db := testutil.SetupTestDB() r := setupGuardedRouter(db) // 每档一个独立店铺,避免 LicenseGuard 的 30s 缓存按 shopID 串扰。 type tc struct { name string role string daysExpired int // >0 已过期天数;-30 表示未过期 wantPostFwd bool // POST 是否应放行(非 403) wantGetFwd bool // GET 是否应放行 wantCode string // 期望 403 body 的 code(空则不校验) wantPhase string // 期望 403 body 的 phase(空则不校验) } cases := []tc{ {"operator_normal", "operator", -30, true, true, "", ""}, {"operator_grace", "operator", 3, true, true, "", ""}, {"operator_readonly", "operator", 10, false, true, "", "readonly"}, {"operator_locked", "operator", 20, false, false, "", "locked"}, {"readonly_role", "readonly", -30, false, true, "READONLY_USER", ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { shop := testutil.CreateTestShop(db, c.name) shopID := shop.ID user := testutil.CreateTestUser(db, shopID, "u_"+c.name, "password123", c.role) seedLicense(t, db, shopID, c.daysExpired) token := getAuthToken(user.ID, shopID, c.role) // POST /products wPost := makeRequest(r, http.MethodPost, "/api/v1/products", token, jsonBody("name", "集成测试酒")) if c.wantPostFwd { assert.NotEqual(t, http.StatusForbidden, wPost.Code, "POST 应放行") } else { assert.Equal(t, http.StatusForbidden, wPost.Code, "POST 应被拦截") body := parseResponse(wPost) if c.wantCode != "" { assert.Equal(t, c.wantCode, body["code"]) } if c.wantPhase != "" { assert.Equal(t, c.wantPhase, body["phase"]) } } // GET /products wGet := makeRequest(r, http.MethodGet, "/api/v1/products", token, nil) if c.wantGetFwd { assert.NotEqual(t, http.StatusForbidden, wGet.Code, "GET 应放行") } else { assert.Equal(t, http.StatusForbidden, wGet.Code, "GET 应被拦截") } }) } }