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) }