mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-12 08:16:37 +08:00
117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
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 让它无需真实数据库也能离线运行,
|
||
// SkipDefaultTransaction 则挡掉 GORM 默认事务包装——那一步会真的去开事务连接,
|
||
// 在 DryRun 下同样会拨号(ToSQL 内部也是这么绕开的)。于是可以直接断言生成语句
|
||
// 在 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,
|
||
SkipDefaultTransaction: 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)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|