Files
kandev/apps/backend/internal/db/migratelog_test.go
T
2026-05-16 20:07:57 +01:00

111 lines
3.4 KiB
Go

package db
import (
"testing"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
"github.com/kandev/kandev/internal/common/logger"
)
// observerLogger returns a *logger.Logger backed by an observer core so
// log entries can be inspected in tests.
func observerLogger(t *testing.T) (*logger.Logger, *observer.ObservedLogs) {
t.Helper()
core, logs := observer.New(zapcore.DebugLevel)
log, err := logger.NewFromZap(zap.New(core))
if err != nil {
t.Fatalf("NewFromZap: %v", err)
}
return log, logs
}
// memDB returns an in-memory SQLite connection for migration tests.
func memDB(t *testing.T) *sqlx.DB {
t.Helper()
db, err := sqlx.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
return db
}
// TestMigrateLogger_Apply_Success verifies that a fresh ALTER TABLE logs
// "migration applied" at INFO.
func TestMigrateLogger_Apply_Success(t *testing.T) {
db := memDB(t)
if _, err := db.Exec(`CREATE TABLE t (id INTEGER PRIMARY KEY)`); err != nil {
t.Fatalf("create table: %v", err)
}
log, logs := observerLogger(t)
m := NewMigrateLogger(db, log)
m.Apply("t.col", `ALTER TABLE t ADD COLUMN col TEXT DEFAULT ''`)
entries := logs.FilterMessage("migration applied").All()
if len(entries) != 1 {
t.Fatalf("expected 1 'migration applied' log, got %d", len(entries))
}
if entries[0].ContextMap()["name"] != "t.col" {
t.Errorf("name field = %q, want %q", entries[0].ContextMap()["name"], "t.col")
}
}
// TestMigrateLogger_Apply_Idempotent verifies that a duplicate-column ALTER
// produces no log output.
func TestMigrateLogger_Apply_Idempotent(t *testing.T) {
db := memDB(t)
if _, err := db.Exec(`CREATE TABLE t (id INTEGER PRIMARY KEY)`); err != nil {
t.Fatalf("create table: %v", err)
}
if _, err := db.Exec(`ALTER TABLE t ADD COLUMN col TEXT DEFAULT ''`); err != nil {
t.Fatalf("initial alter: %v", err)
}
log, logs := observerLogger(t)
m := NewMigrateLogger(db, log)
// Same statement again - should be silent.
m.Apply("t.col", `ALTER TABLE t ADD COLUMN col TEXT DEFAULT ''`)
if logs.Len() != 0 {
t.Errorf("expected no log entries for idempotent re-run, got %d: %v", logs.Len(), logs.All())
}
}
// TestMigrateLogger_Apply_BrokenStatement verifies that a genuinely broken
// statement logs at WARN and not at INFO.
func TestMigrateLogger_Apply_BrokenStatement(t *testing.T) {
db := memDB(t)
log, logs := observerLogger(t)
m := NewMigrateLogger(db, log)
// Reference a table that does not exist - produces a real error.
m.Apply("no_such.col", `ALTER TABLE no_such_table ADD COLUMN col TEXT`)
warns := logs.FilterMessage("migration failed").All()
if len(warns) != 1 {
t.Fatalf("expected 1 'migration failed' WARN log, got %d", len(warns))
}
if warns[0].Level != zapcore.WarnLevel {
t.Errorf("expected WARN level, got %v", warns[0].Level)
}
}
// TestMigrateLogger_Apply_NilLog verifies that a nil logger does not panic.
func TestMigrateLogger_Apply_NilLog(t *testing.T) {
db := memDB(t)
if _, err := db.Exec(`CREATE TABLE t (id INTEGER PRIMARY KEY)`); err != nil {
t.Fatalf("create table: %v", err)
}
m := NewMigrateLogger(db, nil)
// None of these should panic.
m.Apply("t.col", `ALTER TABLE t ADD COLUMN col TEXT DEFAULT ''`)
m.Apply("t.col", `ALTER TABLE t ADD COLUMN col TEXT DEFAULT ''`) // idempotent
m.Apply("bad", `ALTER TABLE missing ADD COLUMN x TEXT`)
}