Files
jiu/backend/internal/handler/stock_in_test.go
T
wangjia 31ea370cea fix(backend): JWT config mapstructure tag 修复 + 模型从 hotel 重构为 shop
- 修复 JWTConfig 缺少 mapstructure tag 导致 access_expire_min 解析为 0,
  token 签发即过期,所有 API 请求返回 401
- 全部 config struct 补齐 mapstructure tag(secret/dsn/hmac_secret 等)
- 模型层从 hotel/HotelID 统一重命名为 shop/ShopID
- 删除旧 migrations(001-004),新增 001_init 综合迁移文件
- 更新 schema.sql、testutil、handler/service/model 相关引用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 22:20:12 +08:00

347 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/testutil"
)
func TestStockInHandler_FullFlow(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI001")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Main")
product := testutil.CreateTestProduct(db, shop.ID, "Test Beer")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 1. 创建入库单(草稿)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{
"product_id": product.ID,
"quantity": 10.0,
"unit_price": 5.0,
},
},
})
require.Equal(t, http.StatusCreated, w.Code)
orderID := extractID(w)
assert.NotZero(t, orderID)
// 验证状态是 draft
respData := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, "draft", respData["status"])
assert.NotEmpty(t, respData["order_no"])
// 2. 提交(draft → pending
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
// 3. 获取单据详情
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
detailData := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, "pending", detailData["status"])
// 4. 审核通过
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
// 5. 验证库存变化
var inv model.Inventory
db.Where("shop_id = ? AND warehouse_id = ? AND product_id = ?",
shop.ID, warehouse.ID, product.ID).First(&inv)
assert.Equal(t, float64(10), inv.Quantity)
// 6. 验证库存流水
var logs []model.InventoryLog
db.Where("shop_id = ? AND product_id = ?", shop.ID, product.ID).Find(&logs)
require.Len(t, logs, 1)
assert.Equal(t, "in", logs[0].Direction)
assert.Equal(t, float64(10), logs[0].Quantity)
}
func TestStockInHandler_List(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI002")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Wine")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建 2 个入库单
for i := 0; i < 2; i++ {
makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product.ID, "quantity": 5.0, "unit_price": 10.0},
},
})
}
w := makeRequest(r, "GET", "/api/v1/stock-in/orders", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
resp := parseResponse(w)
assert.Equal(t, float64(2), resp["total"].(float64))
}
func TestStockInHandler_Submit_WrongStatus(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI003")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Gin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product.ID, "quantity": 5.0},
},
})
orderID := extractID(w)
// 提交
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
// 再次提交(应该失败,因为已经是 pending)
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Approve_NotPending(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI004")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Rum")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建但不提交(状态是 draft)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product.ID, "quantity": 5.0},
},
})
orderID := extractID(w)
// 直接审核(应该失败,因为是 draft 状态)
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderID), token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Reject(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI005")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Tequila")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建并提交
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product.ID, "quantity": 5.0},
},
})
orderID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
// 驳回
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/reject", orderID), token, nil)
assert.Equal(t, http.StatusOK, w.Code)
// 验证状态
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), token, nil)
detailData := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, "rejected", detailData["status"])
}
func TestStockInHandler_GetNotFound(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI006")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "GET", "/api/v1/stock-in/orders/99999", token, nil)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestStockInHandler_TotalAmount(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI007")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product1 := testutil.CreateTestProduct(db, shop.ID, "ProductA")
product2 := testutil.CreateTestProduct(db, shop.ID, "ProductB")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product1.ID, "quantity": 10.0, "unit_price": 5.0}, // 50
{"product_id": product2.ID, "quantity": 3.0, "unit_price": 20.0}, // 60
},
})
require.Equal(t, http.StatusCreated, w.Code)
data := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, float64(110), data["total_amount"])
}
func TestStockInHandler_NoAuth(t *testing.T) {
db := testutil.SetupTestDB()
r := setupProtectedRouter(db)
w := makeRequest(r, "GET", "/api/v1/stock-in/orders", "", nil)
assert.Equal(t, http.StatusUnauthorized, w.Code)
w = makeRequest(r, "POST", "/api/v1/stock-in/orders", "", map[string]interface{}{
"warehouse_id": 1,
})
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestStockInHandler_TenantIsolation(t *testing.T) {
db := testutil.SetupTestDB()
shopA := testutil.CreateTestShop(db, "SI_A")
userA := testutil.CreateTestUser(db, shopA.ID, "adminA", "pass", "admin")
warehouseA := testutil.CreateTestWarehouse(db, shopA.ID, "WA")
productA := testutil.CreateTestProduct(db, shopA.ID, "BeerA")
tokenA := getAuthToken(userA.ID, shopA.ID, "admin")
shopB := testutil.CreateTestShop(db, "SI_B")
userB := testutil.CreateTestUser(db, shopB.ID, "adminB", "pass", "admin")
tokenB := getAuthToken(userB.ID, shopB.ID, "admin")
r := setupProtectedRouter(db)
// 门店 A 创建入库单
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", tokenA, map[string]interface{}{
"warehouse_id": warehouseA.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": productA.ID, "quantity": 5.0},
},
})
require.Equal(t, http.StatusCreated, w.Code)
orderAID := extractID(w)
// 门店 B 看不到门店 A 的订单
w = makeRequest(r, "GET", "/api/v1/stock-in/orders", tokenB, nil)
resp := parseResponse(w)
assert.Equal(t, float64(0), resp["total"].(float64))
// 门店 B 不能获取门店 A 的订单详情
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderAID), tokenB, nil)
assert.Equal(t, http.StatusNotFound, w.Code)
// 门店 B 不能提交门店 A 的订单
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderAID), tokenB, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
// 门店 B 不能审核门店 A 的订单
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderAID), tokenB, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Create_MissingWarehouse(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI008")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 缺少 warehouse_id,应该返回 400
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{},
})
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_Reject_NotPending(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI009")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Whiskey")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建但不提交(draft 状态)
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_id": product.ID, "quantity": 5.0},
},
})
orderID := extractID(w)
// 直接驳回(应该失败,因为是 draft 状态)
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/reject", orderID), token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestStockInHandler_List_FilterByStatus(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI010")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
product := testutil.CreateTestProduct(db, shop.ID, "Vodka")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 创建一个 draft 和一个 pending 订单
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 5.0}},
})
draftOrderID := extractID(w)
w = makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{{"product_id": product.ID, "quantity": 3.0}},
})
pendingOrderID := extractID(w)
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", pendingOrderID), token, nil)
_ = draftOrderID
// 过滤 pending 状态
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=pending", token, nil)
resp := parseResponse(w)
assert.Equal(t, float64(1), resp["total"].(float64))
// 过滤 draft 状态
w = makeRequest(r, "GET", "/api/v1/stock-in/orders?status=draft", token, nil)
resp = parseResponse(w)
assert.Equal(t, float64(1), resp["total"].(float64))
}