Something went wrong. Try again.
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703package store
import ( "database/sql" "embed" "fmt" "io/fs" "testing"
_ "github.com/mattn/go-sqlite3")
//go:embed sql/migrationsvar testMigrationFiles embed.FS
type fakeMigrationFS struct { shouldFailRead bool invalidSQL bool hasNewMigrations bool}
type fakeDirEntry struct { name string}
func (f fakeDirEntry) Name() string { return f.name }func (f fakeDirEntry) IsDir() bool { return false }func (f fakeDirEntry) Type() fs.FileMode { return 0 }func (f fakeDirEntry) Info() (fs.FileInfo, error) { return nil, fmt.Errorf("info not available") }
func (f *fakeMigrationFS) ReadDir(name string) ([]fs.DirEntry, error) { if name == "sql/migrations" { entries := []fs.DirEntry{ fakeDirEntry{name: "0000_create_migrations_table_up.sql"}, } if f.hasNewMigrations { entries = append(entries, fakeDirEntry{name: "0001_test_migration_up.sql"}, fakeDirEntry{name: "0001_test_migration_down.sql"}, ) } return entries, nil } return nil, fmt.Errorf("directory not found: %s", name)}
func (f *fakeMigrationFS) ReadFile(name string) ([]byte, error) { if f.shouldFailRead { return nil, fmt.Errorf("simulated read failure") } if f.invalidSQL { return []byte("INVALID SQL SYNTAX GOES HERE AND MAKES DATABASE SAD"), nil } if name == "sql/migrations/0000_create_migrations_table_up.sql" { return []byte("CREATE TABLE migrations (version TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP);"), nil } if name == "sql/migrations/0001_test_migration_up.sql" { return []byte("CREATE TABLE test_table (id INTEGER PRIMARY KEY);"), nil } if name == "sql/migrations/0001_test_migration_down.sql" { return []byte("DROP TABLE IF EXISTS test_table;"), nil } return nil, fmt.Errorf("file not found: %s", name)}
func createTestDB(t *testing.T) *sql.DB { db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatalf("Failed to create in-memory database: %v", err) }
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { t.Fatalf("Failed to enable foreign keys: %v", err) }
t.Cleanup(func() { db.Close() })
return db}
func TestNewMigrationRunner(t *testing.T) { db := createTestDB(t)
runner := CreateMigrationRunner(db, testMigrationFiles) if runner == nil { t.Fatal("NewMigrationRunner should not return nil") }
if runner.db != db { t.Error("Migration runner should store the database reference") }}
func TestMigrationRunner_RunMigrations(t *testing.T) { t.Run("runs migrations successfully", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
var count int err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&count) if err != nil { t.Fatalf("Failed to check migrations table: %v", err) }
if count != 1 { t.Error("Migrations table should exist after running migrations") }
var migrationCount int err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&migrationCount) if err != nil { t.Fatalf("Failed to count applied migrations: %v", err) }
if migrationCount == 0 { t.Error("At least one migration should be applied") } })
t.Run("handles migration directory read failure", func(t *testing.T) { db := createTestDB(t)
emptyFS := embed.FS{} runner := CreateMigrationRunner(db, emptyFS)
err := runner.RunMigrations() if err == nil { t.Error("RunMigrations should fail when migration directory cannot be read") } })
t.Run("handles migration table check failure", func(t *testing.T) { db := createTestDB(t) db.Close()
runner := CreateMigrationRunner(db, testMigrationFiles) err := runner.RunMigrations() if err == nil { t.Error("RunMigrations should fail when database connection is closed") } })
t.Run("handles migration file read failure", func(t *testing.T) { db := createTestDB(t)
fakeFS := &fakeMigrationFS{shouldFailRead: true, hasNewMigrations: true} runner := CreateMigrationRunner(db, fakeFS)
err := runner.RunMigrations() if err == nil { t.Error("RunMigrations should fail when migration file cannot be read") } })
t.Run("handles invalid SQL in migration file", func(t *testing.T) { db := createTestDB(t)
fakeFS := &fakeMigrationFS{invalidSQL: true, hasNewMigrations: true} runner := CreateMigrationRunner(db, fakeFS)
err := runner.RunMigrations() if err == nil { t.Error("RunMigrations should fail when migration contains invalid SQL") } })
t.Run("handles migration record insertion failure", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("First RunMigrations failed: %v", err) }
_, err = db.Exec("DROP TABLE migrations") if err != nil { t.Fatalf("Failed to drop migrations table: %v", err) }
_, err = db.Exec("CREATE TABLE migrations (version TEXT PRIMARY KEY CHECK(length(version) < 0))") if err != nil { t.Fatalf("Failed to create migrations table with constraint: %v", err) }
err = runner.RunMigrations() if err == nil { t.Error("RunMigrations should fail when migration record cannot be inserted") } })
t.Run("skips already applied migrations", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("First RunMigrations failed: %v", err) }
var initialCount int err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&initialCount) if err != nil { t.Fatalf("Failed to count migrations: %v", err) }
err = runner.RunMigrations() if err != nil { t.Fatalf("Second RunMigrations failed: %v", err) }
var finalCount int err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&finalCount) if err != nil { t.Fatalf("Failed to count migrations after second run: %v", err) }
if finalCount != initialCount { t.Errorf("Expected %d migrations, got %d (migrations should not be re-applied)", initialCount, finalCount) } })
t.Run("creates expected tables", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
expectedTables := []string{"migrations", "tasks", "movies", "tv_shows", "books", "notes"}
for _, tableName := range expectedTables { var count int err = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count) if err != nil { t.Fatalf("Failed to check table %s: %v", tableName, err) }
if count != 1 { t.Errorf("Table %s should exist after migrations", tableName) } } })}
func TestMigrationRunner_GetAppliedMigrations(t *testing.T) { t.Run("returns empty list when no migrations table", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
migrations, err := runner.GetAppliedMigrations() if err != nil { t.Fatalf("GetAppliedMigrations failed: %v", err) }
if len(migrations) != 0 { t.Errorf("Expected 0 migrations, got %d", len(migrations)) } })
t.Run("handles database connection failure", func(t *testing.T) { db := createTestDB(t) db.Close() runner := CreateMigrationRunner(db, testMigrationFiles)
_, err := runner.GetAppliedMigrations() if err == nil { t.Error("GetAppliedMigrations should fail when database connection is closed") } })
t.Run("handles query execution failure", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
// Close the database to trigger a query failure db.Close()
_, err = runner.GetAppliedMigrations() if err == nil { t.Error("GetAppliedMigrations should fail when database is closed") } })
t.Run("handles row scan failure", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
// Insert a record with NULL applied_at which should cause scan issues _, err = db.Exec("INSERT INTO migrations (version, applied_at) VALUES ('test', NULL)") if err != nil { t.Fatalf("Failed to insert NULL migration record: %v", err) }
_, err = runner.GetAppliedMigrations() if err == nil { t.Error("GetAppliedMigrations should fail when scanning NULL applied_at field") } })
t.Run("returns applied migrations", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
// Run migrations first err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
migrations, err := runner.GetAppliedMigrations() if err != nil { t.Fatalf("GetAppliedMigrations failed: %v", err) }
if len(migrations) == 0 { t.Error("Should have applied migrations") }
for _, migration := range migrations { if migration.Version == "" { t.Error("Migration version should not be empty") } if !migration.Applied { t.Error("Migration should be marked as applied") } if migration.AppliedAt == "" { t.Error("Migration should have applied timestamp") } }
for i := 1; i < len(migrations); i++ { if migrations[i-1].Version > migrations[i].Version { t.Error("Migrations should be sorted by version") } } })}
func TestMigrationRunner_GetAvailableMigrations(t *testing.T) { t.Run("returns available migrations from embedded files", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
migrations, err := runner.GetAvailableMigrations() if err != nil { t.Fatalf("GetAvailableMigrations failed: %v", err) }
if len(migrations) == 0 { t.Error("Should have available migrations") }
for _, migration := range migrations { if migration.Version == "" { t.Error("Migration version should not be empty") } if migration.UpSQL == "" { t.Error("Migration should have up SQL") } // Note: Down SQL might be empty for some migrations, so we don't check it }
for i := 1; i < len(migrations); i++ { if migrations[i-1].Version > migrations[i].Version { t.Error("Migrations should be sorted by version") } } })
t.Run("handles migration directory read failure", func(t *testing.T) { db := createTestDB(t)
emptyFS := embed.FS{} runner := CreateMigrationRunner(db, emptyFS)
_, err := runner.GetAvailableMigrations() if err == nil { t.Error("GetAvailableMigrations should fail when migration directory cannot be read") } })
t.Run("handles migration file read failure", func(t *testing.T) { db := createTestDB(t)
fakeFS := &fakeMigrationFS{shouldFailRead: true} runner := CreateMigrationRunner(db, fakeFS)
_, err := runner.GetAvailableMigrations() if err == nil { t.Error("GetAvailableMigrations should fail when migration file cannot be read") } })
t.Run("includes both up and down SQL when available", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
migrations, err := runner.GetAvailableMigrations() if err != nil { t.Fatalf("GetAvailableMigrations failed: %v", err) }
var foundMigrationWithDown bool for _, migration := range migrations { if migration.UpSQL != "" && migration.DownSQL != "" { foundMigrationWithDown = true break } }
if !foundMigrationWithDown { t.Log("Note: No migrations found with both up and down SQL - this may be expected") } })}
func TestMigrationRunner_Rollback(t *testing.T) { t.Run("fails when no migrations to rollback", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.Rollback() if err == nil { t.Error("Rollback should fail when no migrations are applied") } })
t.Run("handles database connection failure", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
db.Close()
err = runner.Rollback() if err == nil { t.Error("Rollback should fail when database connection is closed") } })
t.Run("handles migration directory read failure during rollback", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
emptyFS := embed.FS{} runner.migrationFiles = emptyFS
err = runner.Rollback() if err == nil { t.Error("Rollback should fail when migration directory cannot be read") } })
t.Run("handles missing down migration file", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
fakeFS := &fakeMigrationFS{} runner.migrationFiles = fakeFS
err = runner.Rollback() if err == nil { t.Error("Rollback should fail when down migration file is not found") } })
t.Run("handles down migration file read failure", func(t *testing.T) { db := createTestDB(t)
fakeFS := &fakeMigrationFS{} runner := CreateMigrationRunner(db, fakeFS)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
fakeFS.shouldFailRead = true
err = runner.Rollback() if err == nil { t.Error("Rollback should fail when down migration file cannot be read") } })
t.Run("handles invalid down migration SQL", func(t *testing.T) { db := createTestDB(t)
fakeFS := &fakeMigrationFS{} runner := CreateMigrationRunner(db, fakeFS)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
fakeFS.invalidSQL = true
err = runner.Rollback() if err == nil { t.Error("Rollback should fail when down migration contains invalid SQL") } })
t.Run("handles migration record deletion failure", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
_, err = db.Exec("DROP TABLE migrations") if err != nil { t.Fatalf("Failed to drop migrations table: %v", err) }
err = runner.Rollback() if err == nil { t.Error("Rollback should fail when migration record cannot be deleted") } })
t.Run("rolls back last migration", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
var initialCount int err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&initialCount) if err != nil { t.Fatalf("Failed to count migrations: %v", err) }
if initialCount == 0 { t.Skip("No migrations to rollback") }
err = runner.Rollback() if err != nil { t.Fatalf("Rollback failed: %v", err) }
var finalCount int err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&finalCount) if err != nil { t.Fatalf("Failed to count migrations after rollback: %v", err) }
if finalCount != initialCount-1 { t.Errorf("Expected %d migrations after rollback, got %d", initialCount-1, finalCount) } })}
func TestMigrationHelperFunctions(t *testing.T) { t.Run("extractVersionFromFilename", func(t *testing.T) { testCases := []struct { filename string expected string }{ {"0000_create_migrations_table_up.sql", "0000"}, {"0001_create_all_tables_up.sql", "0001"}, {"0002_add_indexes_down.sql", "0002"}, {"invalid_filename.sql", "invalid"}, {"", ""}, }
for _, tc := range testCases { result := extractVersionFromFilename(tc.filename) if result != tc.expected { t.Errorf("extractVersionFromFilename(%s): expected %s, got %s", tc.filename, tc.expected, result) } } })
t.Run("extractNameFromFilename", func(t *testing.T) { testCases := []struct { filename string expected string }{ {"0000_create_migrations_table_up.sql", "create_migrations_table"}, {"0001_create_all_tables_up.sql", "create_all_tables"}, {"0002_add_indexes_down.sql", "add_indexes"}, {"invalid_filename.sql", ""}, {"0003_up.sql", ""}, {"", ""}, }
for _, tc := range testCases { result := extractNameFromFilename(tc.filename) if result != tc.expected { t.Errorf("extractNameFromFilename(%s): expected %s, got %s", tc.filename, tc.expected, result) } } })}
func TestMigrationIntegration(t *testing.T) { t.Run("full migration lifecycle", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, testMigrationFiles)
available, err := runner.GetAvailableMigrations() if err != nil { t.Fatalf("GetAvailableMigrations failed: %v", err) }
if len(available) == 0 { t.Skip("No migrations available for testing") }
err = runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations failed: %v", err) }
applied, err := runner.GetAppliedMigrations() if err != nil { t.Fatalf("GetAppliedMigrations failed: %v", err) }
if len(applied) == 0 { t.Error("No migrations were applied") }
tables := []string{"tasks", "movies", "tv_shows", "books", "notes"} for _, table := range tables { var count int err = db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count) if err != nil { t.Errorf("Failed to query table %s: %v", table, err) } }
if len(applied) > 1 { // Only test rollback if we have more than one migration err = runner.Rollback() if err != nil { t.Logf("Rollback failed (may be expected): %v", err) } } })
t.Run("migration runner works with real database", func(t *testing.T) { db := createTestDB(t) runner := CreateMigrationRunner(db, migrationFiles)
err := runner.RunMigrations() if err != nil { t.Fatalf("RunMigrations with real files failed: %v", err) }
var count int err = db.QueryRow("SELECT COUNT(*) FROM migrations").Scan(&count) if err != nil { t.Fatalf("Failed to count real migrations: %v", err) }
if count == 0 { t.Error("Real migrations should be applied") } })}