package database import ( "os" "path/filepath" "testing" "with_you/internal/config" "with_you/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // 注意:完整的 NewDB→autoMigrate 在 SQLite 下会因 model 包多个表共用 // 同名索引 idx_created(data_change_log / login_log / operation_log)而冲突。 // 这是 model 包的既有约束(PostgreSQL 经 CREATE INDEX IF NOT EXISTS 容忍), // 与本次分层拆分无关。因此种子逻辑测试在隔离的 SQLite 库上验证: // 仅迁移被种子函数读写的表,避开跨表索引冲突,专注验证种子正确性与幂等性。 // newSeedTestDB 创建一个仅迁移种子相关表的 SQLite 测试库。 func newSeedTestDB(t *testing.T) *gorm.DB { t.Helper() dbPath := filepath.Join(t.TempDir(), "seed_test.db") db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open sqlite failed: %v", err) } // 仅迁移种子函数涉及的表(无 idx_created 冲突) if err := db.AutoMigrate( &model.Role{}, &model.CasbinRule{}, &model.Channel{}, &model.MaterialSubject{}, &model.Post{}, // dropPostsHotScoreColumnIfExists 依赖 posts 表存在 ); err != nil { t.Fatalf("autoMigrate seed tables failed: %v", err) } t.Cleanup(func() { if sqlDB, err := db.DB(); err == nil { _ = sqlDB.Close() } }) return db } // TestSeedRoles 验证角色种子写入 5 个预定义角色。 func TestSeedRoles(t *testing.T) { db := newSeedTestDB(t) if err := seedRoles(db); err != nil { t.Fatalf("seedRoles failed: %v", err) } var count int64 db.Model(&model.Role{}).Count(&count) if count != 5 { t.Errorf("expected 5 seeded roles, got %d", count) } expected := []string{ model.RoleSuperAdmin, model.RoleAdmin, model.RoleModerator, model.RoleUser, model.RoleBanned, } for _, name := range expected { var role model.Role if err := db.Where("name = ?", name).First(&role).Error; err != nil { t.Errorf("expected role %q: %v", name, err) } } } // TestSeedRoles_Idempotent 验证角色种子幂等:表非空时跳过。 func TestSeedRoles_Idempotent(t *testing.T) { db := newSeedTestDB(t) if err := seedRoles(db); err != nil { t.Fatalf("first seedRoles: %v", err) } // 第二次调用应跳过(表已有数据) if err := seedRoles(db); err != nil { t.Fatalf("second seedRoles: %v", err) } var count int64 db.Model(&model.Role{}).Count(&count) if count != 5 { t.Errorf("idempotent seed failed: expected 5 roles, got %d", count) } } // TestSeedPermissions 验证权限策略种子写入,含超级管理员通配规则。 func TestSeedPermissions(t *testing.T) { db := newSeedTestDB(t) if err := seedPermissions(db); err != nil { t.Fatalf("seedPermissions failed: %v", err) } var count int64 db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count) if count == 0 { t.Fatal("expected seeded permissions, got 0") } // 超级管理员 admin/** 通配(globMatch 中 ** 跨 '/') var rule model.CasbinRule if err := db.Where("ptype=? AND v0=? AND v1=?", "p", model.RoleSuperAdmin, "admin/**").First(&rule).Error; err != nil { t.Errorf("expected superadmin admin/** wildcard permission: %v", err) } } // TestSeedPermissions_Idempotent 验证权限种子幂等。 func TestSeedPermissions_Idempotent(t *testing.T) { db := newSeedTestDB(t) if err := seedPermissions(db); err != nil { t.Fatalf("first seedPermissions: %v", err) } var count1 int64 db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count1) if err := seedPermissions(db); err != nil { t.Fatalf("second seedPermissions: %v", err) } var count2 int64 db.Model(&model.CasbinRule{}).Where("ptype = ?", "p").Count(&count2) if count2 != count1 { t.Errorf("seed not idempotent: first=%d, second=%d", count1, count2) } } // TestSeedChannels 验证频道种子写入 5 个默认频道。 func TestSeedChannels(t *testing.T) { db := newSeedTestDB(t) if err := seedChannels(db); err != nil { t.Fatalf("seedChannels failed: %v", err) } var count int64 db.Model(&model.Channel{}).Count(&count) if count != 5 { t.Errorf("expected 5 channels, got %d", count) } // 抽查一个频道 var ch model.Channel if err := db.Where("slug = ?", "market").First(&ch).Error; err != nil { t.Errorf("expected market channel: %v", err) } } // TestSeedMaterialSubjects 验证学科种子写入 8 个默认学科。 func TestSeedMaterialSubjects(t *testing.T) { db := newSeedTestDB(t) if err := seedMaterialSubjects(db); err != nil { t.Fatalf("seedMaterialSubjects failed: %v", err) } var count int64 db.Model(&model.MaterialSubject{}).Count(&count) if count != 8 { t.Errorf("expected 8 material subjects, got %d", count) } } // TestParseGormLogLevel 验证日志级别解析映射。 func TestParseGormLogLevel(t *testing.T) { cases := []struct { in string want logger.LogLevel }{ {"silent", logger.Silent}, {"error", logger.Error}, {"warn", logger.Warn}, {"info", logger.Info}, {"", logger.Warn}, // 默认 {"bogus", logger.Warn}, // 未知值降级为 warn } for _, c := range cases { if got := parseGormLogLevel(c.in); got != c.want { t.Errorf("parseGormLogLevel(%q) = %v, want %v", c.in, got, c.want) } } } // TestDropPostsHotScoreColumnIfExists_NoTable 验证当 posts 表不存在时安全跳过。 func TestDropPostsHotScoreColumnIfExists_NoTable(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "empty.db") db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open sqlite: %v", err) } t.Cleanup(func() { if sqlDB, err := db.DB(); err == nil { _ = sqlDB.Close() } }) // 未迁移 posts 表 → 应安全返回 nil if err := dropPostsHotScoreColumnIfExists(db); err != nil { t.Errorf("expected nil when posts table absent, got: %v", err) } } // 编译期断言:确保 NewDB 签名符合 (*config.DatabaseConfig) -> (*gorm.DB, error) var _ = func() (*gorm.DB, error) { return NewDB(&config.DatabaseConfig{}) } var _ = os.RemoveAll // 保持 os 引用可用(供未来临时文件场景扩展)