7a09d1537c
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>
202 lines
6.4 KiB
Go
202 lines
6.4 KiB
Go
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
|
|
}
|