From 531566291b991b4af443f5f7054dcf9c6a0a84dd Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 27 Jun 2026 16:31:02 +0200 Subject: [PATCH] test: add data-fix runner characterization tests --- internal/database/data_fixes_test.go | 142 +++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 internal/database/data_fixes_test.go diff --git a/internal/database/data_fixes_test.go b/internal/database/data_fixes_test.go new file mode 100644 index 00000000..d454854f --- /dev/null +++ b/internal/database/data_fixes_test.go @@ -0,0 +1,142 @@ +package database + +import ( + "context" + "database/sql" + "errors" + "reflect" + "testing" + + dbfixes "mal/internal/database/fixes" + + _ "github.com/mattn/go-sqlite3" +) + +func TestRunDataFixListRunsUnappliedFixesAndRecordsSuccesses(t *testing.T) { + sqlDB := newEmptyTestDB(t) + defer closeTestDB(t, sqlDB) + + var applied []string + fixes := []dbfixes.Fix{ + { + ID: "first", + Apply: func(ctx context.Context, sqlDB *sql.DB, deps dbfixes.Dependencies) error { + applied = append(applied, "first") + return nil + }, + }, + { + ID: "second", + Apply: func(ctx context.Context, sqlDB *sql.DB, deps dbfixes.Dependencies) error { + applied = append(applied, "second") + return nil + }, + }, + } + + if err := runDataFixList(sqlDB, dbfixes.Dependencies{}, fixes); err != nil { + t.Fatalf("runDataFixList: %v", err) + } + + if !reflect.DeepEqual(applied, []string{"first", "second"}) { + t.Fatalf("applied fixes = %v, want [first second]", applied) + } + assertAppliedDataFixes(context.Background(), t, sqlDB, []string{"first", "second"}) +} + +func TestRunDataFixListSkipsAlreadyAppliedFixes(t *testing.T) { + sqlDB := newEmptyTestDB(t) + defer closeTestDB(t, sqlDB) + + ctx := context.Background() + if _, err := sqlDB.ExecContext(ctx, `CREATE TABLE data_fixes (id TEXT PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)`); err != nil { + t.Fatalf("create data_fixes: %v", err) + } + if _, err := sqlDB.ExecContext(ctx, `INSERT INTO data_fixes (id) VALUES ('already-applied')`); err != nil { + t.Fatalf("insert applied fix: %v", err) + } + + var ran bool + fixes := []dbfixes.Fix{ + { + ID: "already-applied", + Apply: func(ctx context.Context, sqlDB *sql.DB, deps dbfixes.Dependencies) error { + ran = true + return nil + }, + }, + } + + if err := runDataFixList(sqlDB, dbfixes.Dependencies{}, fixes); err != nil { + t.Fatalf("runDataFixList: %v", err) + } + + if ran { + t.Fatal("already applied fix ran") + } + assertAppliedDataFixes(context.Background(), t, sqlDB, []string{"already-applied"}) +} + +func TestRunDataFixListDoesNotRecordFailedFix(t *testing.T) { + sqlDB := newEmptyTestDB(t) + defer closeTestDB(t, sqlDB) + + fixErr := errors.New("boom") + fixes := []dbfixes.Fix{ + { + ID: "fails", + Apply: func(ctx context.Context, sqlDB *sql.DB, deps dbfixes.Dependencies) error { + return fixErr + }, + }, + } + + err := runDataFixList(sqlDB, dbfixes.Dependencies{}, fixes) + if !errors.Is(err, fixErr) { + t.Fatalf("runDataFixList error = %v, want wrapped %v", err, fixErr) + } + + assertAppliedDataFixes(context.Background(), t, sqlDB, nil) +} + +func newEmptyTestDB(t *testing.T) *sql.DB { + t.Helper() + + sqlDB, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + sqlDB.SetMaxOpenConns(1) + + return sqlDB +} + +func assertAppliedDataFixes(ctx context.Context, t *testing.T, sqlDB *sql.DB, want []string) { + t.Helper() + + rows, err := sqlDB.QueryContext(ctx, `SELECT id FROM data_fixes ORDER BY id`) + if err != nil { + t.Fatalf("query data_fixes: %v", err) + } + defer func() { + if err := rows.Close(); err != nil { + t.Errorf("close rows: %v", err) + } + }() + + var got []string + for rows.Next() { + var id string + if err := rows.Scan(&id); err != nil { + t.Fatalf("scan data_fix id: %v", err) + } + got = append(got, id) + } + if err := rows.Err(); err != nil { + t.Fatalf("iterate data_fixes: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Fatalf("applied data fixes = %v, want %v", got, want) + } +}