- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
223 lines
6.1 KiB
Go
223 lines
6.1 KiB
Go
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")
|
||
}
|
||
|
||
// 超级管理员 /* 通配
|
||
var rule model.CasbinRule
|
||
if err := db.Where("ptype=? AND v0=? AND v1=?", "p", model.RoleSuperAdmin, "/*").First(&rule).Error; err != nil {
|
||
t.Errorf("expected superadmin 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 引用可用(供未来临时文件场景扩展)
|