Files
junhong_cmp_fiber/tests/unit/model_test.go
huang 984ccccc63 docs(constitution): 新增数据库设计原则(v2.4.0)
在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。

主要变更:
- 新增原则IX:数据库设计原则(Database Design Principles)
- 强制要求:数据库表不得使用外键约束
- 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等)
- 强制要求:表关系必须通过ID字段手动维护
- 强制要求:关联数据查询必须显式编写,避免ORM魔法
- 强制要求:时间字段由GORM处理,不使用数据库触发器

设计理念:
- 提升业务逻辑灵活性(无数据库约束限制)
- 优化高并发性能(无外键检查开销)
- 增强代码可读性(显式查询,无隐式预加载)
- 简化数据库架构和迁移流程
- 支持分布式和微服务场景

版本升级:2.3.0 → 2.4.0(MINOR)
2025-11-13 13:40:19 +08:00

503 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package unit
import (
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert"
)
// TestUserValidation 测试用户模型验证
func TestUserValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的创建用户请求", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
Email: "valid@example.com",
Password: "password123",
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("用户名太短", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "ab", // 少于 3 个字符
Email: "valid@example.com",
Password: "password123",
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("用户名太长", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "a123456789012345678901234567890123456789012345678901", // 超过 50 个字符
Email: "valid@example.com",
Password: "password123",
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("无效的邮箱格式", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
Email: "invalid-email",
Password: "password123",
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("密码太短", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
Email: "valid@example.com",
Password: "short", // 少于 8 个字符
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("缺少必填字段", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
// 缺少 Email 和 Password
}
err := validate.Struct(req)
assert.Error(t, err)
})
}
// TestUserUpdateValidation 测试用户更新验证
func TestUserUpdateValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的更新请求", func(t *testing.T) {
email := "newemail@example.com"
status := constants.UserStatusActive
req := &model.UpdateUserRequest{
Email: &email,
Status: &status,
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("无效的邮箱格式", func(t *testing.T) {
email := "invalid-email"
req := &model.UpdateUserRequest{
Email: &email,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("无效的状态值", func(t *testing.T) {
status := "invalid_status"
req := &model.UpdateUserRequest{
Status: &status,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("空更新请求", func(t *testing.T) {
req := &model.UpdateUserRequest{}
err := validate.Struct(req)
assert.NoError(t, err) // 空更新请求应该是有效的
})
}
// TestOrderValidation 测试订单模型验证
func TestOrderValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的创建订单请求", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Remark: "测试订单",
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("订单号太短", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-123", // 少于 10 个字符
UserID: 1,
Amount: 10000,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("订单号太长", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-12345678901234567890123456789012345678901234567890", // 超过 50 个字符
UserID: 1,
Amount: 10000,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("用户ID无效", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
UserID: 0, // 用户ID必须大于0
Amount: 10000,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("金额为负数", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: -1000, // 金额不能为负数
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("缺少必填字段", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
// 缺少 UserID 和 Amount
}
err := validate.Struct(req)
assert.Error(t, err)
})
}
// TestOrderUpdateValidation 测试订单更新验证
func TestOrderUpdateValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的更新请求", func(t *testing.T) {
status := constants.OrderStatusPaid
remark := "已支付"
req := &model.UpdateOrderRequest{
Status: &status,
Remark: &remark,
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("无效的状态值", func(t *testing.T) {
status := "invalid_status"
req := &model.UpdateOrderRequest{
Status: &status,
}
err := validate.Struct(req)
assert.Error(t, err)
})
}
// TestUserModel 测试用户模型
func TestUserModel(t *testing.T) {
t.Run("创建用户模型", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
assert.Equal(t, "testuser", user.Username)
assert.Equal(t, "test@example.com", user.Email)
assert.Equal(t, constants.UserStatusActive, user.Status)
})
t.Run("用户表名", func(t *testing.T) {
user := &model.User{}
assert.Equal(t, "tb_user", user.TableName())
})
t.Run("软删除字段", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
// DeletedAt 应该是 nil (未删除)
assert.True(t, user.DeletedAt.Time.IsZero())
})
t.Run("LastLoginAt 可选字段", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
assert.Nil(t, user.LastLoginAt)
// 设置登录时间
now := time.Now()
user.LastLoginAt = &now
assert.NotNil(t, user.LastLoginAt)
assert.Equal(t, now, *user.LastLoginAt)
})
}
// TestOrderModel 测试订单模型
func TestOrderModel(t *testing.T) {
t.Run("创建订单模型", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
Remark: "测试订单",
}
assert.Equal(t, "ORD-2025-001", order.OrderID)
assert.Equal(t, uint(1), order.UserID)
assert.Equal(t, int64(10000), order.Amount)
assert.Equal(t, constants.OrderStatusPending, order.Status)
})
t.Run("订单表名", func(t *testing.T) {
order := &model.Order{}
assert.Equal(t, "tb_order", order.TableName())
})
t.Run("可选时间字段", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
}
assert.Nil(t, order.PaidAt)
assert.Nil(t, order.CompletedAt)
// 设置支付时间
now := time.Now()
order.PaidAt = &now
assert.NotNil(t, order.PaidAt)
assert.Equal(t, now, *order.PaidAt)
})
}
// TestBaseModel 测试基础模型
func TestBaseModel(t *testing.T) {
t.Run("BaseModel 字段", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
// ID 应该是 0 (未保存)
assert.Zero(t, user.ID)
// 时间戳应该是零值
assert.True(t, user.CreatedAt.IsZero())
assert.True(t, user.UpdatedAt.IsZero())
})
}
// TestUserStatusConstants 测试用户状态常量
func TestUserStatusConstants(t *testing.T) {
t.Run("用户状态常量定义", func(t *testing.T) {
assert.Equal(t, "active", constants.UserStatusActive)
assert.Equal(t, "inactive", constants.UserStatusInactive)
assert.Equal(t, "suspended", constants.UserStatusSuspended)
})
t.Run("用户状态验证", func(t *testing.T) {
validStatuses := []string{
constants.UserStatusActive,
constants.UserStatusInactive,
constants.UserStatusSuspended,
}
for _, status := range validStatuses {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: status,
}
assert.Contains(t, validStatuses, user.Status)
}
})
}
// TestOrderStatusConstants 测试订单状态常量
func TestOrderStatusConstants(t *testing.T) {
t.Run("订单状态常量定义", func(t *testing.T) {
assert.Equal(t, "pending", constants.OrderStatusPending)
assert.Equal(t, "paid", constants.OrderStatusPaid)
assert.Equal(t, "processing", constants.OrderStatusProcessing)
assert.Equal(t, "completed", constants.OrderStatusCompleted)
assert.Equal(t, "cancelled", constants.OrderStatusCancelled)
})
t.Run("订单状态流转", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
}
// 订单状态流转pending -> paid -> processing -> completed
assert.Equal(t, constants.OrderStatusPending, order.Status)
order.Status = constants.OrderStatusPaid
assert.Equal(t, constants.OrderStatusPaid, order.Status)
order.Status = constants.OrderStatusProcessing
assert.Equal(t, constants.OrderStatusProcessing, order.Status)
order.Status = constants.OrderStatusCompleted
assert.Equal(t, constants.OrderStatusCompleted, order.Status)
})
t.Run("订单取消", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-002",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
}
// 从任何状态都可以取消
order.Status = constants.OrderStatusCancelled
assert.Equal(t, constants.OrderStatusCancelled, order.Status)
})
}
// TestUserResponse 测试用户响应模型
func TestUserResponse(t *testing.T) {
t.Run("创建用户响应", func(t *testing.T) {
now := time.Now()
resp := &model.UserResponse{
ID: 1,
Username: "testuser",
Email: "test@example.com",
Status: constants.UserStatusActive,
CreatedAt: now,
UpdatedAt: now,
}
assert.Equal(t, uint(1), resp.ID)
assert.Equal(t, "testuser", resp.Username)
assert.Equal(t, "test@example.com", resp.Email)
assert.Equal(t, constants.UserStatusActive, resp.Status)
})
t.Run("用户响应不包含密码", func(t *testing.T) {
// UserResponse 结构体不应该包含 Password 字段
resp := &model.UserResponse{
ID: 1,
Username: "testuser",
Email: "test@example.com",
Status: constants.UserStatusActive,
}
// 验证结构体大小合理 (不包含密码字段)
assert.NotNil(t, resp)
})
}
// TestListResponse 测试列表响应模型
func TestListResponse(t *testing.T) {
t.Run("用户列表响应", func(t *testing.T) {
users := []model.UserResponse{
{ID: 1, Username: "user1", Email: "user1@example.com", Status: constants.UserStatusActive},
{ID: 2, Username: "user2", Email: "user2@example.com", Status: constants.UserStatusActive},
}
resp := &model.ListUsersResponse{
Users: users,
Page: 1,
PageSize: 20,
Total: 100,
TotalPages: 5,
}
assert.Equal(t, 2, len(resp.Users))
assert.Equal(t, 1, resp.Page)
assert.Equal(t, 20, resp.PageSize)
assert.Equal(t, int64(100), resp.Total)
assert.Equal(t, 5, resp.TotalPages)
})
t.Run("订单列表响应", func(t *testing.T) {
orders := []model.OrderResponse{
{ID: 1, OrderID: "ORD-001", UserID: 1, Amount: 10000, Status: constants.OrderStatusPending},
{ID: 2, OrderID: "ORD-002", UserID: 1, Amount: 20000, Status: constants.OrderStatusPaid},
}
resp := &model.ListOrdersResponse{
Orders: orders,
Page: 1,
PageSize: 20,
Total: 50,
TotalPages: 3,
}
assert.Equal(t, 2, len(resp.Orders))
assert.Equal(t, 1, resp.Page)
assert.Equal(t, 20, resp.PageSize)
assert.Equal(t, int64(50), resp.Total)
assert.Equal(t, 3, resp.TotalPages)
})
}
// TestFieldTags 测试字段标签
func TestFieldTags(t *testing.T) {
t.Run("User GORM 标签", func(t *testing.T) {
user := &model.User{}
// 验证 TableName 方法存在
tableName := user.TableName()
assert.Equal(t, "tb_user", tableName)
})
t.Run("Order GORM 标签", func(t *testing.T) {
order := &model.Order{}
// 验证 TableName 方法存在
tableName := order.TableName()
assert.Equal(t, "tb_order", tableName)
})
}