2026-03-15 07:07:59 +01:00
package db_test
// migrate_test.go — TDD tests for the tracked migration system.
//
// RED phase: these tests are written before the implementation exists.
// They verify the contract of MigrateFS after it gains schema_versions tracking.
//
// Test matrix:
// TestMigrate_SchemaVersionsTableCreated — schema_versions exists after first run
// TestMigrate_AllMigrationsRecorded — every applied file is recorded
// TestMigrate_SkipsAlreadyApplied — second call skips files already in schema_versions
// TestMigrate_AppliesNewMigrationsOnly — only new files are applied on subsequent runs
// TestMigrate_OrderIsLexicographic — migrations execute in sorted filename order
// TestMigrate_SeedExistingDatabase — existing DB (no schema_versions) is seeded
// TestMigrate_SeedDoesNotReRunMigrations — seeded migrations are not re-executed
// TestMigrate_SchemaVersionsAppliedAtRecorded — applied_at column is populated
// TestMigrate_EmptyFSSucceeds — empty FS is fine, no error
// TestMigrate_InvalidSQLReturnsError — bad SQL still surfaces as an error
// TestMigrate_ReadFileErrorReturnsError — FS read failure surfaces as an error
// TestMigrate_PartialRunRecordsOnlyApplied — failure mid-run leaves earlier files recorded
// TestMigrate_AppliedAtIsISO8601 — applied_at timestamp format is valid
import (
2026-07-23 17:03:52 +02:00
"context"
2026-03-15 07:07:59 +01:00
"database/sql"
"fmt"
"io/fs"
"strings"
"testing"
"testing/fstest"
"time"
2026-08-28 06:54:32 +02:00
"github.com/J3vb/OwnCord/Server/db"
2026-03-15 07:07:59 +01:00
)
// failReadDirFS is an fs.FS whose root Open succeeds but ReadDir always errors.
// This exercises the sqlFilenames ReadDir error path.
type failReadDirFS struct {}
func ( failReadDirFS ) Open ( name string ) ( fs . File , error ) {
if name == "." {
return & badDirFile {}, nil
}
return nil , fmt . Errorf ( "no files" )
}
type badDirFile struct {}
2026-04-01 11:38:11 +02:00
func ( badDirFile ) Read ([] byte ) ( int , error ) { return 0 , fmt . Errorf ( "not a file" ) }
func ( badDirFile ) Close () error { return nil }
func ( badDirFile ) Stat () ( fs . FileInfo , error ) { return fakeDirInfo {}, nil }
2026-03-15 07:07:59 +01:00
func ( badDirFile ) ReadDir ( int ) ([] fs . DirEntry , error ) {
return nil , fmt . Errorf ( "readdir always fails" )
}
// ---- helpers ----------------------------------------------------------------
// countVersions returns the number of rows in schema_versions.
func countVersions ( t * testing . T , database * db . DB ) int {
t . Helper ()
var n int
2026-07-23 17:03:52 +02:00
err := database . QueryRowContext ( context . Background (), "SELECT COUNT(*) FROM schema_versions" ). Scan ( & n )
2026-03-15 07:07:59 +01:00
if err != nil {
t . Fatalf ( "counting schema_versions: %v" , err )
}
return n
}
// hasVersion reports whether a specific filename is recorded in schema_versions.
func hasVersion ( t * testing . T , database * db . DB , filename string ) bool {
t . Helper ()
var v string
2026-07-23 17:03:52 +02:00
err := database . QueryRowContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"SELECT version FROM schema_versions WHERE version = ?" , filename ,
). Scan ( & v )
if err == sql . ErrNoRows {
return false
}
if err != nil {
t . Fatalf ( "querying schema_versions for %q: %v" , filename , err )
}
return true
}
// tableExists reports whether a table (or virtual table) exists in sqlite_master.
func tableExists ( t * testing . T , database * db . DB , name string ) bool {
t . Helper ()
var n string
2026-07-23 17:03:52 +02:00
err := database . QueryRowContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"SELECT name FROM sqlite_master WHERE type='table' AND name=?" , name ,
). Scan ( & n )
if err == sql . ErrNoRows {
return false
}
if err != nil {
t . Fatalf ( "checking table %q: %v" , name , err )
}
return true
}
// simpleFS builds an fstest.MapFS with the provided filename→SQL pairs.
func simpleFS ( pairs ... string ) fstest . MapFS {
if len ( pairs ) % 2 != 0 {
panic ( "simpleFS requires an even number of arguments (name, sql, ...)" )
}
m := fstest . MapFS {}
for i := 0 ; i < len ( pairs ); i += 2 {
m [ pairs [ i ]] = & fstest . MapFile { Data : [] byte ( pairs [ i + 1 ])}
}
return m
}
// ---- tests ------------------------------------------------------------------
// TestMigrate_SchemaVersionsTableCreated verifies that MigrateFS creates the
// schema_versions tracking table on first run.
func TestMigrate_SchemaVersionsTableCreated ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_create_foo.sql" , "CREATE TABLE IF NOT EXISTS foo (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
if ! tableExists ( t , database , "schema_versions" ) {
t . Error ( "schema_versions table was not created by MigrateFS" )
}
}
// TestMigrate_AllMigrationsRecorded verifies that every applied .sql file
// gets a row inserted into schema_versions.
func TestMigrate_AllMigrationsRecorded ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_alpha.sql" , "CREATE TABLE IF NOT EXISTS alpha (id INTEGER PRIMARY KEY);" ,
2026-04-01 11:38:11 +02:00
"002_beta.sql" , "CREATE TABLE IF NOT EXISTS beta (id INTEGER PRIMARY KEY);" ,
2026-03-15 07:07:59 +01:00
"003_gamma.sql" , "CREATE TABLE IF NOT EXISTS gamma (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
for _ , name := range [] string { "001_alpha.sql" , "002_beta.sql" , "003_gamma.sql" } {
if ! hasVersion ( t , database , name ) {
t . Errorf ( "migration %q not recorded in schema_versions" , name )
}
}
}
// TestMigrate_SkipsAlreadyApplied verifies that a second call to MigrateFS
// with the same FS does not re-execute already-applied migrations.
func TestMigrate_SkipsAlreadyApplied ( t * testing . T ) {
database := openMemory ( t )
// This migration inserts a row; if re-run it would violate UNIQUE.
fsys := simpleFS (
"001_unique.sql" , `
CREATE TABLE IF NOT EXISTS unique_check (val TEXT UNIQUE);
INSERT INTO unique_check (val) VALUES ('singleton');
` ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() first run error: %v" , err )
}
// Second run — must not fail even though the INSERT would conflict if re-run.
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() second run error (migration was re-executed): %v" , err )
}
// Confirm the row exists exactly once.
var count int
2026-07-23 17:03:52 +02:00
if err := database . QueryRowContext ( context . Background (), "SELECT COUNT(*) FROM unique_check WHERE val='singleton'" ). Scan ( & count ); err != nil {
2026-03-15 07:07:59 +01:00
t . Fatalf ( "counting unique_check: %v" , err )
}
if count != 1 {
t . Errorf ( "unique_check has %d rows, want exactly 1 — migration was re-run" , count )
}
}
// TestMigrate_AppliesNewMigrationsOnly verifies that when a new file is added
// to the FS, only that file is applied on the second call.
func TestMigrate_AppliesNewMigrationsOnly ( t * testing . T ) {
database := openMemory ( t )
fsFirst := simpleFS (
"001_base.sql" , "CREATE TABLE IF NOT EXISTS base_tbl (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsFirst ); err != nil {
t . Fatalf ( "MigrateFS() first run error: %v" , err )
}
versionsAfterFirst := countVersions ( t , database )
// Add a second migration.
fsSecond := simpleFS (
"001_base.sql" , "CREATE TABLE IF NOT EXISTS base_tbl (id INTEGER PRIMARY KEY);" ,
"002_extra.sql" , "CREATE TABLE IF NOT EXISTS extra_tbl (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsSecond ); err != nil {
t . Fatalf ( "MigrateFS() second run error: %v" , err )
}
versionsAfterSecond := countVersions ( t , database )
if versionsAfterSecond != versionsAfterFirst + 1 {
t . Errorf (
"expected %d version rows after second run, got %d" ,
versionsAfterFirst + 1 , versionsAfterSecond ,
)
}
if ! hasVersion ( t , database , "002_extra.sql" ) {
t . Error ( "002_extra.sql not recorded after second run" )
}
if ! tableExists ( t , database , "extra_tbl" ) {
t . Error ( "extra_tbl not created by second run" )
}
}
// TestMigrate_OrderIsLexicographic verifies that migrations are applied in
// sorted filename order, not insertion or readdir order.
func TestMigrate_OrderIsLexicographic ( t * testing . T ) {
database := openMemory ( t )
// 002 creates the table; 001 tries to insert into it.
// If run out of order (002 before 001) the INSERT would fail with "no such table".
// With lexicographic ordering 001 runs first and creates the table,
// then 002 inserts into it — so we verify the correct order by checking
// the table was created before the insert was attempted.
fsys := simpleFS (
"002_insert.sql" , "INSERT INTO order_check (label) VALUES ('second');" ,
"001_create.sql" , "CREATE TABLE IF NOT EXISTS order_check (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
var label string
2026-07-23 17:03:52 +02:00
if err := database . QueryRowContext ( context . Background (), "SELECT label FROM order_check LIMIT 1" ). Scan ( & label ); err != nil {
2026-03-15 07:07:59 +01:00
t . Fatalf ( "selecting from order_check: %v" , err )
}
if label != "second" {
t . Errorf ( "label = %q, want 'second'" , label )
}
}
// TestMigrate_SeedExistingDatabase verifies that when schema_versions does not
// exist but other known tables do (simulating an existing DB from before
// tracking was added), all current migration filenames are seeded so they are
// not re-executed.
func TestMigrate_SeedExistingDatabase ( t * testing . T ) {
database := openMemory ( t )
// Manually create a table to simulate a previously-migrated database
// that does not yet have schema_versions.
2026-07-23 17:03:52 +02:00
if _ , err := database . ExecContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY);" ,
); err != nil {
t . Fatalf ( "setup: creating users table: %v" , err )
}
// This migration would drop and recreate users; if it runs it will wipe data.
// The seeding logic must prevent it from running.
fsys := simpleFS (
"001_initial.sql" , `
DROP TABLE IF EXISTS users;
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
` ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
// The migration must be recorded (seeded).
if ! hasVersion ( t , database , "001_initial.sql" ) {
t . Error ( "001_initial.sql should be seeded into schema_versions for existing DB" )
}
// The users table must still have its original schema (no 'name' column),
// proving the DROP/CREATE did not run.
2026-07-23 17:03:52 +02:00
_ , err := database . ExecContext ( context . Background (), "INSERT INTO users (id) VALUES (42)" )
2026-03-15 07:07:59 +01:00
if err != nil {
t . Errorf ( "users table appears to have been recreated (DROP ran): %v" , err )
}
}
// TestMigrate_SeedDoesNotReRunMigrations is a companion to the seeding test:
// after seeding, a subsequent MigrateFS call with the same FS must be a no-op.
// The seeding heuristic triggers on the presence of the "users" sentinel table,
// so we create that table to simulate a pre-tracking database.
func TestMigrate_SeedDoesNotReRunMigrations ( t * testing . T ) {
database := openMemory ( t )
// Simulate an existing DB: create the "users" sentinel table so the seeding
// heuristic fires, plus the table that the migration would modify.
2026-07-23 17:03:52 +02:00
if _ , err := database . ExecContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY);" ,
); err != nil {
t . Fatalf ( "setup users: %v" , err )
}
2026-07-23 17:03:52 +02:00
if _ , err := database . ExecContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"CREATE TABLE IF NOT EXISTS existing (id INTEGER PRIMARY KEY);" ,
); err != nil {
t . Fatalf ( "setup existing: %v" , err )
}
// This migration would INSERT into existing; if it runs, count becomes 1.
fsys := simpleFS (
"001_existing.sql" , `
CREATE TABLE IF NOT EXISTS existing (id INTEGER PRIMARY KEY);
INSERT INTO existing (id) VALUES (1);
` ,
)
// First call — seeds because schema_versions is absent AND users table exists.
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() first run error: %v" , err )
}
// Second call — must be a no-op (migration is already recorded).
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() second run error: %v" , err )
}
// existing table should be empty — the INSERT was never executed (seeded only).
var count int
2026-07-23 17:03:52 +02:00
if err := database . QueryRowContext ( context . Background (), "SELECT COUNT(*) FROM existing" ). Scan ( & count ); err != nil {
2026-03-15 07:07:59 +01:00
t . Fatalf ( "counting existing: %v" , err )
}
if count != 0 {
t . Errorf ( "existing has %d rows, want 0 — seeded migration was re-executed" , count )
}
}
2026-08-15 16:30:05 +02:00
// TestMigrate_InterruptedSeedDoesNotOrphanTrackingTable pins OC-0213:
// schema_versions must not be created outside the seed transaction. If it
// is, an interrupted/failed first-run seed (process killed, OOM, disk
// full — anything that keeps the seed transaction from committing) leaves
// an empty schema_versions table behind. On the next start,
// schemaVersionsExists() reports true, the seeding branch is skipped
// forever, and every migration in the set is replayed against the live,
// already-populated database — including destructive ones.
//
// This test simulates the interruption with PRAGMA max_page_count: it caps
// the database's page budget so MigrateFS's seed transaction runs out of
// room partway through recording filenames, exactly like a crash mid-seed.
// It then lifts the cap (as a real restart would have headroom again) and
// calls MigrateFS a second time, verifying that seeding — not destructive
// execution — is what happens.
func TestMigrate_InterruptedSeedDoesNotOrphanTrackingTable ( t * testing . T ) {
database := openMemory ( t )
ctx := context . Background ()
// Simulate a pre-tracking existing database: the "users" sentinel table
// triggers the seeding heuristic, and carries data that the destructive
// migration below would wipe if it were ever executed instead of seeded.
if _ , err := database . ExecContext ( ctx ,
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)" ,
); err != nil {
t . Fatalf ( "setup users table: %v" , err )
}
if _ , err := database . ExecContext ( ctx ,
"INSERT INTO users (id, name) VALUES (1, 'admin')" ,
); err != nil {
t . Fatalf ( "setup admin row: %v" , err )
}
// A large migration set: one file is destructive (drops and recreates
// users, losing the row above), and hundreds of harmless, idempotent
// files pad the seed transaction out so a tight page budget is
// guaranteed to run out partway through — not on the very first insert,
// not never.
pairs := make ([] string , 0 , 2 * 502 )
pairs = append ( pairs ,
"000_destroy_users.sql" ,
"DROP TABLE IF EXISTS users; CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);" ,
)
for i := 1 ; i <= 500 ; i ++ {
pairs = append ( pairs ,
fmt . Sprintf ( "%04d_noop.sql" , i ),
"CREATE TABLE IF NOT EXISTS placeholder (id INTEGER PRIMARY KEY);" ,
)
}
fsys := simpleFS ( pairs ... )
var basePages int
if err := database . QueryRowContext ( ctx , "PRAGMA page_count" ). Scan ( & basePages ); err != nil {
t . Fatalf ( "PRAGMA page_count: %v" , err )
}
if _ , err := database . ExecContext ( ctx , fmt . Sprintf ( "PRAGMA max_page_count = %d" , basePages + 3 )); err != nil {
t . Fatalf ( "PRAGMA max_page_count: %v" , err )
}
// First "startup": the seed transaction is interrupted partway through.
if err := db . MigrateFS ( database , fsys ); err == nil {
t . Fatal ( "MigrateFS() under the page-budget constraint: expected an error simulating an interrupted seed, got nil" )
}
// Lift the constraint — the next real startup would run on a machine
// with headroom restored.
if _ , err := database . ExecContext ( ctx , "PRAGMA max_page_count = 4294967294" ); err != nil {
t . Fatalf ( "PRAGMA max_page_count reset: %v" , err )
}
// Second "startup" (the retry). If the interrupted seed above left an
// orphaned, empty schema_versions table behind, MigrateFS now believes
// tracking is already in place, skips seeding entirely, and applies
// every migration for real — including 000_destroy_users.sql.
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() second run error: %v" , err )
}
var name string
err := database . QueryRowContext ( ctx , "SELECT name FROM users WHERE id = 1" ). Scan ( & name )
if err != nil {
t . Fatalf ( "users row id=1 is gone: 000_destroy_users.sql was executed instead of seeded — an interrupted seed orphaned an empty schema_versions table: %v" , err )
}
if name != "admin" {
t . Errorf ( "users.name = %q, want %q — users table appears to have been recreated" , name , "admin" )
}
}
2026-03-15 07:07:59 +01:00
// TestMigrate_SchemaVersionsAppliedAtRecorded verifies that applied_at is
// populated for every recorded migration.
func TestMigrate_SchemaVersionsAppliedAtRecorded ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_ts.sql" , "CREATE TABLE IF NOT EXISTS ts_test (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
var appliedAt string
2026-07-23 17:03:52 +02:00
err := database . QueryRowContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"SELECT applied_at FROM schema_versions WHERE version = '001_ts.sql'" ,
). Scan ( & appliedAt )
if err != nil {
t . Fatalf ( "querying applied_at: %v" , err )
}
if appliedAt == "" {
t . Error ( "applied_at should not be empty" )
}
}
// TestMigrate_AppliedAtIsISO8601 verifies applied_at is a parseable datetime.
func TestMigrate_AppliedAtIsISO8601 ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_dt.sql" , "CREATE TABLE IF NOT EXISTS dt_test (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
var appliedAt string
2026-07-23 17:03:52 +02:00
if err := database . QueryRowContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"SELECT applied_at FROM schema_versions WHERE version = '001_dt.sql'" ,
). Scan ( & appliedAt ); err != nil {
t . Fatalf ( "querying applied_at: %v" , err )
}
// SQLite datetime('now') produces "YYYY-MM-DD HH:MM:SS".
formats := [] string {
"2006-01-02 15:04:05" ,
time . RFC3339 ,
}
var parsed bool
for _ , f := range formats {
if _ , err := time . Parse ( f , appliedAt ); err == nil {
parsed = true
break
}
}
if ! parsed {
t . Errorf ( "applied_at %q is not a recognised datetime format" , appliedAt )
}
}
// TestMigrate_EmptyFSSucceeds verifies that an empty FS returns no error and
// still creates the schema_versions table.
func TestMigrate_EmptyFSSucceeds ( t * testing . T ) {
database := openMemory ( t )
fsys := fstest . MapFS {}
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() with empty FS error: %v" , err )
}
if ! tableExists ( t , database , "schema_versions" ) {
t . Error ( "schema_versions should be created even for empty FS" )
}
if countVersions ( t , database ) != 0 {
t . Error ( "schema_versions should be empty for empty FS" )
}
}
// TestMigrate_InvalidSQLReturnsError verifies that a migration with invalid
// SQL causes MigrateFS to return a non-nil error.
func TestMigrate_InvalidSQLReturnsError ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_bad.sql" , "THIS IS NOT VALID SQL !!!@@@###" ,
)
err := db . MigrateFS ( database , fsys )
if err == nil {
t . Error ( "MigrateFS() should return error for invalid SQL, got nil" )
}
}
// TestMigrate_ReadFileErrorReturnsError verifies that an FS read failure
// surfaces as an error from MigrateFS.
func TestMigrate_ReadFileErrorReturnsError ( t * testing . T ) {
database := openMemory ( t )
err := db . MigrateFS ( database , failReadFS {})
if err == nil {
t . Error ( "MigrateFS() should return error when ReadFile fails" )
}
}
// TestMigrate_PartialRunRecordsOnlyApplied verifies that if the second
// migration in a set fails, only the first is recorded in schema_versions.
func TestMigrate_PartialRunRecordsOnlyApplied ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_good.sql" , "CREATE TABLE IF NOT EXISTS partial_good (id INTEGER PRIMARY KEY);" ,
2026-04-01 11:38:11 +02:00
"002_bad.sql" , "THIS IS DEFINITELY NOT SQL;" ,
2026-03-15 07:07:59 +01:00
)
_ = db . MigrateFS ( database , fsys ) // we expect an error; ignore it here
if ! hasVersion ( t , database , "001_good.sql" ) {
t . Error ( "001_good.sql should be recorded even though 002 failed" )
}
if hasVersion ( t , database , "002_bad.sql" ) {
t . Error ( "002_bad.sql should NOT be recorded because it failed" )
}
}
// TestMigrate_NonSQLFilesSkipped verifies that files without a .sql extension
// are skipped and not recorded in schema_versions.
func TestMigrate_NonSQLFilesSkipped ( t * testing . T ) {
database := openMemory ( t )
fsys := fstest . MapFS {
2026-04-01 11:38:11 +02:00
"README.md" : { Data : [] byte ( "not sql" )},
"001_ok.sql" : { Data : [] byte ( "CREATE TABLE IF NOT EXISTS ns_test (id INTEGER PRIMARY KEY);" )},
"002_ok.go" : { Data : [] byte ( "package migrations" )},
2026-03-15 07:07:59 +01:00
}
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
if hasVersion ( t , database , "README.md" ) {
t . Error ( "README.md should not be recorded in schema_versions" )
}
if hasVersion ( t , database , "002_ok.go" ) {
t . Error ( "002_ok.go should not be recorded in schema_versions" )
}
if ! hasVersion ( t , database , "001_ok.sql" ) {
t . Error ( "001_ok.sql should be recorded in schema_versions" )
}
}
// TestMigrate_WithRealMigrations is an integration smoke test: run the
// production migration set through the tracked MigrateFS and verify the
// schema_versions table contains exactly one row per .sql file in the FS.
func TestMigrate_WithRealMigrations ( t * testing . T ) {
database := openMemory ( t )
if err := db . Migrate ( database ); err != nil {
t . Fatalf ( "Migrate() error: %v" , err )
}
if ! tableExists ( t , database , "schema_versions" ) {
t . Fatal ( "schema_versions not created by Migrate()" )
}
// Count .sql files in the embedded FS by running Migrate again (no-op) and
// inspecting the version count. We just verify the count is > 0.
n := countVersions ( t , database )
if n == 0 {
t . Error ( "schema_versions is empty after running production migrations" )
}
}
// TestMigrate_WithRealMigrationsIdempotent verifies the production migration
// set can be run twice without error via the tracked path.
func TestMigrate_WithRealMigrationsIdempotent ( t * testing . T ) {
database := openMemory ( t )
if err := db . Migrate ( database ); err != nil {
t . Fatalf ( "Migrate() first run error: %v" , err )
}
if err := db . Migrate ( database ); err != nil {
t . Fatalf ( "Migrate() second run error: %v" , err )
}
}
// TestMigrate_SeedDetectionUsesKnownTable verifies the seeding heuristic: it
// must detect an existing DB by the presence of a known table (e.g. "users"),
// not by an arbitrary table name.
func TestMigrate_SeedDetectionUsesKnownTable ( t * testing . T ) {
database := openMemory ( t )
// Create only an unrelated table — not one of the known sentinel tables.
2026-07-23 17:03:52 +02:00
if _ , err := database . ExecContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"CREATE TABLE IF NOT EXISTS unrelated (id INTEGER PRIMARY KEY);" ,
); err != nil {
t . Fatalf ( "setup: %v" , err )
}
fsys := simpleFS (
"001_new.sql" , "CREATE TABLE IF NOT EXISTS new_table (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
// Since "users" table was absent, seeding must NOT have occurred and the
// migration must have actually been applied.
if ! tableExists ( t , database , "new_table" ) {
t . Error ( "new_table should exist — migration was not seeded, so it must have run" )
}
}
// TestMigrate_ErrorMessageContainsFilename verifies that when a migration
// fails, the returned error message includes the filename for easier debugging.
func TestMigrate_ErrorMessageContainsFilename ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"042_broken.sql" , "INVALID SQL STATEMENT;" ,
)
err := db . MigrateFS ( database , fsys )
if err == nil {
t . Fatal ( "expected error, got nil" )
}
if ! strings . Contains ( err . Error (), "042_broken.sql" ) {
t . Errorf ( "error %q does not mention the failing filename" , err . Error ())
}
}
// TestMigrate_ReadDirErrorReturnsError verifies that when the FS returns an
// error from ReadDir, MigrateFS propagates it as a non-nil error.
func TestMigrate_ReadDirErrorReturnsError ( t * testing . T ) {
database := openMemory ( t )
err := db . MigrateFS ( database , failReadDirFS {})
if err == nil {
t . Error ( "MigrateFS() should return error when ReadDir fails" )
}
}
// TestMigrate_SchemaVersionsHasPrimaryKey verifies the schema_versions table
// uses version as PRIMARY KEY, preventing duplicate rows for the same file.
func TestMigrate_SchemaVersionsHasPrimaryKey ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_pk.sql" , "CREATE TABLE IF NOT EXISTS pk_test (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
// Attempting a duplicate insert must fail.
2026-07-23 17:03:52 +02:00
_ , err := database . ExecContext ( context . Background (),
2026-03-15 07:07:59 +01:00
"INSERT INTO schema_versions (version, applied_at) VALUES ('001_pk.sql', datetime('now'))" ,
)
if err == nil {
t . Error ( "duplicate insert into schema_versions should fail — PRIMARY KEY not enforced" )
}
}
// TestMigrate_SeedRecordsAllFilesFromFS verifies that seeding writes a row for
// every .sql file in the FS, including when there are multiple files.
func TestMigrate_SeedRecordsAllFilesFromFS ( t * testing . T ) {
database := openMemory ( t )
// Create the users sentinel to trigger seeding on first call.
2026-07-23 17:03:52 +02:00
if _ , err := database . ExecContext ( context . Background (), "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY);" ); err != nil {
2026-03-15 07:07:59 +01:00
t . Fatalf ( "setup: %v" , err )
}
fsys := simpleFS (
"001_a.sql" , "CREATE TABLE IF NOT EXISTS seed_a (id INTEGER PRIMARY KEY);" ,
"002_b.sql" , "CREATE TABLE IF NOT EXISTS seed_b (id INTEGER PRIMARY KEY);" ,
"003_c.sql" , "CREATE TABLE IF NOT EXISTS seed_c (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
for _ , name := range [] string { "001_a.sql" , "002_b.sql" , "003_c.sql" } {
if ! hasVersion ( t , database , name ) {
t . Errorf ( "%q not seeded into schema_versions" , name )
}
}
// Tables from the seeded migrations must NOT have been created
// (seeding records without executing).
for _ , tbl := range [] string { "seed_a" , "seed_b" , "seed_c" } {
if tableExists ( t , database , tbl ) {
t . Errorf ( "table %q should not exist — migration was seeded, not executed" , tbl )
}
}
}
// TestMigrate_FreshDatabaseRunsAllMigrations verifies that a completely fresh
// database (no tables at all) runs every migration without seeding.
func TestMigrate_FreshDatabaseRunsAllMigrations ( t * testing . T ) {
database := openMemory ( t )
fsys := simpleFS (
"001_fresh.sql" , "CREATE TABLE IF NOT EXISTS fresh_a (id INTEGER PRIMARY KEY);" ,
"002_fresh.sql" , "CREATE TABLE IF NOT EXISTS fresh_b (id INTEGER PRIMARY KEY);" ,
)
if err := db . MigrateFS ( database , fsys ); err != nil {
t . Fatalf ( "MigrateFS() error: %v" , err )
}
for _ , tbl := range [] string { "fresh_a" , "fresh_b" } {
if ! tableExists ( t , database , tbl ) {
t . Errorf ( "table %q should exist on fresh database run" , tbl )
}
}
if countVersions ( t , database ) != 2 {
t . Errorf ( "expected 2 version rows, got %d" , countVersions ( t , database ))
}
}
// TestMigrate_LargeNumberOfMigrations verifies that MigrateFS can handle
// a large set of migrations without degrading or skipping any.
func TestMigrate_LargeNumberOfMigrations ( t * testing . T ) {
database := openMemory ( t )
const n = 50
pairs := make ([] string , 0 , n * 2 )
for i := 1 ; i <= n ; i ++ {
name := fmt . Sprintf ( "%03d_large.sql" , i )
sql := fmt . Sprintf ( "CREATE TABLE IF NOT EXISTS large_%03d (id INTEGER PRIMARY KEY);" , i )
pairs = append ( pairs , name , sql )
}
if err := db . MigrateFS ( database , simpleFS ( pairs ... )); err != nil {
t . Fatalf ( "MigrateFS() error with %d migrations: %v" , n , err )
}
got := countVersions ( t , database )
if got != n {
t . Errorf ( "schema_versions has %d rows, want %d" , got , n )
}
}
2026-08-01 22:06:14 +02:00
// TestMigrate_022SeedsMentionEveryone locks migration 022: the privileged
// seeded roles gain MENTION_EVERYONE (bit 21) and the plain Member role does
// not, so @everyone stays gated after an upgrade of an existing database.
func TestMigrate_022SeedsMentionEveryone ( t * testing . T ) {
database := openMemory ( t )
if err := db . Migrate ( database ); err != nil {
t . Fatalf ( "Migrate() error: %v" , err )
}
const mentionEveryone = int64 ( 0x200000 )
for _ , tc := range [] struct {
roleID int64
name string
want bool
}{
{ 1 , "Owner" , true },
{ 2 , "Admin" , true },
{ 3 , "Moderator" , true },
{ 4 , "Member" , false },
} {
var perms int64
if err := database . QueryRowContext ( context . Background (),
`SELECT permissions FROM roles WHERE id = ?` , tc . roleID ). Scan ( & perms ); err != nil {
t . Fatalf ( "read role %s: %v" , tc . name , err )
}
if got := perms & mentionEveryone != 0 ; got != tc . want {
t . Errorf ( "%s MENTION_EVERYONE = %v, want %v (perms=0x%X)" , tc . name , got , tc . want , perms )
}
}
}
// TestMigrate_022CreatesMentionSchema locks the storage phase 3 relies on.
func TestMigrate_022CreatesMentionSchema ( t * testing . T ) {
database := openMemory ( t )
if err := db . Migrate ( database ); err != nil {
t . Fatalf ( "Migrate() error: %v" , err )
}
if ! tableExists ( t , database , "message_mentions" ) {
t . Error ( "message_mentions table not created" )
}
var n int
if err := database . QueryRowContext ( context . Background (),
`SELECT COUNT(*) FROM pragma_table_info('messages') WHERE name = 'mentions_everyone'` ,
). Scan ( & n ); err != nil {
t . Fatalf ( "pragma_table_info: %v" , err )
}
if n != 1 {
t . Error ( "messages.mentions_everyone column not added" )
}
}