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,132 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/wangjia/jiu/backend/testutil"
|
||||
)
|
||||
|
||||
func TestAuthService_Login_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "HOTEL001")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := NewAuthService(db)
|
||||
pair, user, err := svc.Login("HOTEL001", "admin", "password123")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pair)
|
||||
require.NotNil(t, user)
|
||||
assert.NotEmpty(t, pair.AccessToken)
|
||||
assert.NotEmpty(t, pair.RefreshToken)
|
||||
assert.Equal(t, "admin", user.Username)
|
||||
}
|
||||
|
||||
func TestAuthService_Login_WrongPassword(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "HOTEL002")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := NewAuthService(db)
|
||||
pair, user, err := svc.Login("HOTEL002", "admin", "wrongpassword")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrInvalidCredentials, err)
|
||||
assert.Nil(t, pair)
|
||||
assert.Nil(t, user)
|
||||
}
|
||||
|
||||
func TestAuthService_Login_WrongHotel(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
testutil.CreateTestHotel(db, "HOTEL003")
|
||||
|
||||
svc := NewAuthService(db)
|
||||
pair, user, err := svc.Login("NONEXISTENT", "admin", "password123")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrInvalidCredentials, err)
|
||||
assert.Nil(t, pair)
|
||||
assert.Nil(t, user)
|
||||
}
|
||||
|
||||
func TestAuthService_Login_DisabledUser(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "HOTEL004")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "disabled", "password123", "operator")
|
||||
// 禁用用户
|
||||
db.Model(user).Update("is_active", false)
|
||||
|
||||
svc := NewAuthService(db)
|
||||
pair, u, err := svc.Login("HOTEL004", "disabled", "password123")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrUserInactive, err)
|
||||
assert.Nil(t, pair)
|
||||
assert.Nil(t, u)
|
||||
}
|
||||
|
||||
func TestAuthService_Login_WrongUsername(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "HOTEL005")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := NewAuthService(db)
|
||||
pair, user, err := svc.Login("HOTEL005", "nonexistent", "password123")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrInvalidCredentials, err)
|
||||
assert.Nil(t, pair)
|
||||
assert.Nil(t, user)
|
||||
}
|
||||
|
||||
func TestAuthService_RefreshTokens(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "HOTEL006")
|
||||
testutil.CreateTestUser(db, hotel.ID, "admin", "password123", "admin")
|
||||
|
||||
svc := NewAuthService(db)
|
||||
pair, _, err := svc.Login("HOTEL006", "admin", "password123")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pair)
|
||||
|
||||
// 用 refresh token 换新 token
|
||||
newPair, err := svc.RefreshTokens(pair.RefreshToken)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, newPair)
|
||||
assert.NotEmpty(t, newPair.AccessToken)
|
||||
assert.NotEmpty(t, newPair.RefreshToken)
|
||||
}
|
||||
|
||||
func TestAuthService_RefreshTokens_Invalid(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
testutil.InitConfig()
|
||||
|
||||
svc := NewAuthService(db)
|
||||
newPair, err := svc.RefreshTokens("invalid.token.here")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, newPair)
|
||||
}
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
hash, err := HashPassword("mypassword")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, hash)
|
||||
assert.NotEqual(t, "mypassword", hash)
|
||||
|
||||
// 第二次哈希应不同(bcrypt 加 salt)
|
||||
hash2, err := HashPassword("mypassword")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, hash, hash2)
|
||||
}
|
||||
|
||||
func TestInitConfig(t *testing.T) {
|
||||
testutil.InitConfig()
|
||||
}
|
||||
|
||||
func init() {
|
||||
testutil.InitConfig()
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"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 TestLicenseService_Activate_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC001")
|
||||
|
||||
// 创建许可证
|
||||
expiry := time.Now().Add(30 * 24 * time.Hour)
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "AAAAA-BBBBB-CCCCC-DDDDD",
|
||||
IsActive: true,
|
||||
ExpiresAt: &expiry,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Activate("AAAAA-BBBBB-CCCCC-DDDDD", "device-001")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, "device-001", result.DeviceID)
|
||||
assert.NotNil(t, result.ActivatedAt)
|
||||
}
|
||||
|
||||
func TestLicenseService_Activate_AlreadyBoundToDifferentDevice(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC002")
|
||||
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "EEEEE-FFFFF-GGGGG-HHHHH",
|
||||
DeviceID: "existing-device",
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Activate("EEEEE-FFFFF-GGGGG-HHHHH", "new-device")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrDeviceMismatch, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestLicenseService_Activate_SameDevice(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC003")
|
||||
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "IIIII-JJJJJ-KKKKK-LLLLL",
|
||||
DeviceID: "same-device",
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
// 同一设备重新激活应该成功
|
||||
result, err := svc.Activate("IIIII-JJJJJ-KKKKK-LLLLL", "same-device")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, "same-device", result.DeviceID)
|
||||
}
|
||||
|
||||
func TestLicenseService_Activate_NotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Activate("NONEX-ISTEN-TTTTT-LICCC", "device-001")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrLicenseNotFound, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestLicenseService_Activate_Inactive(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC004")
|
||||
|
||||
// 先创建激活的许可证,再禁用(避免 GORM 零值跳过问题)
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "MMMMM-NNNNN-OOOOO-PPPPP",
|
||||
IsActive: true,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
// 禁用
|
||||
require.NoError(t, db.Model(lic).Update("is_active", false).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Activate("MMMMM-NNNNN-OOOOO-PPPPP", "device-001")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrLicenseInactive, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestLicenseService_Activate_Expired(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC005")
|
||||
|
||||
// 已过期
|
||||
expiry := time.Now().Add(-24 * time.Hour)
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "QQQQQ-RRRRR-SSSSS-TTTTT",
|
||||
IsActive: true,
|
||||
ExpiresAt: &expiry,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Activate("QQQQQ-RRRRR-SSSSS-TTTTT", "device-001")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrLicenseExpired, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestLicenseService_Verify_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC006")
|
||||
|
||||
expiry := time.Now().Add(30 * 24 * time.Hour)
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "UUUUU-VVVVV-WWWWW-XXXXX",
|
||||
DeviceID: "my-device",
|
||||
IsActive: true,
|
||||
ExpiresAt: &expiry,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Verify(hotel.ID, "my-device")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, "my-device", result.DeviceID)
|
||||
}
|
||||
|
||||
func TestLicenseService_Verify_Expired(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC007")
|
||||
|
||||
// 已过期
|
||||
expiry := time.Now().Add(-1 * time.Hour)
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "YYYYY-ZZZZZ-AAAAA-BBBBB",
|
||||
DeviceID: "expired-device",
|
||||
IsActive: true,
|
||||
ExpiresAt: &expiry,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Verify(hotel.ID, "expired-device")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrLicenseExpired, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestLicenseService_Verify_NotFound(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC008")
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Verify(hotel.ID, "nonexistent-device")
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrLicenseNotFound, err)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestLicenseService_Verify_NoExpiry(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "LIC009")
|
||||
|
||||
// 永久许可证(无过期时间)
|
||||
lic := &model.License{
|
||||
HotelID: hotel.ID,
|
||||
LicenseKey: "CCCCC-DDDDD-EEEEE-FFFFF",
|
||||
DeviceID: "lifetime-device",
|
||||
IsActive: true,
|
||||
ExpiresAt: nil,
|
||||
}
|
||||
require.NoError(t, db.Create(lic).Error)
|
||||
|
||||
svc := NewLicenseService(db)
|
||||
result, err := svc.Verify(hotel.ID, "lifetime-device")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
testutil.InitConfig()
|
||||
|
||||
expiry := time.Now().Add(30 * 24 * time.Hour)
|
||||
key := GenerateKey(1, "annual", &expiry)
|
||||
|
||||
assert.NotEmpty(t, key)
|
||||
// 格式:XXXXX-XXXXX-XXXXX-XXXXX
|
||||
assert.Equal(t, 23, len(key)) // 4*5 + 3 dashes = 23
|
||||
assert.Equal(t, '-', rune(key[5]))
|
||||
assert.Equal(t, '-', rune(key[11]))
|
||||
assert.Equal(t, '-', rune(key[17]))
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 TestStockService_ApproveStockIn_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK001")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Main Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Beer")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
|
||||
// 创建入库单
|
||||
order := model.StockInOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "IN20240101000001",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "pending",
|
||||
OrderDate: time.Now(),
|
||||
Items: []model.StockInItem{
|
||||
{
|
||||
HotelID: hotel.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 10,
|
||||
UnitPrice: 5.0,
|
||||
TotalPrice: 50.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&order).Error)
|
||||
|
||||
svc := NewStockService(db)
|
||||
err := svc.ApproveStockIn(hotel.ID, order.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证单据状态
|
||||
var updated model.StockInOrder
|
||||
db.First(&updated, order.ID)
|
||||
assert.Equal(t, "approved", updated.Status)
|
||||
|
||||
// 验证库存
|
||||
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)
|
||||
|
||||
// 验证库存流水
|
||||
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)
|
||||
assert.Equal(t, float64(0), logs[0].QtyBefore)
|
||||
assert.Equal(t, float64(10), logs[0].QtyAfter)
|
||||
}
|
||||
|
||||
func TestStockService_ApproveStockIn_NotPending(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK002")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
_ = testutil.CreateTestProduct(db, hotel.ID, "Wine")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
|
||||
order := model.StockInOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "IN20240101000002",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "draft", // 非 pending 状态
|
||||
OrderDate: time.Now(),
|
||||
}
|
||||
require.NoError(t, db.Create(&order).Error)
|
||||
|
||||
svc := NewStockService(db)
|
||||
err := svc.ApproveStockIn(hotel.ID, order.ID, user.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not in pending status")
|
||||
}
|
||||
|
||||
func TestStockService_ApproveStockOut_Success(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK003")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Whiskey")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
|
||||
// 先入库
|
||||
inv := model.Inventory{
|
||||
HotelID: hotel.ID,
|
||||
WarehouseID: warehouse.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 20,
|
||||
}
|
||||
require.NoError(t, db.Create(&inv).Error)
|
||||
|
||||
// 创建出库单
|
||||
order := model.StockOutOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "OUT20240101000001",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "pending",
|
||||
OrderDate: time.Now(),
|
||||
Items: []model.StockOutItem{
|
||||
{
|
||||
HotelID: hotel.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 5,
|
||||
UnitPrice: 10.0,
|
||||
TotalPrice: 50.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&order).Error)
|
||||
|
||||
svc := NewStockService(db)
|
||||
err := svc.ApproveStockOut(hotel.ID, order.ID, user.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证库存减少
|
||||
var updatedInv model.Inventory
|
||||
db.First(&updatedInv, inv.ID)
|
||||
assert.Equal(t, float64(15), updatedInv.Quantity)
|
||||
|
||||
// 验证单据状态
|
||||
var updatedOrder model.StockOutOrder
|
||||
db.First(&updatedOrder, order.ID)
|
||||
assert.Equal(t, "approved", updatedOrder.Status)
|
||||
}
|
||||
|
||||
func TestStockService_ApproveStockOut_InsufficientStock(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK004")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Vodka")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
|
||||
// 库存只有 3
|
||||
inv := model.Inventory{
|
||||
HotelID: hotel.ID,
|
||||
WarehouseID: warehouse.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 3,
|
||||
}
|
||||
require.NoError(t, db.Create(&inv).Error)
|
||||
|
||||
// 要出库 10
|
||||
order := model.StockOutOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "OUT20240101000002",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "pending",
|
||||
OrderDate: time.Now(),
|
||||
Items: []model.StockOutItem{
|
||||
{
|
||||
HotelID: hotel.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&order).Error)
|
||||
|
||||
svc := NewStockService(db)
|
||||
err := svc.ApproveStockOut(hotel.ID, order.ID, user.ID)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInsufficientStock)
|
||||
}
|
||||
|
||||
func TestStockService_ApproveStockOut_ProductNotInInventory(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK005")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Rum")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
|
||||
// 没有创建库存记录
|
||||
order := model.StockOutOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "OUT20240101000003",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "pending",
|
||||
OrderDate: time.Now(),
|
||||
Items: []model.StockOutItem{
|
||||
{
|
||||
HotelID: hotel.ID,
|
||||
ProductID: product.ID,
|
||||
Quantity: 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&order).Error)
|
||||
|
||||
svc := NewStockService(db)
|
||||
err := svc.ApproveStockOut(hotel.ID, order.ID, user.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not in inventory")
|
||||
}
|
||||
|
||||
func TestStockService_GenerateOrderNo(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK006")
|
||||
|
||||
svc := NewStockService(db)
|
||||
no, err := svc.GenerateOrderNo(hotel.ID, "stock_in")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, no)
|
||||
// 单号格式: prefix + date + 6位序号
|
||||
assert.Contains(t, no, "st")
|
||||
|
||||
// 第二次生成序号应递增
|
||||
no2, err := svc.GenerateOrderNo(hotel.ID, "stock_in")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, no, no2)
|
||||
}
|
||||
|
||||
// TestStockService_GenerateOrderNo_Sequential 验证顺序调用生成唯一递增单号
|
||||
// 注:并发安全由 MySQL 的事务锁保证,SQLite in-memory 不模拟此场景
|
||||
func TestStockService_GenerateOrderNo_Sequential(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK007")
|
||||
|
||||
svc := NewStockService(db)
|
||||
|
||||
const count = 5
|
||||
seen := map[string]bool{}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
no, err := svc.GenerateOrderNo(hotel.ID, "stock_in")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, no)
|
||||
assert.False(t, seen[no], fmt.Sprintf("duplicate order no: %s", no))
|
||||
seen[no] = true
|
||||
}
|
||||
|
||||
assert.Len(t, seen, count, "should generate exactly %d unique order numbers", count)
|
||||
}
|
||||
|
||||
func TestStockService_MultipleStockIn_AccumulatesInventory(t *testing.T) {
|
||||
db := testutil.SetupTestDB()
|
||||
hotel := testutil.CreateTestHotel(db, "STOCK008")
|
||||
warehouse := testutil.CreateTestWarehouse(db, hotel.ID, "Warehouse")
|
||||
product := testutil.CreateTestProduct(db, hotel.ID, "Gin")
|
||||
user := testutil.CreateTestUser(db, hotel.ID, "admin", "pass", "admin")
|
||||
|
||||
svc := NewStockService(db)
|
||||
|
||||
// 第一次入库
|
||||
order1 := model.StockInOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "IN001",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "pending",
|
||||
OrderDate: time.Now(),
|
||||
Items: []model.StockInItem{
|
||||
{HotelID: hotel.ID, ProductID: product.ID, Quantity: 10},
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&order1).Error)
|
||||
require.NoError(t, svc.ApproveStockIn(hotel.ID, order1.ID, user.ID))
|
||||
|
||||
// 第二次入库
|
||||
order2 := model.StockInOrder{
|
||||
TenantBase: model.TenantBase{HotelID: hotel.ID},
|
||||
OrderNo: "IN002",
|
||||
WarehouseID: warehouse.ID,
|
||||
OperatorID: user.ID,
|
||||
Status: "pending",
|
||||
OrderDate: time.Now(),
|
||||
Items: []model.StockInItem{
|
||||
{HotelID: hotel.ID, ProductID: product.ID, Quantity: 5},
|
||||
},
|
||||
}
|
||||
require.NoError(t, db.Create(&order2).Error)
|
||||
require.NoError(t, svc.ApproveStockIn(hotel.ID, order2.ID, user.ID))
|
||||
|
||||
// 总库存应该是 15
|
||||
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(15), inv.Quantity)
|
||||
}
|
||||
Reference in New Issue
Block a user