This commit is contained in:
truewhile
2026-10-11 21:44:20 +08:00
parent 6186c8b848
commit 3c30c64a39
2 changed files with 139 additions and 3 deletions
@@ -0,0 +1,113 @@
package repository
import (
"context"
"strings"
"sync"
"testing"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
// sqlCaptureLogger 只记录 GORM 生成的 SQL,不打印任何日志。
type sqlCaptureLogger struct {
mu sync.Mutex
lines []string
}
func (l *sqlCaptureLogger) LogMode(gormlogger.LogLevel) gormlogger.Interface { return l }
func (l *sqlCaptureLogger) Info(context.Context, string, ...interface{}) {}
func (l *sqlCaptureLogger) Warn(context.Context, string, ...interface{}) {}
func (l *sqlCaptureLogger) Error(context.Context, string, ...interface{}) {}
func (l *sqlCaptureLogger) Trace(_ context.Context, _ time.Time, fc func() (string, int64), _ error) {
sql, _ := fc()
l.mu.Lock()
l.lines = append(l.lines, sql)
l.mu.Unlock()
}
func (l *sqlCaptureLogger) all() string {
l.mu.Lock()
defer l.mu.Unlock()
return strings.Join(l.lines, "\n")
}
// newPostgresDryRunRepos 返回使用 PostgreSQL 方言的 repository 容器:
// DryRun 只构建 SQL 不执行,DisableAutomaticPing 让它无需真实数据库也能离线运行,
// 于是可以直接断言生成语句在 PostgreSQL 下是否合法。
func newPostgresDryRunRepos(t *testing.T) (*Container, *sqlCaptureLogger) {
t.Helper()
rec := &sqlCaptureLogger{}
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: "postgres://mebox:mebox@127.0.0.1:5432/mebox?sslmode=disable",
}), &gorm.Config{
DryRun: true,
DisableAutomaticPing: true,
Logger: rec,
})
if err != nil {
t.Fatalf("open dry-run postgres: %v", err)
}
return New(db), rec
}
// TestReaderListsUseDialectQuotedReservedColumns 守住阅读子系统列表查询在
// PostgreSQL 下的合法性。reader_books.order / reader_chapters.index /
// reader_replace_rules.order 都是关键字级列名(order 在 PostgreSQL 里是保留字)。
//
// 排序条件曾经写成硬编码反引号的原始字符串,例如 .Order("`order` ASC"):
// SQLite 正常,PostgreSQL 会把反引号词法分析成运算符,直接报
// `syntax error at or near "order"` (SQLSTATE 42601),书架 / 目录 / 替换规则整页 500。
// 现在改用 clause.Column,由 GORM 按方言加引号(SQLite 反引号 / PostgreSQL 双引号)。
func TestReaderListsUseDialectQuotedReservedColumns(t *testing.T) {
ctx := context.Background()
cases := []struct {
name string
run func(repos *Container) error
wants []string
}{
{
name: "books",
run: func(repos *Container) error { _, err := repos.Reader.ListBooks(ctx, "user-1"); return err },
wants: []string{`"order"`, `"updated_at" DESC`},
},
{
name: "chapters",
run: func(repos *Container) error { _, err := repos.Reader.ListChapters(ctx, "book-1"); return err },
wants: []string{`"index"`},
},
{
name: "replace rules",
run: func(repos *Container) error { _, err := repos.Reader.ListReplaceRules(ctx, "user-1"); return err },
wants: []string{`"order" DESC`, `"created_at"`},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
repos, rec := newPostgresDryRunRepos(t)
if err := tc.run(repos); err != nil {
t.Fatalf("build query: %v", err)
}
sql := rec.all()
if strings.TrimSpace(sql) == "" {
t.Fatal("no SQL captured from the repository call")
}
if strings.Contains(sql, "`") {
t.Fatalf("SQL uses backtick quoting, which PostgreSQL rejects:\n%s", sql)
}
if !strings.Contains(sql, "ORDER BY") {
t.Fatalf("SQL has no ORDER BY clause:\n%s", sql)
}
for _, want := range tc.wants {
if !strings.Contains(sql, want) {
t.Fatalf("SQL missing %q:\n%s", want, sql)
}
}
})
}
}