From 21310e771602414d2956c58e64ce43b10a831de3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 29 Apr 2026 12:04:16 -0400 Subject: [PATCH] test: bootstrap test saves/restores non-test users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to the dbtest.ResetDB change. The bootstrap integration test fundamentally needs an empty users table — it exercises the "first time admin is created" path, which Bootstrap() guards with a count==0 check. So this test alone still has to TRUNCATE users. To keep the operator's admin login intact on a shared dev DB: - Before TRUNCATE, save every user that doesn't start with 'test-'. - Use cfg.Username = 'test-admin' for the bootstrap call so the test row is itself test-prefixed and gets cleaned up by other tests' ResetDB calls. - t.Cleanup restores the saved rows after assertions complete, so admin/admin (or whatever password the operator set) keeps working between test runs. Co-Authored-By: Claude Opus 4.7 --- internal/auth/bootstrap_test.go | 55 ++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/internal/auth/bootstrap_test.go b/internal/auth/bootstrap_test.go index 6bb7e8ce..21a37b82 100644 --- a/internal/auth/bootstrap_test.go +++ b/internal/auth/bootstrap_test.go @@ -38,19 +38,64 @@ func TestBootstrap_Integration(t *testing.T) { } t.Cleanup(pool.Close) - // Start from a known-empty users table. Other tests that insert users - // must run in isolation; a shared compose pg is fine for solo dev. + // Bootstrap requires an empty users table to test the first-run path. + // When the test DB is shared with the operator's dev environment, the + // TRUNCATE wipes their real admin login. Save any non-test users now and + // reinsert them after the test finishes so the operator can log back in. + type savedUser struct { + username, passwordHash, apiToken string + isAdmin, listenbrainzEnabled bool + subsonicPassword, listenbrainzToken *string + } + var saved []savedUser + rows, err := pool.Query(ctx, ` + SELECT username, password_hash, api_token, is_admin, + subsonic_password, listenbrainz_token, listenbrainz_enabled + FROM users + WHERE username NOT LIKE 'test-%'`) + if err != nil { + t.Fatalf("save users: %v", err) + } + for rows.Next() { + var s savedUser + if err := rows.Scan(&s.username, &s.passwordHash, &s.apiToken, &s.isAdmin, + &s.subsonicPassword, &s.listenbrainzToken, &s.listenbrainzEnabled); err != nil { + rows.Close() + t.Fatalf("scan: %v", err) + } + saved = append(saved, s) + } + rows.Close() + t.Cleanup(func() { + // Remove anything the test created, then restore non-test users. + if _, err := pool.Exec(context.Background(), "TRUNCATE users RESTART IDENTITY CASCADE"); err != nil { + t.Logf("cleanup truncate: %v", err) + return + } + for _, s := range saved { + if _, err := pool.Exec(context.Background(), ` + INSERT INTO users (username, password_hash, api_token, is_admin, + subsonic_password, listenbrainz_token, listenbrainz_enabled) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + s.username, s.passwordHash, s.apiToken, s.isAdmin, + s.subsonicPassword, s.listenbrainzToken, s.listenbrainzEnabled); err != nil { + t.Logf("restore %q: %v", s.username, err) + } + } + }) + if _, err := pool.Exec(ctx, "TRUNCATE users RESTART IDENTITY CASCADE"); err != nil { t.Fatalf("truncate: %v", err) } - cfg := config.AdminBootstrapConfig{Username: "admin", Password: "hunter2"} + const testAdmin = "test-admin" + cfg := config.AdminBootstrapConfig{Username: testAdmin, Password: "hunter2"} if err := Bootstrap(ctx, pool, cfg, logger); err != nil { t.Fatalf("Bootstrap: %v", err) } q := dbq.New(pool) - user, err := q.GetUserByUsername(ctx, "admin") + user, err := q.GetUserByUsername(ctx, testAdmin) if err != nil { t.Fatalf("GetUserByUsername: %v", err) } @@ -71,7 +116,7 @@ func TestBootstrap_Integration(t *testing.T) { if err != nil { t.Fatalf("GetUserByAPIToken: %v", err) } - if byToken.Username != "admin" { + if byToken.Username != testAdmin { t.Errorf("token lookup returned %q", byToken.Username) }