Files
jiu/backend/internal/handler/product_option_test.go
T
wangjia a51a44056d feat(backend): 库存按商品过滤 + 香型(分类)字典 CRUD
商品域富数据/重组所需后端:
- GET /inventory 加 product_id 过滤 → 解锁商品详情「当前库存/货值/各仓库分布」
  (配合已支持 product_id 的 /inventory/logs,详情可拼出大半富数据)
- 新增 /product-options/categories 香型字典 CRUD(ProductCategory 模型已存在,
  补管理接口)→ 商品列表 5-tab 重组的最后一块字典
均守多租户 shop_id;新增 3 个测试(FilterByProduct/CategoryCRUD/CategoryIsolation)。
go build/vet/test 全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
2026-07-01 20:46:52 +08:00

158 lines
5.5 KiB
Go

package handler
import (
"fmt"
"net/http"
"testing"
"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"
)
// setupOptionRouter 注册 product-options 名称路由(含 JWT)。
func setupOptionRouter(db *gorm.DB) *gin.Engine {
h := NewProductOptionHandler(db)
r := gin.New()
r.Use(gin.Recovery())
api := r.Group("/api/v1")
api.Use(middleware.JWT(db))
names := api.Group("/product-options/names")
names.GET("", h.ListNames)
return r
}
func TestProductOptionHandler_ListNamesKeyword(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "OPT001")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupOptionRouter(db)
db.Create(&model.ProductNameOption{
TenantBase: model.TenantBase{ShopID: shop.ID}, Code: "P001", Name: "茅台",
})
db.Create(&model.ProductNameOption{
TenantBase: model.TenantBase{ShopID: shop.ID}, Code: "P002", Name: "五粮液",
})
// 1. 无 keyword → 返回全部
w := makeRequest(r, "GET", "/api/v1/product-options/names", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
data := parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 2)
// 2. keyword 命中名称
w = makeRequest(r, "GET", "/api/v1/product-options/names?keyword=五粮", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
data = parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 1)
assert.Equal(t, "五粮液", data[0].(map[string]interface{})["name"])
// 3. keyword 命中编码
w = makeRequest(r, "GET", "/api/v1/product-options/names?keyword=P001", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
data = parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 1)
assert.Equal(t, "茅台", data[0].(map[string]interface{})["name"])
// 4. keyword 无命中 → 空
w = makeRequest(r, "GET", "/api/v1/product-options/names?keyword=不存在", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
data = parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 0)
}
func TestProductOptionHandler_ListNamesIsolation(t *testing.T) {
db := testutil.SetupTestDB()
shopA := testutil.CreateTestShop(db, "OPT_A")
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
shopB := testutil.CreateTestShop(db, "OPT_B")
r := setupOptionRouter(db)
db.Create(&model.ProductNameOption{
TenantBase: model.TenantBase{ShopID: shopB.ID}, Name: "他店商品",
})
// A 店即使 keyword 命中 B 店数据也查不到(shop_id 隔离)
w := makeRequest(r, "GET", "/api/v1/product-options/names?keyword=他店", tokenA, nil)
assert.Equal(t, http.StatusOK, w.Code)
data := parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 0)
}
// setupCategoryRouter 注册 香型/分类 字典 CRUD 路由(含 JWT)。
func setupCategoryRouter(db *gorm.DB) *gin.Engine {
h := NewProductOptionHandler(db)
r := gin.New()
r.Use(gin.Recovery())
api := r.Group("/api/v1")
api.Use(middleware.JWT(db))
cats := api.Group("/product-options/categories")
cats.GET("", h.ListCategories)
cats.POST("", h.CreateCategory)
cats.PUT("/:id", h.UpdateCategory)
cats.DELETE("/:id", h.DeleteCategory)
return r
}
func TestProductOptionHandler_CategoryCRUD(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "CAT001")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupCategoryRouter(db)
// 1. 创建
w := makeRequest(r, "POST", "/api/v1/product-options/categories", token,
map[string]interface{}{"name": "酱香"})
assert.Equal(t, http.StatusCreated, w.Code)
id := parseResponse(w)["data"].(map[string]interface{})["id"]
// 2. 列表命中
w = makeRequest(r, "GET", "/api/v1/product-options/categories", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
data := parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 1)
assert.Equal(t, "酱香", data[0].(map[string]interface{})["name"])
// 3. 更新
w = makeRequest(r, "PUT",
fmt.Sprintf("/api/v1/product-options/categories/%v", id), token,
map[string]interface{}{"name": "浓香"})
assert.Equal(t, http.StatusOK, w.Code)
w = makeRequest(r, "GET", "/api/v1/product-options/categories?keyword=浓", token, nil)
data = parseResponse(w)["data"].([]interface{})
assert.Len(t, data, 1)
// 4. 删除
w = makeRequest(r, "DELETE",
fmt.Sprintf("/api/v1/product-options/categories/%v", id), token, nil)
assert.Equal(t, http.StatusOK, w.Code)
w = makeRequest(r, "GET", "/api/v1/product-options/categories", token, nil)
assert.Len(t, parseResponse(w)["data"].([]interface{}), 0)
}
func TestProductOptionHandler_CategoryIsolation(t *testing.T) {
db := testutil.SetupTestDB()
shopA := testutil.CreateTestShop(db, "CAT_A")
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
shopB := testutil.CreateTestShop(db, "CAT_B")
r := setupCategoryRouter(db)
db.Create(&model.ProductCategory{
TenantBase: model.TenantBase{ShopID: shopB.ID}, Name: "他店香型",
})
// A 店查不到 B 店香型(shop_id 隔离)
w := makeRequest(r, "GET", "/api/v1/product-options/categories?keyword=他店", tokenA, nil)
assert.Equal(t, http.StatusOK, w.Code)
assert.Len(t, parseResponse(w)["data"].([]interface{}), 0)
}