Files
jiu/backend/testutil/setup.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

379 lines
9.6 KiB
Go

package testutil
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
)
// InitConfig 初始化测试用配置
func InitConfig() {
config.C = config.Config{
Server: config.ServerConfig{
Port: "8080",
Mode: "test",
},
JWT: config.JWTConfig{
Secret: "test-secret-key-for-testing",
AccessExpireMin: 60,
RefreshExpireH: 168,
},
License: config.LicenseConfig{
HMACSecret: "test-license-hmac-secret",
},
}
}
// SetupTestDB 创建 SQLite in-memory 测试数据库并自动迁移
func SetupTestDB() *gorm.DB {
InitConfig()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
panic(fmt.Sprintf("failed to open sqlite: %v", err))
}
// SQLite 不支持 ENUM,直接用原始 SQL 建表
stmts := []string{
`CREATE TABLE IF NOT EXISTS hotels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
name TEXT NOT NULL,
code TEXT UNIQUE,
address TEXT,
phone TEXT,
custom_fields TEXT
)`,
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
username TEXT,
password_hash TEXT,
real_name TEXT,
phone TEXT,
role TEXT DEFAULT 'operator',
is_active INTEGER DEFAULT 1,
custom_fields TEXT
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uk_hotel_username ON users(hotel_id, username)`,
`CREATE TABLE IF NOT EXISTS licenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
license_key TEXT UNIQUE,
device_id TEXT,
type TEXT DEFAULT 'trial',
expires_at DATETIME,
is_active INTEGER DEFAULT 1,
features TEXT,
activated_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS product_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
name TEXT NOT NULL,
parent_id INTEGER,
sort_order INTEGER DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
code TEXT,
barcode TEXT,
name TEXT NOT NULL,
series TEXT,
spec TEXT,
unit TEXT,
category_id INTEGER,
brand TEXT,
purchase_price REAL,
sale_price REAL,
min_stock INTEGER DEFAULT 0,
custom_fields TEXT,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS warehouses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
name TEXT NOT NULL,
location TEXT,
is_default INTEGER DEFAULT 0,
custom_fields TEXT
)`,
`CREATE TABLE IF NOT EXISTS partners (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
type TEXT DEFAULT 'supplier',
contact TEXT,
phone TEXT,
address TEXT,
bank_account TEXT,
credit_limit REAL,
balance REAL DEFAULT 0,
custom_fields TEXT,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS stock_in_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
order_no TEXT,
type TEXT DEFAULT 'purchase',
warehouse_id INTEGER NOT NULL,
partner_id INTEGER,
operator_id INTEGER NOT NULL,
reviewer_id INTEGER,
status TEXT DEFAULT 'draft',
order_date DATETIME,
total_amount REAL DEFAULT 0,
reviewed_at DATETIME,
custom_fields TEXT,
remark TEXT
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uk_hotel_sio_order_no ON stock_in_orders(hotel_id, order_no)`,
`CREATE TABLE IF NOT EXISTS stock_in_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
order_id INTEGER NOT NULL,
hotel_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity REAL NOT NULL,
unit_price REAL DEFAULT 0,
total_price REAL DEFAULT 0,
batch_no TEXT,
custom_fields TEXT,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS stock_out_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
order_no TEXT,
type TEXT DEFAULT 'sale',
warehouse_id INTEGER NOT NULL,
partner_id INTEGER,
operator_id INTEGER NOT NULL,
reviewer_id INTEGER,
status TEXT DEFAULT 'draft',
order_date DATETIME,
total_amount REAL DEFAULT 0,
reviewed_at DATETIME,
custom_fields TEXT,
remark TEXT
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uk_hotel_soo_order_no ON stock_out_orders(hotel_id, order_no)`,
`CREATE TABLE IF NOT EXISTS stock_out_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
order_id INTEGER NOT NULL,
hotel_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity REAL NOT NULL,
unit_price REAL DEFAULT 0,
total_price REAL DEFAULT 0,
custom_fields TEXT,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS inventories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hotel_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity REAL DEFAULT 0,
updated_at DATETIME,
UNIQUE(hotel_id, warehouse_id, product_id)
)`,
`CREATE TABLE IF NOT EXISTS inventory_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hotel_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
direction TEXT,
quantity REAL,
qty_before REAL,
qty_after REAL,
ref_type TEXT,
ref_id INTEGER,
operator_id INTEGER,
created_at DATETIME
)`,
`CREATE TABLE IF NOT EXISTS inventory_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
hotel_id INTEGER NOT NULL,
check_no TEXT,
warehouse_id INTEGER NOT NULL,
operator_id INTEGER NOT NULL,
status TEXT DEFAULT 'draft',
check_date DATETIME,
remark TEXT
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS uk_hotel_check_no ON inventory_checks(hotel_id, check_no)`,
`CREATE TABLE IF NOT EXISTS inventory_check_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
check_id INTEGER NOT NULL,
hotel_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
system_qty REAL,
actual_qty REAL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS number_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hotel_id INTEGER NOT NULL,
type TEXT,
prefix TEXT DEFAULT '',
current_no INTEGER DEFAULT 0,
date_format TEXT DEFAULT 'YYYYMMDD',
updated_at DATETIME,
UNIQUE(hotel_id, type)
)`,
}
for _, stmt := range stmts {
if err := db.Exec(stmt).Error; err != nil {
panic(fmt.Sprintf("failed to create table: %v\nSQL: %s", err, stmt))
}
}
return db
}
// CreateTestHotel 创建测试酒店
func CreateTestHotel(db *gorm.DB, code string) *model.Hotel {
hotel := &model.Hotel{
Name: "Test Hotel " + code,
Code: code,
}
if err := db.Create(hotel).Error; err != nil {
panic(fmt.Sprintf("failed to create test hotel: %v", err))
}
return hotel
}
// hashPassword 内部使用的密码哈希函数,避免循环依赖
func hashPassword(plain string) string {
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
panic(fmt.Sprintf("failed to hash password: %v", err))
}
return string(b)
}
// CreateTestUser 创建测试用户
func CreateTestUser(db *gorm.DB, hotelID uint64, username, password, role string) *model.User {
hash := hashPassword(password)
user := &model.User{
TenantBase: model.TenantBase{
Base: model.Base{},
HotelID: hotelID,
},
Username: username,
PasswordHash: hash,
RealName: "Test User " + username,
Role: role,
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
panic(fmt.Sprintf("failed to create test user: %v", err))
}
return user
}
// CreateTestWarehouse 创建测试仓库
func CreateTestWarehouse(db *gorm.DB, hotelID uint64, name string) *model.Warehouse {
w := &model.Warehouse{
TenantBase: model.TenantBase{
HotelID: hotelID,
},
Name: name,
IsDefault: true,
}
if err := db.Create(w).Error; err != nil {
panic(fmt.Sprintf("failed to create test warehouse: %v", err))
}
return w
}
// CreateTestProduct 创建测试商品
func CreateTestProduct(db *gorm.DB, hotelID uint64, name string) *model.Product {
p := &model.Product{
TenantBase: model.TenantBase{
HotelID: hotelID,
},
Name: name,
Code: "P-" + name,
Unit: "个",
}
if err := db.Create(p).Error; err != nil {
panic(fmt.Sprintf("failed to create test product: %v", err))
}
return p
}
// GetAuthToken 生成测试 JWT token
func GetAuthToken(userID, hotelID uint64, role string) string {
InitConfig()
now := time.Now()
claims := middleware.Claims{
UserID: userID,
HotelID: hotelID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(now),
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
SignedString([]byte(config.C.JWT.Secret))
if err != nil {
panic(fmt.Sprintf("failed to sign token: %v", err))
}
return token
}