Files
jiu/backend/internal/handler/stock_in_test.go
T
wangjia 7a09d1537c 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>
2026-04-04 07:24:14 +08:00

214 lines
7.7 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()
hotel := testutil.CreateTestHotel(db, "SI001")
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Main")
product := testutil.CreateTestProduct(db, hotel.ID, "Test Beer")
token := getAuthToken(user.ID, hotel.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("hotel_id = ? AND warehouse_id = ? AND product_id = ?",
hotel.ID, warehouse.ID, product.ID).First(&inv)
assert.Equal(t, float64(10), inv.Quantity)
// 6. 验证库存流水
var logs []model.InventoryLog
db.Where("hotel_id = ? AND product_id = ?", hotel.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()
hotel := testutil.CreateTestHotel(db, "SI002")
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)
// 创建 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()
hotel := testutil.CreateTestHotel(db, "SI003")
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
product := testutil.CreateTestProduct(db, hotel.ID, "Gin")
token := getAuthToken(user.ID, hotel.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()
hotel := testutil.CreateTestHotel(db, "SI004")
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
product := testutil.CreateTestProduct(db, hotel.ID, "Rum")
token := getAuthToken(user.ID, hotel.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()
hotel := testutil.CreateTestHotel(db, "SI005")
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
product := testutil.CreateTestProduct(db, hotel.ID, "Tequila")
token := getAuthToken(user.ID, hotel.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()
hotel := testutil.CreateTestHotel(db, "SI006")
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/stock-in/orders/99999", token, nil)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestStockInHandler_TotalAmount(t *testing.T) {
db := testutil.SetupTestDB()
hotel := testutil.CreateTestHotel(db, "SI007")
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
product1 := testutil.CreateTestProduct(db, hotel.ID, "ProductA")
product2 := testutil.CreateTestProduct(db, hotel.ID, "ProductB")
token := getAuthToken(user.ID, hotel.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"])
}