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:
wangjia
2026-04-04 07:24:14 +08:00
parent 0e42f0e417
commit 7a09d1537c
13 changed files with 2292 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
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"])
}