test: 后端自动化测试套件
Service 单元测试(SQLite in-memory): - auth: 登录成功/失败/禁用账号,token 刷新 - stock: 入库审核、出库审核(含库存不足)、单号生成 - license: 激活、设备绑定验证、过期检测 Handler 集成测试(httptest + SQLite): - auth: 登录 API 成功/401 场景 - product: CRUD 完整流程 + hotel_id 隔离验证 - stock_in: 创建→提交→审核完整流程 - stock_out: 完整流程 + 库存不足 400 - inventory: 库存查询/过滤/盘点创建 - warehouse: CRUD 覆盖率:service 90.4%,handler 56.1% 共 50 个测试用例,全部 PASS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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 TestInventoryHandler_List(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV001")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Beer")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 直接插入库存记录
|
||||
inv := model.Inventory{
|
||||
HotelID: hotel.ID,
|
||||
WarehouseID: warehouse.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 100,
|
||||
}
|
||||
require.NoError(t, db.Create(&inv).Error)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/inventory", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestInventoryHandler_List_FilterByWarehouse(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV002")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse1 := testutil.CreateTestWarehouse(db, hotel.ID, "W1")
|
||||
warehouse2 := testutil.CreateTestWarehouse(db, hotel.ID, "W2")
|
||||
product1 := testutil.CreateTestProduct(db, hotel.ID, "Beer1")
|
||||
product2 := testutil.CreateTestProduct(db, hotel.ID, "Beer2")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 两个仓库各有一个库存
|
||||
db.Create(&model.Inventory{HotelID: hotel.ID, WarehouseID: warehouse1.ID, ProductID: product1.ID, Quantity: 10})
|
||||
db.Create(&model.Inventory{HotelID: hotel.ID, WarehouseID: warehouse2.ID, ProductID: product2.ID, Quantity: 20})
|
||||
|
||||
// 按仓库过滤
|
||||
w := makeRequest(r, "GET", fmt.Sprintf("/api/v1/inventory?warehouse_id=%d", warehouse1.ID), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestInventoryHandler_List_InStockOnly(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV003")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product1 := testutil.CreateTestProduct(db, hotel.ID, "InStock")
|
||||
product2 := testutil.CreateTestProduct(db, hotel.ID, "OutOfStock")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
db.Create(&model.Inventory{HotelID: hotel.ID, WarehouseID: warehouse.ID, ProductID: product1.ID, Quantity: 10})
|
||||
db.Create(&model.Inventory{HotelID: hotel.ID, WarehouseID: warehouse.ID, ProductID: product2.ID, Quantity: 0})
|
||||
|
||||
// 只显示有库存的
|
||||
w := makeRequest(r, "GET", "/api/v1/inventory?in_stock=1", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestInventoryHandler_Logs(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV004")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Wine")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建库存流水
|
||||
opID := user.ID
|
||||
db.Create(&model.InventoryLog{
|
||||
HotelID: hotel.ID,
|
||||
WarehouseID: warehouse.ID,
|
||||
ProductID: product.ID,
|
||||
Direction: "in",
|
||||
Quantity: 10,
|
||||
QtyBefore: 0,
|
||||
QtyAfter: 10,
|
||||
RefType: "stock_in",
|
||||
RefID: 1,
|
||||
OperatorID: &opID,
|
||||
})
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/inventory/logs", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestInventoryHandler_CreateCheck(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV005")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Whiskey")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 先创建库存
|
||||
db.Create(&model.Inventory{
|
||||
HotelID: hotel.ID,
|
||||
WarehouseID: warehouse.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 50,
|
||||
})
|
||||
|
||||
// 创建盘点单
|
||||
w := makeRequest(r, "POST", "/api/v1/inventory/checks", token, map[string]interface{}{
|
||||
"check_no": "CHK001",
|
||||
"warehouse_id": warehouse.ID,
|
||||
"check_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{
|
||||
"product_id": product.ID,
|
||||
"actual_qty": 48.0,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
checkID := extractID(w)
|
||||
assert.NotZero(t, checkID)
|
||||
|
||||
data := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, "draft", data["status"])
|
||||
|
||||
// 检查 system_qty 是否自动填入
|
||||
items := data["items"].([]interface{})
|
||||
require.Len(t, items, 1)
|
||||
item := items[0].(map[string]interface{})
|
||||
assert.Equal(t, float64(50), item["system_qty"])
|
||||
}
|
||||
|
||||
func TestInventoryHandler_GetCheck(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV006")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Vodka")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建盘点单
|
||||
w := makeRequest(r, "POST", "/api/v1/inventory/checks", token, map[string]interface{}{
|
||||
"check_no": "CHK002",
|
||||
"warehouse_id": warehouse.ID,
|
||||
"check_date": time.Now().Format(time.RFC3339),
|
||||
"items": []map[string]interface{}{
|
||||
{"product_id": product.ID, "actual_qty": 10.0},
|
||||
},
|
||||
})
|
||||
checkID := extractID(w)
|
||||
|
||||
// 获取盘点单
|
||||
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/inventory/checks/%d", checkID), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
data := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, "CHK002", data["check_no"])
|
||||
}
|
||||
|
||||
func TestInventoryHandler_GetCheck_NotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV007")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/inventory/checks/99999", token, nil)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestInventoryHandler_List_HotelIsolation(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
|
||||
hotelA := testutil.CreateTestHotel(db, "INV_A")
|
||||
userA := testutil.CreateTestUser(db, hotelA.ID, "adminA", "pass", "admin")
|
||||
warehouseA := testutil.CreateTestWarehouse(db, hotelA.ID, "WA")
|
||||
productA := testutil.CreateTestProduct(db, hotelA.ID, "ProductA")
|
||||
tokenA := getAuthToken(userA.ID, hotelA.ID, "admin")
|
||||
|
||||
hotelB := testutil.CreateTestHotel(db, "INV_B")
|
||||
userB := testutil.CreateTestUser(db, hotelB.ID, "adminB", "pass", "admin")
|
||||
tokenB := getAuthToken(userB.ID, hotelB.ID, "admin")
|
||||
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 酒店 A 有库存
|
||||
db.Create(&model.Inventory{HotelID: hotelA.ID, WarehouseID: warehouseA.ID, ProductID: productA.ID, Quantity: 100})
|
||||
|
||||
// 酒店 A 能看到自己的库存
|
||||
w := makeRequest(r, "GET", "/api/v1/inventory", tokenA, nil)
|
||||
respA := parseResponse(w)
|
||||
assert.Equal(t, float64(1), respA["total"].(float64))
|
||||
|
||||
// 酒店 B 看不到酒店 A 的库存
|
||||
w = makeRequest(r, "GET", "/api/v1/inventory", tokenB, nil)
|
||||
respB := parseResponse(w)
|
||||
assert.Equal(t, float64(0), respB["total"].(float64))
|
||||
}
|
||||
|
||||
func TestInventoryHandler_AfterStockInApprove(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "INV008")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Champagne")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 初始库存为 0
|
||||
w := makeRequest(r, "GET", "/api/v1/inventory", token, nil)
|
||||
assert.Equal(t, float64(0), parseResponse(w)["total"].(float64))
|
||||
|
||||
// 入库流程
|
||||
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": 20.0, "unit_price": 15.0},
|
||||
},
|
||||
})
|
||||
orderID := extractID(w)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/submit", orderID), token, nil)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d/approve", orderID), token, nil)
|
||||
|
||||
// 检查库存
|
||||
w = makeRequest(r, "GET", "/api/v1/inventory", token, nil)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
data := resp["data"].([]interface{})
|
||||
invItem := data[0].(map[string]interface{})
|
||||
assert.Equal(t, float64(20), invItem["quantity"])
|
||||
}
|
||||
Reference in New Issue
Block a user