170 lines
4.6 KiB
Go
170 lines
4.6 KiB
Go
|
|
package store
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"testing/fstest"
|
||
|
|
|
||
|
|
_ "modernc.org/sqlite"
|
||
|
|
)
|
||
|
|
|
||
|
|
// openRawDB opens a raw *sql.DB to a fresh temp SQLite file and wraps it in
|
||
|
|
// a Store — without applying the real embedded migrations. Tests use it to
|
||
|
|
// exercise migrate() with synthetic migration sets.
|
||
|
|
func openRawDB(t *testing.T) *Store {
|
||
|
|
t.Helper()
|
||
|
|
path := filepath.Join(t.TempDir(), "broker.db")
|
||
|
|
db, err := sql.Open("sqlite", buildDSN(path))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sql.Open: %v", err)
|
||
|
|
}
|
||
|
|
t.Cleanup(func() { _ = db.Close() })
|
||
|
|
return &Store{db: db, path: path}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMigrate_AppliesOnlyPendingVersions(t *testing.T) {
|
||
|
|
ctx := t.Context()
|
||
|
|
s := openRawDB(t)
|
||
|
|
|
||
|
|
fsys := fstest.MapFS{
|
||
|
|
"m/0001_a.sql": {Data: []byte("CREATE TABLE t1 (x INTEGER);")},
|
||
|
|
"m/0002_b.sql": {Data: []byte("CREATE TABLE t2 (y INTEGER);")},
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := s.migrate(ctx, fsys, "m"); err != nil {
|
||
|
|
t.Fatalf("first migrate: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Re-running is a no-op; both migrations already recorded.
|
||
|
|
if err := s.migrate(ctx, fsys, "m"); err != nil {
|
||
|
|
t.Fatalf("second migrate: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var count int
|
||
|
|
if err := s.db.QueryRowContext(ctx,
|
||
|
|
`SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil {
|
||
|
|
t.Fatalf("count: %v", err)
|
||
|
|
}
|
||
|
|
if count != 2 {
|
||
|
|
t.Errorf("applied count = %d, want 2", count)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMigrate_BadSQLRollsBack(t *testing.T) {
|
||
|
|
ctx := t.Context()
|
||
|
|
s := openRawDB(t)
|
||
|
|
|
||
|
|
fsys := fstest.MapFS{
|
||
|
|
"m/0001_good.sql": {Data: []byte("CREATE TABLE ok (x INTEGER);")},
|
||
|
|
"m/0002_bad.sql": {Data: []byte("THIS IS NOT SQL")},
|
||
|
|
}
|
||
|
|
|
||
|
|
err := s.migrate(ctx, fsys, "m")
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("expected migrate to fail on bad SQL")
|
||
|
|
}
|
||
|
|
if !strings.Contains(err.Error(), "0002_bad.sql") {
|
||
|
|
t.Errorf("error should mention failing migration: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0001 should have been applied and committed; 0002 should not.
|
||
|
|
var count int
|
||
|
|
if err := s.db.QueryRowContext(ctx,
|
||
|
|
`SELECT COUNT(*) FROM schema_migrations`).Scan(&count); err != nil {
|
||
|
|
t.Fatalf("count: %v", err)
|
||
|
|
}
|
||
|
|
if count != 1 {
|
||
|
|
t.Errorf("after failed migrate, applied count = %d, want 1", count)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMigrate_MalformedFilenameFails(t *testing.T) {
|
||
|
|
ctx := t.Context()
|
||
|
|
s := openRawDB(t)
|
||
|
|
|
||
|
|
fsys := fstest.MapFS{
|
||
|
|
"m/no_number.sql": {Data: []byte("SELECT 1;")},
|
||
|
|
}
|
||
|
|
err := s.migrate(ctx, fsys, "m")
|
||
|
|
if err == nil || !strings.Contains(err.Error(), "non-numeric") {
|
||
|
|
t.Errorf("want malformed-filename error, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMigrate_LoadAppliedVersionsOnClosedDBFails(t *testing.T) {
|
||
|
|
// Exercise the error path in loadAppliedVersions by closing the DB
|
||
|
|
// before migrate reads schema_migrations.
|
||
|
|
ctx := t.Context()
|
||
|
|
s := openRawDB(t)
|
||
|
|
if _, err := s.db.ExecContext(ctx, `
|
||
|
|
CREATE TABLE schema_migrations (
|
||
|
|
version INTEGER PRIMARY KEY,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
applied_at TEXT NOT NULL
|
||
|
|
)`); err != nil {
|
||
|
|
t.Fatalf("seed: %v", err)
|
||
|
|
}
|
||
|
|
// Close so the subsequent query fails.
|
||
|
|
if err := s.db.Close(); err != nil {
|
||
|
|
t.Fatalf("close: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
_, err := loadAppliedVersions(ctx, s.db)
|
||
|
|
if err == nil {
|
||
|
|
t.Error("expected error from loadAppliedVersions on closed DB")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMigrate_RecordStepFails_OnPKConflict(t *testing.T) {
|
||
|
|
// A migration that manually inserts its own version row forces
|
||
|
|
// applyMigration's own INSERT into schema_migrations to fail with a
|
||
|
|
// primary-key conflict. Exercises the record-step error branch.
|
||
|
|
ctx := t.Context()
|
||
|
|
s := openRawDB(t)
|
||
|
|
|
||
|
|
fsys := fstest.MapFS{
|
||
|
|
"m/0001_hijack.sql": {Data: []byte(
|
||
|
|
`INSERT INTO schema_migrations (version, name) VALUES (1, 'hijack');`)},
|
||
|
|
}
|
||
|
|
err := s.migrate(ctx, fsys, "m")
|
||
|
|
if err == nil || !strings.Contains(err.Error(), "record") {
|
||
|
|
t.Errorf("want record-fail error, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestLoadAppliedVersions_ScanError(t *testing.T) {
|
||
|
|
// Seed a schema_migrations table with a non-integer version so Scan
|
||
|
|
// into *int fails. Exercises loadAppliedVersions' scan-error branch.
|
||
|
|
ctx := t.Context()
|
||
|
|
s := openRawDB(t)
|
||
|
|
|
||
|
|
if _, err := s.db.ExecContext(ctx, `CREATE TABLE schema_migrations (
|
||
|
|
version TEXT PRIMARY KEY,
|
||
|
|
name TEXT NOT NULL,
|
||
|
|
applied_at TEXT NOT NULL
|
||
|
|
)`); err != nil {
|
||
|
|
t.Fatalf("create table: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := s.db.ExecContext(ctx,
|
||
|
|
`INSERT INTO schema_migrations VALUES ('not-a-number', 'x', 'now')`); err != nil {
|
||
|
|
t.Fatalf("seed row: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := loadAppliedVersions(ctx, s.db); err == nil {
|
||
|
|
t.Error("expected scan error on non-int version")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMigrate_CanceledContext(t *testing.T) {
|
||
|
|
s := openRawDB(t)
|
||
|
|
ctx, cancel := context.WithCancel(t.Context())
|
||
|
|
cancel()
|
||
|
|
|
||
|
|
err := s.migrate(ctx, fstest.MapFS{"m/0001_a.sql": {Data: []byte("SELECT 1;")}}, "m")
|
||
|
|
if err == nil {
|
||
|
|
t.Error("expected error on canceled context")
|
||
|
|
}
|
||
|
|
}
|