测试库不再使用 SQLite 内存库,改为每个测试包独立的 MySQL 库, 表结构由 backend/migrations 的真实迁移建立。 基础设施 - 新增 internal/database/testdb.go:NewTestDB() 保持原签名(28 处调用零改动), 内部连接 MySQL 测试库,首次调用重建库并执行全部真实迁移,之后每次调用清空全表 - 库名 = 测试库基础名 + 包目录名,同包共享、跨包隔离,使 go test ./... 可并发执行 (共用一库时各包会互相 DROP/CREATE 导致 database exists) - 新增 NewDryRunDB():MySQL 方言 + DryRun,供断言 SQL 形态的 5 处测试使用 - 新增 AutoMigrateIfMissing():只对不存在的表 AutoMigrate, 避免改动迁移定义的索引与约束 - 新增 scripts/prepare-test-db.sh:建库与通配授权,可重复执行 - 33 处 sqlite.Open 全部替换,26 个测试文件改用 MySQL - 删除 46 处手写 SQLite 方言 DDL(AUTOINCREMENT / INTEGER PRIMARY KEY / TEXT), 表统一由真实迁移建立 - go.mod 移除 gorm.io/driver/sqlite 与 mattn/go-sqlite3 - 删除生产代码里 distribution/repository.go 的 sqlite 方言分支(strftime) 对齐环境后暴露并修复的真实问题 - DATE(col) 在 MySQL 返回 DATE 类型,使 Date string 字段变成 RFC3339 时间串, 大盘日期会带出时间部分;28 处改为 DATE_FORMAT(col, '%Y-%m-%d') - 系统自动创建分销结算批次时 created_by 写入 0,违反指向 admin_users 的外键, 生产会直接插入失败;改为写 NULL(新增 adminRef helper) - MySQL DATETIME 仅到秒,静默截断 Go 侧写入的时间, 导致租用时长计算与结算时点回读比对偏差; rental_orders 的 9 个业务时间列提到 DATETIME(6) - 测试数据补齐严格模式要求的 NOT NULL 列、修正超出列长的取值、补齐外键父记录 迁移 - 000065 的 ALTER TABLE rental_orders 一并完成业务时间列精度调整, feature 侧不再单开迁移文件 脚本与文档 - check.sh / check-backend-coverage.sh 去掉 -p 1 串行与 SQLite 相关注释 - 更新 docs/Repository层测试补充总结.md 的测试运行说明
194 lines
5.7 KiB
Go
194 lines
5.7 KiB
Go
package notification
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"hfb_sys/backend/internal/database"
|
|
"hfb_sys/backend/internal/model"
|
|
)
|
|
|
|
func TestRepositoryUnreadCountAndMarkRead(t *testing.T) {
|
|
db := database.NewTestDB()
|
|
if err := database.AutoMigrateIfMissing(db, &model.Notification{}); err != nil {
|
|
t.Fatalf("AutoMigrate() error = %v", err)
|
|
}
|
|
repo := NewRepository(db)
|
|
ctx := context.Background()
|
|
|
|
if err := db.Create(&model.Notification{
|
|
UserID: 10,
|
|
Type: "order",
|
|
Title: "订单通知",
|
|
Content: "请处理订单",
|
|
}).Error; err != nil {
|
|
t.Fatalf("Create() error = %v", err)
|
|
}
|
|
|
|
count, err := repo.UnreadCount(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("UnreadCount() error = %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("UnreadCount() = %d, want 1", count)
|
|
}
|
|
|
|
if err := repo.MarkRead(ctx, 10, 1); err != nil {
|
|
t.Fatalf("MarkRead() error = %v", err)
|
|
}
|
|
// 重复标记应保持幂等,不应误报不存在。
|
|
if err := repo.MarkRead(ctx, 10, 1); err != nil {
|
|
t.Fatalf("MarkRead() repeat error = %v", err)
|
|
}
|
|
|
|
count, err = repo.UnreadCount(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("UnreadCount() after read error = %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("UnreadCount() after read = %d, want 0", count)
|
|
}
|
|
}
|
|
|
|
func TestRepositoryMarkReadNotFound(t *testing.T) {
|
|
db := database.NewTestDB()
|
|
if err := database.AutoMigrateIfMissing(db, &model.Notification{}); err != nil {
|
|
t.Fatalf("AutoMigrate() error = %v", err)
|
|
}
|
|
repo := NewRepository(db)
|
|
|
|
err := repo.MarkRead(context.Background(), 10, 99)
|
|
if !errors.Is(err, ErrNotificationNotFound) {
|
|
t.Fatalf("MarkRead() error = %v, want ErrNotificationNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestNotificationTenantIsolation(t *testing.T) {
|
|
db := database.NewTestDB()
|
|
if err := database.AutoMigrateIfMissing(db, &model.Notification{}, &model.DistributionSite{}, &model.RentalOrder{}); err != nil {
|
|
t.Fatalf("AutoMigrate() error = %v", err)
|
|
}
|
|
repo := NewRepository(db)
|
|
ctx := context.Background()
|
|
|
|
site := model.DistributionSite{
|
|
ID: 101,
|
|
SiteCode: "branch_a",
|
|
SiteName: "分站A",
|
|
Status: "active",
|
|
}
|
|
if err := db.Create(&site).Error; err != nil {
|
|
t.Fatalf("create distribution site error: %v", err)
|
|
}
|
|
|
|
siteID := site.ID
|
|
// 1. 创建主站通知
|
|
mainNotif := model.Notification{
|
|
UserID: 20,
|
|
Type: "order",
|
|
Title: "主站订单通知",
|
|
Content: "这是主站的订单通知",
|
|
BizType: "order",
|
|
DistributionSiteID: nil,
|
|
}
|
|
if err := db.Create(&mainNotif).Error; err != nil {
|
|
t.Fatalf("create main notif error: %v", err)
|
|
}
|
|
|
|
// 2. 创建分站通知
|
|
branchNotif := model.Notification{
|
|
UserID: 20,
|
|
Type: "order",
|
|
Title: "分站A订单通知",
|
|
Content: "这是分站A的订单通知",
|
|
BizType: "order",
|
|
DistributionSiteID: &siteID,
|
|
}
|
|
if err := db.Create(&branchNotif).Error; err != nil {
|
|
t.Fatalf("create branch notif error: %v", err)
|
|
}
|
|
|
|
// 3. 验证未读数隔离
|
|
mainUnread, err := repo.UnreadCount(ctx, 20)
|
|
if err != nil || mainUnread != 1 {
|
|
t.Fatalf("mainUnread = %d, err = %v, want 1", mainUnread, err)
|
|
}
|
|
|
|
branchUnread, err := repo.UnreadCount(ctx, 20, "branch_a")
|
|
if err != nil || branchUnread != 1 {
|
|
t.Fatalf("branchUnread = %d, err = %v, want 1", branchUnread, err)
|
|
}
|
|
|
|
// 4. 验证列表查询隔离
|
|
mainList, err := repo.List(ctx, 20, 1, 10)
|
|
if err != nil || mainList.Total != 1 {
|
|
t.Fatalf("mainList total = %d, err = %v, want 1", mainList.Total, err)
|
|
}
|
|
items := mainList.Items.([]NotificationDTO)
|
|
if items[0].Title != "主站订单通知" {
|
|
t.Fatalf("mainList item title = %s, want 主站订单通知", items[0].Title)
|
|
}
|
|
|
|
branchList, err := repo.List(ctx, 20, 1, 10, "branch_a")
|
|
if err != nil || branchList.Total != 1 {
|
|
t.Fatalf("branchList total = %d, err = %v, want 1", branchList.Total, err)
|
|
}
|
|
branchItems := branchList.Items.([]NotificationDTO)
|
|
if branchItems[0].Title != "分站A订单通知" {
|
|
t.Fatalf("branchList item title = %s, want 分站A订单通知", branchItems[0].Title)
|
|
}
|
|
|
|
// 5. 跨站点标记单条已读阻断
|
|
err = repo.MarkRead(ctx, 20, mainNotif.ID, "branch_a")
|
|
if !errors.Is(err, ErrNotificationNotFound) {
|
|
t.Fatalf("cross-site MarkRead error = %v, want ErrNotificationNotFound", err)
|
|
}
|
|
|
|
// 6. 分站全部已读不影响主站
|
|
affected, err := repo.MarkAllRead(ctx, 20, "branch_a")
|
|
if err != nil || affected != 1 {
|
|
t.Fatalf("MarkAllRead affected = %d, err = %v, want 1", affected, err)
|
|
}
|
|
|
|
branchUnreadAfter, err := repo.UnreadCount(ctx, 20, "branch_a")
|
|
if err != nil || branchUnreadAfter != 0 {
|
|
t.Fatalf("branchUnreadAfter = %d, want 0", branchUnreadAfter)
|
|
}
|
|
|
|
mainUnreadAfter, err := repo.UnreadCount(ctx, 20)
|
|
if err != nil || mainUnreadAfter != 1 {
|
|
t.Fatalf("mainUnreadAfter = %d, want 1 (should remain unread)", mainUnreadAfter)
|
|
}
|
|
|
|
// 7. 测试 Append 自动关联订单 distribution_site_id
|
|
order := model.RentalOrder{
|
|
ID: 3001,
|
|
DistributionSiteID: &siteID,
|
|
OrderNo: "ORD3001",
|
|
}
|
|
if err := db.Create(&order).Error; err != nil {
|
|
t.Fatalf("create order error: %v", err)
|
|
}
|
|
orderID := order.ID
|
|
err = Append(db, Entry{
|
|
UserID: 20,
|
|
Type: "order",
|
|
Title: "订单自动关联分站",
|
|
Content: "自动读取订单 site ID",
|
|
BizType: "order",
|
|
BizID: &orderID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Append with auto lookup error: %v", err)
|
|
}
|
|
|
|
var autoNotif model.Notification
|
|
if err := db.Where("biz_id = ? AND biz_type = 'order'", orderID).First(&autoNotif).Error; err != nil {
|
|
t.Fatalf("find autoNotif error: %v", err)
|
|
}
|
|
if autoNotif.DistributionSiteID == nil || *autoNotif.DistributionSiteID != siteID {
|
|
t.Fatalf("autoNotif.DistributionSiteID = %v, want %d", autoNotif.DistributionSiteID, siteID)
|
|
}
|
|
}
|