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,175 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/wangjia/jiu/backend/internal/service"
|
||||
"github.com/wangjia/jiu/backend/testutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
testutil.InitConfig()
|
||||
}
|
||||
|
||||
func newTestAuthRouter(t *testing.T) (*gin.Engine, *gin.Engine) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "AUTHTEST")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
testutil.CreateTestUser(db, hotel.ID, "disabled", "password123", "operator")
|
||||
// 禁用该用户
|
||||
db.Exec("UPDATE users SET is_active = 0 WHERE username = 'disabled' AND hotel_id = ?", hotel.ID)
|
||||
|
||||
svc := service.NewAuthService(db)
|
||||
h := NewAuthHandler(svc)
|
||||
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/auth/login", h.Login)
|
||||
r.POST("/api/v1/auth/refresh", h.Refresh)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "AH001")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := service.NewAuthService(db)
|
||||
h := NewAuthHandler(svc)
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/auth/login", h.Login)
|
||||
|
||||
body := map[string]string{
|
||||
"hotel_code": "AH001",
|
||||
"username": "admin",
|
||||
"password": "password123",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
assert.NotEmpty(t, data["access_token"])
|
||||
assert.NotEmpty(t, data["refresh_token"])
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login_WrongPassword(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "AH002")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := service.NewAuthService(db)
|
||||
h := NewAuthHandler(svc)
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/auth/login", h.Login)
|
||||
|
||||
body := map[string]string{
|
||||
"hotel_code": "AH002",
|
||||
"username": "admin",
|
||||
"password": "wrongpassword",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login_MissingFields(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := service.NewAuthService(db)
|
||||
h := NewAuthHandler(svc)
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/auth/login", h.Login)
|
||||
|
||||
// 缺少必填字段
|
||||
body := map[string]string{
|
||||
"hotel_code": "AH003",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestAuthHandler_Refresh_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "AH004")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := service.NewAuthService(db)
|
||||
h := NewAuthHandler(svc)
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/auth/login", h.Login)
|
||||
r.POST("/api/v1/auth/refresh", h.Refresh)
|
||||
|
||||
// 先登录获取 token
|
||||
loginBody := map[string]string{
|
||||
"hotel_code": "AH004",
|
||||
"username": "admin",
|
||||
"password": "password123",
|
||||
}
|
||||
loginBytes, _ := json.Marshal(loginBody)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/auth/login", bytes.NewBuffer(loginBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
var loginResp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &loginResp)
|
||||
data := loginResp["data"].(map[string]interface{})
|
||||
refreshToken := data["refresh_token"].(string)
|
||||
|
||||
// 刷新 token
|
||||
refreshBody := map[string]string{"refresh_token": refreshToken}
|
||||
refreshBytes, _ := json.Marshal(refreshBody)
|
||||
w2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequest("POST", "/api/v1/auth/refresh", bytes.NewBuffer(refreshBytes))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w2, req2)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w2.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w2.Body.Bytes(), &resp)
|
||||
newData := resp["data"].(map[string]interface{})
|
||||
assert.NotEmpty(t, newData["access_token"])
|
||||
}
|
||||
|
||||
func TestAuthHandler_Refresh_InvalidToken(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
svc := service.NewAuthService(db)
|
||||
h := NewAuthHandler(svc)
|
||||
r := gin.New()
|
||||
r.POST("/api/v1/auth/refresh", h.Refresh)
|
||||
|
||||
body := map[string]string{"refresh_token": "invalid.token.here"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/auth/refresh", bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/wangjia/jiu/backend/testutil"
|
||||
)
|
||||
|
||||
func TestProductHandler_CRUD(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "PROD001")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 1. Create
|
||||
w := makeRequest(r, "POST", "/api/v1/products", token, map[string]interface{}{
|
||||
"name": "Test Beer",
|
||||
"code": "BEER001",
|
||||
"unit": "瓶",
|
||||
})
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
productID := extractID(w)
|
||||
assert.NotZero(t, productID)
|
||||
|
||||
// 2. List
|
||||
w = makeRequest(r, "GET", "/api/v1/products", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
total := resp["total"].(float64)
|
||||
assert.Equal(t, float64(1), total)
|
||||
|
||||
// 3. Update
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/products/%d", productID), token, map[string]interface{}{
|
||||
"name": "Updated Beer",
|
||||
"code": "BEER001",
|
||||
"unit": "瓶",
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
updatedData := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, "Updated Beer", updatedData["name"])
|
||||
|
||||
// 4. List with keyword
|
||||
w = makeRequest(r, "GET", "/api/v1/products?keyword=Updated", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp = parseResponse(w)
|
||||
assert.Equal(t, float64(1), resp["total"].(float64))
|
||||
|
||||
// 5. Delete (soft)
|
||||
w = makeRequest(r, "DELETE", fmt.Sprintf("/api/v1/products/%d", productID), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// 6. List after delete - should be 0
|
||||
w = makeRequest(r, "GET", "/api/v1/products", token, nil)
|
||||
resp = parseResponse(w)
|
||||
assert.Equal(t, float64(0), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestProductHandler_HotelIsolation(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
|
||||
// 酒店 A
|
||||
hotelA := testutil.CreateTestHotel(db, "ISOL_A")
|
||||
userA := testutil.CreateTestUser(db, hotelA.ID, "adminA", "pass", "admin")
|
||||
tokenA := getAuthToken(userA.ID, hotelA.ID, "admin")
|
||||
|
||||
// 酒店 B
|
||||
hotelB := testutil.CreateTestHotel(db, "ISOL_B")
|
||||
userB := testutil.CreateTestUser(db, hotelB.ID, "adminB", "pass", "admin")
|
||||
tokenB := getAuthToken(userB.ID, hotelB.ID, "admin")
|
||||
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 酒店 A 创建商品
|
||||
w := makeRequest(r, "POST", "/api/v1/products", tokenA, map[string]interface{}{
|
||||
"name": "Hotel A Beer",
|
||||
"unit": "瓶",
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
productAID := extractID(w)
|
||||
|
||||
// 酒店 B 创建商品
|
||||
w = makeRequest(r, "POST", "/api/v1/products", tokenB, map[string]interface{}{
|
||||
"name": "Hotel B Wine",
|
||||
"unit": "瓶",
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
// 酒店 A 只能看到自己的商品
|
||||
w = makeRequest(r, "GET", "/api/v1/products", tokenA, nil)
|
||||
respA := parseResponse(w)
|
||||
assert.Equal(t, float64(1), respA["total"].(float64))
|
||||
dataA := respA["data"].([]interface{})
|
||||
assert.Equal(t, "Hotel A Beer", dataA[0].(map[string]interface{})["name"])
|
||||
|
||||
// 酒店 B 只能看到自己的商品
|
||||
w = makeRequest(r, "GET", "/api/v1/products", tokenB, nil)
|
||||
respB := parseResponse(w)
|
||||
assert.Equal(t, float64(1), respB["total"].(float64))
|
||||
dataB := respB["data"].([]interface{})
|
||||
assert.Equal(t, "Hotel B Wine", dataB[0].(map[string]interface{})["name"])
|
||||
|
||||
// 酒店 B 不能修改酒店 A 的商品
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/products/%d", productAID), tokenB, map[string]interface{}{
|
||||
"name": "Hacked Product",
|
||||
})
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// 酒店 B 不能删除酒店 A 的商品
|
||||
w = makeRequest(r, "DELETE", fmt.Sprintf("/api/v1/products/%d", productAID), tokenB, nil)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestProductHandler_NoAuth(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 无 token 访问
|
||||
w := makeRequest(r, "GET", "/api/v1/products", "", nil)
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
|
||||
func TestProductHandler_List_Pagination(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "PROD002")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 创建 5 个商品
|
||||
for i := 1; i <= 5; i++ {
|
||||
makeRequest(r, "POST", "/api/v1/products", token, map[string]interface{}{
|
||||
"name": fmt.Sprintf("Product %d", i),
|
||||
"unit": "个",
|
||||
})
|
||||
}
|
||||
|
||||
// 每页 2 个,第 1 页
|
||||
w := makeRequest(r, "GET", "/api/v1/products?page=1&page_size=2", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
data := resp["data"].([]interface{})
|
||||
assert.Len(t, data, 2)
|
||||
assert.Equal(t, float64(5), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestProductHandler_UpdateNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "PROD003")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "PUT", "/api/v1/products/99999", token, map[string]interface{}{
|
||||
"name": "Nonexistent",
|
||||
})
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestProductHandler_DeleteNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "PROD004")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "DELETE", "/api/v1/products/99999", token, nil)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestProductHandler_Create_HotelIDFromToken(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "PROD005")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 尝试在请求体中传入不同的 hotel_id
|
||||
w := makeRequest(r, "POST", "/api/v1/products", token, map[string]interface{}{
|
||||
"name": "Test Product",
|
||||
"hotel_id": 9999, // 尝试注入其他酒店 ID
|
||||
"unit": "个",
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
resp := parseResponse(w)
|
||||
data := resp["data"].(map[string]interface{})
|
||||
|
||||
// hotel_id 应该是从 token 中获取的,而不是请求体中的
|
||||
createdHotelID := uint64(data["hotel_id"].(float64))
|
||||
assert.Equal(t, hotel.ID, createdHotelID)
|
||||
|
||||
// 反序列化验证
|
||||
dataBytes, _ := json.Marshal(data)
|
||||
_ = dataBytes
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
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 TestStockOutHandler_FullFlow(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SO001")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Main")
|
||||
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: 100,
|
||||
})
|
||||
|
||||
// 1. 创建出库单
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-out/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": 15.0,
|
||||
"unit_price": 20.0,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
orderID := extractID(w)
|
||||
assert.NotZero(t, orderID)
|
||||
|
||||
data := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, "draft", data["status"])
|
||||
assert.NotEmpty(t, data["order_no"])
|
||||
|
||||
// 2. 提交
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// 3. 获取详情
|
||||
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/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-out/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(85), inv.Quantity)
|
||||
}
|
||||
|
||||
func TestStockOutHandler_InsufficientStock(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SO002")
|
||||
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)
|
||||
|
||||
// 库存只有 5
|
||||
db.Create(&model.Inventory{
|
||||
HotelID: hotel.ID,
|
||||
WarehouseID: warehouse.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 5,
|
||||
})
|
||||
|
||||
// 创建出库单要出 10
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-out/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},
|
||||
},
|
||||
})
|
||||
orderID := extractID(w)
|
||||
makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/submit", orderID), token, nil)
|
||||
|
||||
// 审核应该失败
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/approve", orderID), token, nil)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func TestStockOutHandler_List(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SO003")
|
||||
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)
|
||||
|
||||
// 先建立库存
|
||||
db.Create(&model.Inventory{HotelID: hotel.ID, WarehouseID: warehouse.ID, ProductID: product.ID, Quantity: 100})
|
||||
|
||||
// 创建 2 个出库单
|
||||
for i := 0; i < 2; i++ {
|
||||
makeRequest(r, "POST", "/api/v1/stock-out/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": 1.0},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
w := makeRequest(r, "GET", "/api/v1/stock-out/orders", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
assert.Equal(t, float64(2), resp["total"].(float64))
|
||||
}
|
||||
|
||||
func TestStockOutHandler_Reject(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SO004")
|
||||
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)
|
||||
|
||||
db.Create(&model.Inventory{HotelID: hotel.ID, WarehouseID: warehouse.ID, ProductID: product.ID, Quantity: 100})
|
||||
|
||||
w := makeRequest(r, "POST", "/api/v1/stock-out/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-out/orders/%d/submit", orderID), token, nil)
|
||||
|
||||
// 驳回
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-out/orders/%d/reject", orderID), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// 验证状态
|
||||
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-out/orders/%d", orderID), token, nil)
|
||||
detailData := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, "rejected", detailData["status"])
|
||||
}
|
||||
|
||||
func TestStockOutHandler_GetNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "SO005")
|
||||
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-out/orders/99999", token, nil)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||||
"github.com/wangjia/jiu/backend/internal/service"
|
||||
"github.com/wangjia/jiu/backend/testutil"
|
||||
)
|
||||
|
||||
// setupProtectedRouter 创建带 JWT 中间件的测试路由
|
||||
func setupProtectedRouter(db *gorm.DB) *gin.Engine {
|
||||
stockSvc := service.NewStockService(db)
|
||||
licenseSvc := service.NewLicenseService(db)
|
||||
|
||||
productH := NewProductHandler(db)
|
||||
stockInH := NewStockInHandler(db, stockSvc)
|
||||
stockOutH := NewStockOutHandler(db, stockSvc)
|
||||
inventoryH := NewInventoryHandler(db)
|
||||
warehouseH := NewWarehouseHandler(db)
|
||||
partnerH := NewPartnerHandler(db)
|
||||
licenseH := NewLicenseHandler(licenseSvc)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
api := r.Group("/api/v1")
|
||||
api.Use(middleware.JWT())
|
||||
|
||||
// 商品路由
|
||||
products := api.Group("/products")
|
||||
products.GET("", productH.List)
|
||||
products.POST("", productH.Create)
|
||||
products.PUT("/:id", productH.Update)
|
||||
products.DELETE("/:id", productH.Delete)
|
||||
|
||||
// 仓库路由
|
||||
warehouses := api.Group("/warehouses")
|
||||
warehouses.GET("", warehouseH.List)
|
||||
warehouses.POST("", warehouseH.Create)
|
||||
warehouses.PUT("/:id", warehouseH.Update)
|
||||
warehouses.DELETE("/:id", warehouseH.Delete)
|
||||
|
||||
// 往来单位路由
|
||||
partners := api.Group("/partners")
|
||||
partners.GET("", partnerH.List)
|
||||
partners.POST("", partnerH.Create)
|
||||
partners.PUT("/:id", partnerH.Update)
|
||||
partners.DELETE("/:id", partnerH.Delete)
|
||||
|
||||
// 入库路由
|
||||
stockIn := api.Group("/stock-in")
|
||||
stockIn.GET("/orders", stockInH.List)
|
||||
stockIn.GET("/orders/:id", stockInH.Get)
|
||||
stockIn.POST("/orders", stockInH.Create)
|
||||
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
||||
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
||||
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
||||
|
||||
// 出库路由
|
||||
stockOut := api.Group("/stock-out")
|
||||
stockOut.GET("/orders", stockOutH.List)
|
||||
stockOut.GET("/orders/:id", stockOutH.Get)
|
||||
stockOut.POST("/orders", stockOutH.Create)
|
||||
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
|
||||
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
||||
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
||||
|
||||
// 库存路由
|
||||
inv := api.Group("/inventory")
|
||||
inv.GET("", inventoryH.List)
|
||||
inv.GET("/logs", inventoryH.Logs)
|
||||
inv.POST("/checks", inventoryH.CreateCheck)
|
||||
inv.GET("/checks/:id", inventoryH.GetCheck)
|
||||
|
||||
// 许可证路由
|
||||
license := api.Group("/license")
|
||||
license.POST("/activate", licenseH.Activate)
|
||||
license.GET("/verify", licenseH.Verify)
|
||||
license.POST("/deactivate", licenseH.Deactivate)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// makeRequest 发起带认证的请求
|
||||
func makeRequest(r *gin.Engine, method, path, token string, body interface{}) *httptest.ResponseRecorder {
|
||||
var bodyBytes []byte
|
||||
if body != nil {
|
||||
bodyBytes, _ = json.Marshal(body)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(method, path, bytes.NewBuffer(bodyBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
// parseResponse 解析 JSON 响应
|
||||
func parseResponse(w *httptest.ResponseRecorder) map[string]interface{} {
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
return resp
|
||||
}
|
||||
|
||||
// getAuthToken 为测试用户获取 token
|
||||
func getAuthToken(userID, hotelID uint64, role string) string {
|
||||
return testutil.GetAuthToken(userID, hotelID, role)
|
||||
}
|
||||
|
||||
// extractID 从响应 data 中提取 id
|
||||
func extractID(w *httptest.ResponseRecorder) uint64 {
|
||||
resp := parseResponse(w)
|
||||
if data, ok := resp["data"].(map[string]interface{}); ok {
|
||||
if id, ok := data["id"].(float64); ok {
|
||||
return uint64(id)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// jsonBody 快速构建 JSON body
|
||||
func jsonBody(kv ...interface{}) map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
for i := 0; i+1 < len(kv); i += 2 {
|
||||
key := fmt.Sprintf("%v", kv[i])
|
||||
m[key] = kv[i+1]
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/wangjia/jiu/backend/testutil"
|
||||
)
|
||||
|
||||
func TestWarehouseHandler_CRUD(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "WH001")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 1. Create
|
||||
w := makeRequest(r, "POST", "/api/v1/warehouses", token, map[string]interface{}{
|
||||
"name": "Main Warehouse",
|
||||
"location": "Floor 1",
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
whID := extractID(w)
|
||||
assert.NotZero(t, whID)
|
||||
|
||||
// 2. List
|
||||
w = makeRequest(r, "GET", "/api/v1/warehouses", token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
resp := parseResponse(w)
|
||||
data := resp["data"].([]interface{})
|
||||
assert.Len(t, data, 1)
|
||||
|
||||
// 3. Update
|
||||
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/warehouses/%d", whID), token, map[string]interface{}{
|
||||
"name": "Updated Warehouse",
|
||||
"location": "Floor 2",
|
||||
})
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
updatedData := parseResponse(w)["data"].(map[string]interface{})
|
||||
assert.Equal(t, "Updated Warehouse", updatedData["name"])
|
||||
|
||||
// 4. Delete
|
||||
w = makeRequest(r, "DELETE", fmt.Sprintf("/api/v1/warehouses/%d", whID), token, nil)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// 5. List after delete - should be empty
|
||||
w = makeRequest(r, "GET", "/api/v1/warehouses", token, nil)
|
||||
resp = parseResponse(w)
|
||||
data = resp["data"].([]interface{})
|
||||
assert.Len(t, data, 0)
|
||||
}
|
||||
|
||||
func TestWarehouseHandler_UpdateNotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "WH002")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
token := getAuthToken(user.ID, hotel.ID, "admin")
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
w := makeRequest(r, "PUT", "/api/v1/warehouses/99999", token, map[string]interface{}{
|
||||
"name": "Nonexistent",
|
||||
})
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestWarehouseHandler_HotelIsolation(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
|
||||
hotelA := testutil.CreateTestHotel(db, "WH_A")
|
||||
userA := testutil.CreateTestUser(db, hotelA.ID, "adminA", "pass", "admin")
|
||||
tokenA := getAuthToken(userA.ID, hotelA.ID, "admin")
|
||||
|
||||
hotelB := testutil.CreateTestHotel(db, "WH_B")
|
||||
userB := testutil.CreateTestUser(db, hotelB.ID, "adminB", "pass", "admin")
|
||||
tokenB := getAuthToken(userB.ID, hotelB.ID, "admin")
|
||||
|
||||
r := setupProtectedRouter(db)
|
||||
|
||||
// 酒店 A 创建仓库
|
||||
w := makeRequest(r, "POST", "/api/v1/warehouses", tokenA, map[string]interface{}{
|
||||
"name": "Hotel A Warehouse",
|
||||
})
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
// 酒店 B 看不到酒店 A 的仓库
|
||||
w = makeRequest(r, "GET", "/api/v1/warehouses", tokenB, nil)
|
||||
resp := parseResponse(w)
|
||||
data := resp["data"].([]interface{})
|
||||
assert.Len(t, data, 0)
|
||||
}
|
||||
Reference in New Issue
Block a user