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") } }