// Package dbtest provides shared helpers for integration tests that need // a clean Postgres state without disturbing the operator's admin user. // // Background: when integration tests run against the same Postgres // instance an operator uses for local dev (a common dev-laptop setup), // a TRUNCATE that includes the users table wipes the operator's admin // login between every test run. To avoid that, tests should: // // 1. Call ResetDB instead of issuing TRUNCATE statements directly. It // truncates every data table EXCEPT users, then deletes only those // users whose username starts with TestUserPrefix. // 2. Always create test user rows with a username that begins with // TestUserPrefix; otherwise those rows survive across runs and // leak into other tests. // // One test legitimately needs to wipe the entire users table: // internal/auth/bootstrap_test.go, which exercises first-time admin // bootstrap. That file does its own TRUNCATE and does not use this // helper. package dbtest import ( "context" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" ) // TestUserPrefix is the required username prefix for any user row // created by an integration test. ResetDB removes every user matching // this prefix and leaves all others intact. const TestUserPrefix = "test-" // dataTables is the union of every non-users table touched by any // integration test in the repo. Truncating the union is harmless for // callers that only care about a subset; truncating tables that don't // exist would error, but every name here corresponds to a migration // that has shipped. var dataTables = []string{ "artist_similarity", "track_similarity", "artist_similarity_unmatched", // M5c "scrobble_queue", "contextual_likes", "general_likes_albums", "general_likes_artists", "general_likes", "play_events", "skip_events", "play_sessions", "sessions", "lidarr_quarantine_actions", "lidarr_quarantine", "playlist_tracks", "playlists", "library_changes", // M7 #357 — must reset to keep cursor isolated per test // SettingsService.reconcile() idempotently re-UpsertProviderSettings // for every registered provider at boot, so truncating this is the // correct per-test reset (clears test-modified enabled/api_key rows). // cover_art_sources_meta is NOT truncated — boot only READS it // (never recreates the singleton, seeded once by 0018); ResetDB // resets its counter via UPDATE below instead. "cover_art_provider_settings", // recsettings.New reconciles shipped defaults on every construction // (#1250), so truncating gives each test pristine tuning values. "recommendation_weight_profiles", "taste_tuning", "recommendation_tuning_audit", "tracks", "albums", "artists", } // ResetDB clears all data tables and removes any user whose username // begins with TestUserPrefix. Users without that prefix (notably the // operator's admin row) are left alone. Calls t.Fatalf on error. func ResetDB(t *testing.T, pool *pgxpool.Pool) { t.Helper() ctx := context.Background() stmt := "TRUNCATE " + strings.Join(dataTables, ", ") + " RESTART IDENTITY CASCADE" if _, err := pool.Exec(ctx, stmt); err != nil { t.Fatalf("dbtest.ResetDB truncate: %v", err) } if _, err := pool.Exec(ctx, "DELETE FROM users WHERE username LIKE $1", TestUserPrefix+"%", ); err != nil { t.Fatalf("dbtest.ResetDB delete test users: %v", err) } // Reset the monotonic cover-art source-version counter to its // post-migration seeded value. Truncating the row would break // SettingsService boot, which reads (never recreates) this // singleton; an UPDATE keeps the row while clearing cross-test // version accumulation (CurrentVersion=4 want 1, key-only-bump). if _, err := pool.Exec(ctx, "UPDATE cover_art_sources_meta SET current_version = 1", ); err != nil { t.Fatalf("dbtest.ResetDB reset cover-art version: %v", err) } }