From 672b59bfd9f5641ee2c2842e36139a5463a6a02b Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Sun, 25 Oct 2026 02:52:27 +0000 Subject: [PATCH 1/3] fix(testhelpers): harden the shared test server for parallel callers A review of #1400 found three issues in shared.go that bite harder once tests on the shared server run in parallel: - The migrated template was built under the first caller's context and its result, errors included, is cached for the whole binary. A first caller that was canceled or hit its deadline mid-build failed every later RequireMigratedDatabase call. The build now runs detached from the caller, like the container start, with its own 10-minute backstop. - The Docker health probe ran on every call. Under CI load a probe that times out would skip a test whose database is reachable, a #1597-style false green. It now runs only until the shared server is up. - dropDatabase read s.admin without the mutex while TerminateSharedPostgres sets it to nil, so a cleanup after Terminate could race or panic. It now holds the mutex and treats a stopped server as nothing left to drop. Each fix has a test that fails without it. Co-Authored-By: Claude Opus 5.5 --- .../database/postgres/testhelpers/shared.go | 45 +++++++++++++- .../testhelpers/shared_integration_test.go | 60 +++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/internal/database/postgres/testhelpers/shared.go b/internal/database/postgres/testhelpers/shared.go index 41aabf957..99e374030 100644 --- a/internal/database/postgres/testhelpers/shared.go +++ b/internal/database/postgres/testhelpers/shared.go @@ -27,7 +27,8 @@ import ( // One mutex covers all of it. Holding it while the container starts and the // template migrates makes concurrent first callers wait for the one setup // instead of racing to do their own, and holding it across CREATE DATABASE -// keeps clones of one template from contending with each other. +// keeps clones of one template from contending with each other. Drops take it +// too, so TerminateSharedPostgres cannot pull the server out from under one. type sharedServer struct { mu sync.Mutex admin *PostgresContainer // connected to the maintenance database, never handed to a test @@ -86,7 +87,13 @@ func RequireEmptyDatabase(ctx context.Context, t *testing.T) *PostgresContainer func requireSharedDatabase(ctx context.Context, t *testing.T, migrationsPath string) *PostgresContainer { t.Helper() - testcontainers.SkipIfProviderIsNotHealthy(t) + // Probe Docker only until the shared server is up. Once it is, the server + // answering is the evidence that matters, and a probe that times out under + // load would otherwise skip a test whose database is reachable, the false + // green #1597 is about. + if !shared.running() { + providerProbe(t) + } name, cfg, err := shared.newDatabase(ctx, t, migrationsPath) require.NoError(t, err, @@ -103,6 +110,17 @@ func requireSharedDatabase(ctx context.Context, t *testing.T, migrationsPath str return &PostgresContainer{Config: cfg, DB: conn} } +// providerProbe skips t when Docker is unusable. A variable so a test can +// observe when it runs. +var providerProbe = testcontainers.SkipIfProviderIsNotHealthy + +// running reports whether the shared server is up. +func (s *sharedServer) running() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.admin != nil +} + // TerminateSharedPostgres stops the shared container if one was started. Call // it from TestMain after m.Run. Ryuk reaps the container if the process dies // first, so this is tidiness rather than the only line of defense. @@ -188,11 +206,20 @@ func (s *sharedServer) template(ctx context.Context, migrationsPath string) (str s.templates = map[string]templateDB{} } tmpl := templateDB{name: fmt.Sprintf("cudly_template_%d", len(s.templates)+1)} - tmpl.err = s.buildTemplate(ctx, tmpl.name, key) + // Detached from ctx, like the container in start: the outcome is cached for + // every later caller, so the first caller's cancellation or deadline must + // not be what decides it. templateBuildTimeout still bounds a hung build. + buildCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), templateBuildTimeout) + defer cancel() + tmpl.err = s.buildTemplate(buildCtx, tmpl.name, key) s.templates[key] = tmpl return tmpl.name, tmpl.err } +// templateBuildTimeout bounds one template build. The full chain takes seconds; +// this is a backstop against a hang, generous enough for a loaded CI runner. +const templateBuildTimeout = 10 * time.Minute + // buildTemplate creates name, applies every migration under migrationsPath to // it, and then closes it to connections. func (s *sharedServer) buildTemplate(ctx context.Context, name, migrationsPath string) error { @@ -219,7 +246,17 @@ func (s *sharedServer) buildTemplate(ctx context.Context, name, migrationsPath s // dropDatabase removes a per-test database. FORCE ends any connection the test // leaked, which would otherwise block the drop and leave the database behind. +// +// It holds s.mu so TerminateSharedPostgres cannot close the admin connection +// mid-drop. With no server left there is nothing to drop: terminating the +// container took every database on it along. func (s *sharedServer) dropDatabase(t *testing.T, name string) { + s.mu.Lock() + defer s.mu.Unlock() + if s.admin == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -229,12 +266,14 @@ func (s *sharedServer) dropDatabase(t *testing.T, name string) { } // configFor returns a copy of the shared container's config pointed at name. +// Callers hold s.mu. func (s *sharedServer) configFor(name string) *database.Config { cfg := *s.admin.Config cfg.Database = name return &cfg } +// exec runs sql on the admin connection. Callers hold s.mu. func (s *sharedServer) exec(ctx context.Context, sql string) error { _, err := s.admin.DB.Exec(ctx, sql) return err diff --git a/internal/database/postgres/testhelpers/shared_integration_test.go b/internal/database/postgres/testhelpers/shared_integration_test.go index 5b6a5a2dd..26734413f 100644 --- a/internal/database/postgres/testhelpers/shared_integration_test.go +++ b/internal/database/postgres/testhelpers/shared_integration_test.go @@ -192,6 +192,66 @@ func TestRequireMigratedDatabase_ConcurrentFirstUse(t *testing.T) { assert.Len(t, shared.templates, 1, "concurrent first callers must share one template") } +// TestRequireMigratedDatabase_CanceledFirstCallerDoesNotPoisonTemplate has the +// caller that triggers the template build arrive with its context already +// canceled. The build's outcome is cached for the whole binary, so it must not +// depend on that caller's context: later callers get a migrated database. +func TestRequireMigratedDatabase_CanceledFirstCallerDoesNotPoisonTemplate(t *testing.T) { + require.NoError(t, TerminateSharedPostgres(), "reset to a cold start, so this caller builds the template") + + canceled, cancel := context.WithCancel(context.Background()) + cancel() + // The caller's own CREATE DATABASE may fail on its dead context; that + // failure is the caller's alone and is not what this test is about. + if name, _, err := shared.newDatabase(canceled, t, sharedTestMigrationsPath()); err == nil { + t.Cleanup(func() { shared.dropDatabase(t, name) }) + } + + shared.mu.Lock() + tmpl, built := shared.templates[templateKey(sharedTestMigrationsPath())] + shared.mu.Unlock() + require.True(t, built, "the canceled first caller must still have built the template") + require.NoError(t, tmpl.err, "a canceled first caller must not cache a failed template") + + ctx, stop := context.WithTimeout(context.Background(), 2*time.Minute) + defer stop() + db := RequireMigratedDatabase(ctx, t, sharedTestMigrationsPath()) + requireFullyMigrated(ctx, t, db.DB, latestMigrationVersion(t)) +} + +// TestRequireEmptyDatabase_CleanupAfterTerminate stops the shared server while +// a test still holds a database on it, as a TestMain teardown racing a late +// cleanup would. The cleanup must find nothing to drop, not fail or panic. +func TestRequireEmptyDatabase_CleanupAfterTerminate(t *testing.T) { + passed := t.Run("terminates the server mid-test", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + db := RequireEmptyDatabase(ctx, t) + require.NoError(t, db.DB.Ping(ctx)) + require.NoError(t, TerminateSharedPostgres()) + }) + assert.True(t, passed, "cleanup after the shared server stopped must not fail the test") +} + +// TestRequireSharedDatabase_ProbesDockerOnlyUntilServerIsUp pins that the +// Docker probe runs on a cold start but not once the server is up. A probe +// that times out under load would skip a test whose database is reachable, +// a false green. +func TestRequireSharedDatabase_ProbesDockerOnlyUntilServerIsUp(t *testing.T) { + require.NoError(t, TerminateSharedPostgres(), "reset to a cold start") + probes := 0 + orig := providerProbe + providerProbe = func(t *testing.T) { probes++; orig(t) } + t.Cleanup(func() { providerProbe = orig }) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + RequireEmptyDatabase(ctx, t) + assert.Equal(t, 1, probes, "a cold start must probe Docker before starting the server") + RequireEmptyDatabase(ctx, t) + assert.Equal(t, 1, probes, "a running server must not be probed again") +} + // connString points cfg's server and credentials at dbName. func connString(cfg *database.Config, dbName string) string { u := url.URL{ From d281878d21d3d609b31b1a7e2bb97296bcc77ee9 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Sun, 25 Oct 2026 02:52:43 +0000 Subject: [PATCH 2/3] test(testhelpers): raise max_connections on the shared test server Tests on the shared server are about to run in parallel. Each running test can hold a pool of up to 10 connections plus the migrator's own and a spare session, about 13, and the admin pool adds up to 10. At the default -parallel (GOMAXPROCS, 16 on a dev machine) that worst case is over 200, past PostgreSQL's default max_connections of 100. Raising the server's limit to 500 is the least invasive fix: every test's pool keeps its size, so no test that holds several connections at once can start waiting on its own pool. Dedicated containers from SetupPostgresContainer are unchanged; only the shared server gets the flag, through an unexported variant that takes extra container options. Measured peak in the migrations package: 50 connections at -parallel=16, 21 at -parallel=4. Co-Authored-By: Claude Opus 5.5 --- .../database/postgres/testhelpers/postgres.go | 16 ++++++++++++---- internal/database/postgres/testhelpers/shared.go | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/database/postgres/testhelpers/postgres.go b/internal/database/postgres/testhelpers/postgres.go index 68ceb7d75..f655d39ad 100644 --- a/internal/database/postgres/testhelpers/postgres.go +++ b/internal/database/postgres/testhelpers/postgres.go @@ -39,18 +39,26 @@ type PostgresContainer struct { // would be noise rather than guidance. func SetupPostgresContainer(ctx context.Context, t *testing.T) (*PostgresContainer, error) { t.Helper() + return setupPostgresContainer(ctx, t) +} + +// setupPostgresContainer is SetupPostgresContainer with extra container +// options, applied after the defaults; the shared server uses it to raise +// max_connections. +func setupPostgresContainer(ctx context.Context, t *testing.T, extra ...testcontainers.ContainerCustomizer) (*PostgresContainer, error) { + t.Helper() // Create PostgreSQL container - postgresContainer, err := postgres.Run(ctx, - "postgres:16-alpine", + opts := append([]testcontainers.ContainerCustomizer{ postgres.WithDatabase("cudly_test"), postgres.WithUsername("cudly_test"), postgres.WithPassword("test_password"), testcontainers.WithWaitStrategy( wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). - WithStartupTimeout(30*time.Second)), - ) + WithStartupTimeout(30 * time.Second)), + }, extra...) + postgresContainer, err := postgres.Run(ctx, "postgres:16-alpine", opts...) if err != nil { return nil, fmt.Errorf("failed to start postgres container: %w", err) } diff --git a/internal/database/postgres/testhelpers/shared.go b/internal/database/postgres/testhelpers/shared.go index 99e374030..9f28cd8af 100644 --- a/internal/database/postgres/testhelpers/shared.go +++ b/internal/database/postgres/testhelpers/shared.go @@ -180,10 +180,23 @@ func (s *sharedServer) start(ctx context.Context, t *testing.T) error { } // Detached from ctx: the container outlives the test that happened to be // first, so that test's deadline or cancellation must not reach it. - s.admin, s.startErr = SetupPostgresContainer(context.WithoutCancel(ctx), t) + s.admin, s.startErr = setupPostgresContainer(context.WithoutCancel(ctx), t, + testcontainers.WithCmdArgs("-c", fmt.Sprintf("max_connections=%d", sharedMaxConnections))) return s.startErr } +// sharedMaxConnections is the shared server's max_connections, raised from +// PostgreSQL's default of 100 because tests on it may run in parallel +// (t.Parallel), up to -parallel (GOMAXPROCS by default) at once. Each running +// test can hold a pool of up to 10 connections (MaxConnections in +// SetupPostgresContainer) plus the migrator's own connection and a spare +// session or two, about 13 at worst, and the admin pool adds up to 10 more. +// 500 covers that worst case up to -parallel=36. Real use is far lower, since +// pools only open what they need: the migrations package peaked at 50 +// connections with -parallel=16 and 21 with -parallel=4. Raising the server limit instead of +// shrinking the per-database pool leaves every test's pool exactly as it was. +const sharedMaxConnections = 500 + // templateKey identifies the migrations directory a template was built from. // It is absolute so that "../database/postgres/migrations" and a // runtime.Caller-derived path to the same directory share one template instead From df25f2d603504a172e9c2eb95e6bbda0896dc969 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Sun, 25 Oct 2026 02:52:44 +0000 Subject: [PATCH 3/3] test(migrations): run the integration tests in parallel Since #1400 every integration test here gets its own database on the shared server, so most of them can run at once. The package was the slowest integration package on CI (190s), mostly tests walking the migration chain one after another. 62 of 69 top-level tests now call t.Parallel, and table subtests do too where each subtest creates its own database and the parent holds none. Subtests that share their parent's database stay sequential. Seven tests stay sequential, each with a one-line reason: - t.Setenv: TestMigrations_AutoHealDirty, and TestPGPasswordAndKeywordDSN_ConnectWithHostilePassword (which also keeps its dedicated server for ALTER ROLE) - swaps os.Stdout and the standard logger: TestAdminBootstrap_LogsToStderr_NotStdout - wall-clock deadline assertions: the four deadline and cancel tests in migrate_cancel_integration_test.go Advisory locks and pg_stat_activity checks in the parallel tests are scoped to the test's own database (advisory locks are per database; the pg_stat_activity queries filter by PID or current_database()), so other databases cannot affect them. Local wall time of the -race test binary: 252s sequentially on origin/main, 111s at -parallel=4, 81s at -parallel=16. Co-Authored-By: Claude Opus 5.5 --- .../000053_executions_account_fk_restrict_test.go | 2 ++ .../migrations/000057_drop_user_role_to_groups_test.go | 2 ++ .../migrations/000060_cleanup_universal_plans_test.go | 1 + .../migrations/000064_relocate_purchaser_group_test.go | 4 ++++ .../migrations/000065_enforce_min_one_admin_test.go | 8 ++++++++ .../000078_monthly_summary_nested_rollup_test.go | 1 + .../000086_remove_approve_own_from_standard_users_test.go | 3 +++ .../000087_purchase_history_marketplace_listing_test.go | 1 + .../000088_grant_view_config_to_nonadmin_groups_test.go | 6 ++++++ .../000089_rename_cancelled_to_canceled_test.go | 2 ++ .../000093_recommendations_state_owner_id_test.go | 1 + .../000095_purchase_history_account_id_width_test.go | 3 +++ .../migrations/000096_seed_ri_exchanger_group_test.go | 4 ++++ .../000098_backfill_execution_step_number_test.go | 1 + .../migrations/000099_user_deactivated_at_test.go | 1 + .../000100_hash_approval_tokens_at_rest_test.go | 2 ++ .../migrations/000102_purchase_plan_update_token_test.go | 1 + .../migrations/000103_api_key_password_version_test.go | 1 + .../migrations/000104_user_mfa_last_totp_counter_test.go | 1 + .../000105_purchase_history_listing_attempt_test.go | 1 + .../000106_invalidate_azure_recs_lookback_units_test.go | 1 + .../migrations/000107_ladder_tranche_amendments_test.go | 2 ++ .../000109_recommendations_availability_zone_test.go | 1 + .../000110_drop_recommendations_8col_natural_key_test.go | 1 + .../000111_recommendations_platform_tenancy_test.go | 1 + .../000112_plan_step_cap_auto_purchase_switch_test.go | 1 + .../000113_purchase_executions_no_submission_test.go | 1 + .../000114_ladder_tranches_status_reason_test.go | 1 + .../migrations/000115_auto_purchase_switch_stamps_test.go | 1 + .../migrations/000117_ladder_tranches_settled_at_test.go | 1 + .../000118_purchase_plans_ramp_baseline_test.go | 1 + .../000119_drop_recommendations_9col_natural_key_test.go | 1 + .../000120_recommendations_source_account_test.go | 1 + .../000121_ladder_tranche_amount_provenance_test.go | 1 + .../000123_purchase_execution_fan_out_shapes_test.go | 1 + .../migrations/000125_lock_default_user_groups_test.go | 1 + .../000126_purchase_history_active_expiry_idx_test.go | 1 + .../000128_executions_detach_keeps_updated_at_test.go | 1 + .../000129_recommendations_state_partial_test.go | 1 + .../000130_purchase_executions_created_by_system_test.go | 1 + .../000131_ri_exchange_direct_executions_test.go | 1 + .../000132_savings_partition_utc_bounds_test.go | 2 ++ ...3_purchase_history_reverify_ec2_offering_class_test.go | 2 ++ .../000134_purchase_suppressions_variant_test.go | 1 + .../postgres/migrations/backfill_admin_group_ids_test.go | 1 + .../postgres/migrations/ensure_admin_user_test.go | 6 ++++++ internal/database/postgres/migrations/helpers_test.go | 5 ++++- .../database/postgres/migrations/migrate_autoheal_test.go | 1 + .../migrations/migrate_cancel_integration_test.go | 5 +++++ .../database/postgres/migrations/migrate_down_all_test.go | 1 + .../postgres/migrations/migrate_idempotency_test.go | 1 + .../migrations/migrate_pgpassword_integration_test.go | 1 + .../migrations/migrate_security_integration_test.go | 1 + .../postgres/migrations/savings_snapshots_pk_test.go | 3 +++ .../postgres/migrations/split_savingsplans_test.go | 5 +++++ 55 files changed, 102 insertions(+), 1 deletion(-) diff --git a/internal/database/postgres/migrations/000053_executions_account_fk_restrict_test.go b/internal/database/postgres/migrations/000053_executions_account_fk_restrict_test.go index bb48a5d61..b0a3ff1ba 100644 --- a/internal/database/postgres/migrations/000053_executions_account_fk_restrict_test.go +++ b/internal/database/postgres/migrations/000053_executions_account_fk_restrict_test.go @@ -18,6 +18,7 @@ import ( // purchase_executions.cloud_account_id FK must be ON DELETE RESTRICT after // the migration runs. Issue #606 — silent-orphan regression guard. func TestMigration_ExecutionsAccountFKRestrict(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -99,6 +100,7 @@ func TestMigration_ExecutionsAccountFKRestrict(t *testing.T) { // emergency rollback re-introduces the (documented) silent-orphan // behavior rather than leaving the database in an indeterminate state. func TestMigration_ExecutionsAccountFKRestrict_Rollback(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000057_drop_user_role_to_groups_test.go b/internal/database/postgres/migrations/000057_drop_user_role_to_groups_test.go index 18ceee325..01827f9b7 100644 --- a/internal/database/postgres/migrations/000057_drop_user_role_to_groups_test.go +++ b/internal/database/postgres/migrations/000057_drop_user_role_to_groups_test.go @@ -39,6 +39,7 @@ func queryGroupIDs(t *testing.T, ctx context.Context, pool *pgxpool.Pool, email // no user loses access; guarantees no user can be left with zero groups; and // removes the role column from both users and sessions. func TestMigration_DropUserRoleToGroups(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -132,6 +133,7 @@ func assertColumnAbsent(t *testing.T, ctx context.Context, pool *pgxpool.Pool, t // TestMigration_DropUserRoleToGroups_Down restores the role column and // reconstructs role from group membership. func TestMigration_DropUserRoleToGroups_Down(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000060_cleanup_universal_plans_test.go b/internal/database/postgres/migrations/000060_cleanup_universal_plans_test.go index 75cb09e0d..1d936d8a9 100644 --- a/internal/database/postgres/migrations/000060_cleanup_universal_plans_test.go +++ b/internal/database/postgres/migrations/000060_cleanup_universal_plans_test.go @@ -33,6 +33,7 @@ import ( // The down migration is a no-op by design (deleted rows cannot be recovered // from SQL alone), so no rollback assertion is made. func TestMigration_CleanupUniversalPlans(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000064_relocate_purchaser_group_test.go b/internal/database/postgres/migrations/000064_relocate_purchaser_group_test.go index 19092d65e..cce44427b 100644 --- a/internal/database/postgres/migrations/000064_relocate_purchaser_group_test.go +++ b/internal/database/postgres/migrations/000064_relocate_purchaser_group_test.go @@ -45,10 +45,12 @@ func queryGroupIDsByEmail(t *testing.T, ctx context.Context, pool *pgxpool.Pool, // collision with Standard Users (000057), and must backfill all Administrators // group members into Purchaser. func TestMigration_RelocatePurchaserGroup(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() t.Run("Purchaser seeded at new UUID after full migration run", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireMigratedDatabase(ctx, t, migrationsPath) pool := container.DB.Pool() @@ -66,6 +68,7 @@ func TestMigration_RelocatePurchaserGroup(t *testing.T) { }) t.Run("admin-backfill: Administrators members land in Purchaser", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -98,6 +101,7 @@ func TestMigration_RelocatePurchaserGroup(t *testing.T) { }) t.Run("idempotent: re-running migrations does not duplicate Purchaser membership", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/000065_enforce_min_one_admin_test.go b/internal/database/postgres/migrations/000065_enforce_min_one_admin_test.go index 2cea63e68..43166bdc2 100644 --- a/internal/database/postgres/migrations/000065_enforce_min_one_admin_test.go +++ b/internal/database/postgres/migrations/000065_enforce_min_one_admin_test.go @@ -61,6 +61,7 @@ func countActiveAdmins(t *testing.T, ctx context.Context, pool *pgxpool.Pool) in // error string and would pass even if the SQL were wrong), this test runs the // actual migration SQL against a real PostgreSQL instance. func TestMigration_EnforceMinOneAdmin_ConcurrentRace(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -142,16 +143,19 @@ func TestMigration_EnforceMinOneAdmin_ConcurrentRace(t *testing.T) { } t.Run("concurrent deactivation never removes the last admin", func(t *testing.T) { + t.Parallel() commits, remaining := runConcurrentRace(t, `UPDATE users SET active = false WHERE id = $1`) assertRaceSafe(t, commits, remaining, "deactivations") }) t.Run("concurrent deletion never removes the last admin", func(t *testing.T) { + t.Parallel() commits, remaining := runConcurrentRace(t, `DELETE FROM users WHERE id = $1`) assertRaceSafe(t, commits, remaining, "deletions") }) t.Run("concurrent group removal never removes the last admin", func(t *testing.T) { + t.Parallel() commits, remaining := runConcurrentRace(t, `UPDATE users SET group_ids = '{}'::uuid[] WHERE id = $1`) assertRaceSafe(t, commits, remaining, "group removals") }) @@ -163,6 +167,7 @@ func TestMigration_EnforceMinOneAdmin_ConcurrentRace(t *testing.T) { // removal that leaves another active admin must succeed. This pins the // trigger's COUNT/active semantics against the real migration SQL. func TestMigration_EnforceMinOneAdmin_BlocksLastAdminRemoval(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -176,6 +181,7 @@ func TestMigration_EnforceMinOneAdmin_BlocksLastAdminRemoval(t *testing.T) { } t.Run("deactivating the sole admin is rejected", func(t *testing.T) { + t.Parallel() pool := freshPool(t) id := seedActiveAdmin(t, ctx, pool, "sole-admin@test.example") require.Equal(t, 1, countActiveAdmins(t, ctx, pool)) @@ -189,6 +195,7 @@ func TestMigration_EnforceMinOneAdmin_BlocksLastAdminRemoval(t *testing.T) { }) t.Run("deleting the sole admin is rejected", func(t *testing.T) { + t.Parallel() pool := freshPool(t) id := seedActiveAdmin(t, ctx, pool, "sole-admin-del@test.example") require.Equal(t, 1, countActiveAdmins(t, ctx, pool)) @@ -202,6 +209,7 @@ func TestMigration_EnforceMinOneAdmin_BlocksLastAdminRemoval(t *testing.T) { }) t.Run("removing one of two admins succeeds", func(t *testing.T) { + t.Parallel() pool := freshPool(t) a := seedActiveAdmin(t, ctx, pool, "two-admins-a@test.example") _ = seedActiveAdmin(t, ctx, pool, "two-admins-b@test.example") diff --git a/internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go b/internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go index 1fe4bce6a..c39e97dfc 100644 --- a/internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go +++ b/internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go @@ -32,6 +32,7 @@ import ( // shape): provider/service total = AVG(250, 400) = 325. The pre-fix flat AVG // returned 650/6 = 108.33. func TestAnalyticsNestedRollup_COR02(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() diff --git a/internal/database/postgres/migrations/000086_remove_approve_own_from_standard_users_test.go b/internal/database/postgres/migrations/000086_remove_approve_own_from_standard_users_test.go index ac24b3b17..ca4dbfb95 100644 --- a/internal/database/postgres/migrations/000086_remove_approve_own_from_standard_users_test.go +++ b/internal/database/postgres/migrations/000086_remove_approve_own_from_standard_users_test.go @@ -38,10 +38,12 @@ func standardUsersHasPurchaseVerb(t *testing.T, ctx context.Context, pool *pgxpo // virtue of ownership. The removal must be surgical -- the sibling own-scoped // verbs (cancel-own, retry-own) seeded alongside it in 000057 must be retained. func TestMigration_RemoveApproveOwnFromStandardUsers(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() t.Run("approve-own removed, sibling own-verbs retained", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -75,6 +77,7 @@ func TestMigration_RemoveApproveOwnFromStandardUsers(t *testing.T) { }) t.Run("down migration restores approve-own", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/000087_purchase_history_marketplace_listing_test.go b/internal/database/postgres/migrations/000087_purchase_history_marketplace_listing_test.go index 810a42cd6..4c354c4ba 100644 --- a/internal/database/postgres/migrations/000087_purchase_history_marketplace_listing_test.go +++ b/internal/database/postgres/migrations/000087_purchase_history_marketplace_listing_test.go @@ -23,6 +23,7 @@ import ( // 000087 applied) would fail because the columns do not exist; after 000087 the // test passes. func TestMigration087_MarketplaceColumns(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() diff --git a/internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go b/internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go index 438e39dcb..bb4f5db8d 100644 --- a/internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go +++ b/internal/database/postgres/migrations/000088_grant_view_config_to_nonadmin_groups_test.go @@ -38,10 +38,12 @@ func groupHasPermission(t *testing.T, ctx context.Context, pool *pgxpool.Pool, g // 403 for those groups. The grant must be surgical: no other permissions are // touched on either group, and the write-gating verb (update:config) is not added. func TestMigration_GrantViewConfigToNonAdminGroups(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() t.Run("view:config granted to Standard Users and Read-Only Users", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -66,6 +68,7 @@ func TestMigration_GrantViewConfigToNonAdminGroups(t *testing.T) { }) t.Run("update:config is NOT granted (write gate is unaffected)", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -79,6 +82,7 @@ func TestMigration_GrantViewConfigToNonAdminGroups(t *testing.T) { }) t.Run("sibling permissions on Standard Users are preserved", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -102,6 +106,7 @@ func TestMigration_GrantViewConfigToNonAdminGroups(t *testing.T) { }) t.Run("down migration removes view:config from both groups", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -126,6 +131,7 @@ func TestMigration_GrantViewConfigToNonAdminGroups(t *testing.T) { }) t.Run("idempotent: applying to a group already holding view:config is safe", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/000089_rename_cancelled_to_canceled_test.go b/internal/database/postgres/migrations/000089_rename_cancelled_to_canceled_test.go index fc7b8e9bf..9549e591b 100644 --- a/internal/database/postgres/migrations/000089_rename_cancelled_to_canceled_test.go +++ b/internal/database/postgres/migrations/000089_rename_cancelled_to_canceled_test.go @@ -47,6 +47,7 @@ const ( // //nolint:misspell // test body deliberately exercises the legacy British DB spelling under test -- see migration 000089 func TestMigration_000089_RollbackWithCanceledRows(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -164,6 +165,7 @@ func TestMigration_000089_RollbackWithCanceledRows(t *testing.T) { // //nolint:misspell // test body deliberately exercises the legacy British DB spelling under test -- see migration 000089 func TestMigration_000089_UpIsAdditiveAndDualCompatible(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000093_recommendations_state_owner_id_test.go b/internal/database/postgres/migrations/000093_recommendations_state_owner_id_test.go index 38384d9ca..8b1909137 100644 --- a/internal/database/postgres/migrations/000093_recommendations_state_owner_id_test.go +++ b/internal/database/postgres/migrations/000093_recommendations_state_owner_id_test.go @@ -18,6 +18,7 @@ import ( // column (issue #261's compare-and-clear guard), and the down migration // removes it cleanly. func TestMigration_RecommendationsStateOwnerID(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000095_purchase_history_account_id_width_test.go b/internal/database/postgres/migrations/000095_purchase_history_account_id_width_test.go index 86739355f..379961389 100644 --- a/internal/database/postgres/migrations/000095_purchase_history_account_id_width_test.go +++ b/internal/database/postgres/migrations/000095_purchase_history_account_id_width_test.go @@ -106,6 +106,7 @@ func accountIDMaxLength(ctx context.Context, t *testing.T, pool *pgxpool.Pool) i // 22001, which is precisely how the audit row for an already-billed commitment // is lost. After 000095 both succeed and round-trip untruncated. func TestMigration095_PurchaseHistoryAccountIDWidth(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() @@ -194,6 +195,7 @@ func TestMigration095_PurchaseHistoryAccountIDWidth(t *testing.T) { // must actually happen. Without this the ALTER branch after the guard is never // executed by any test and a typo in it would ship green. func TestMigration095_DownNarrowsWhenSafe(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() @@ -229,6 +231,7 @@ func TestMigration095_DownNarrowsWhenSafe(t *testing.T) { // VARCHAR(20) with an Azure subscription GUID present would destroy the audit // trail this table exists to provide, so the down migration must refuse. func TestMigration095_DownRefusesToTruncate(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() diff --git a/internal/database/postgres/migrations/000096_seed_ri_exchanger_group_test.go b/internal/database/postgres/migrations/000096_seed_ri_exchanger_group_test.go index dd2ac81db..f2c31f0ee 100644 --- a/internal/database/postgres/migrations/000096_seed_ri_exchanger_group_test.go +++ b/internal/database/postgres/migrations/000096_seed_ri_exchanger_group_test.go @@ -31,10 +31,12 @@ const riExchangerGroupIDTest = "00000000-0000-5000-8000-000000000008" // (#942), by running ON CONFLICT DO NOTHING onto an already-occupied UUID. // These assertions exist so that failure is loud. func TestMigration_SeedRIExchangerGroup(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() t.Run("RI Exchanger seeded, system-managed, granting execute:ri-exchange", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireMigratedDatabase(ctx, t, migrationsPath) pool := container.DB.Pool() @@ -70,6 +72,7 @@ func TestMigration_SeedRIExchangerGroup(t *testing.T) { }) t.Run("admin-backfill: Administrators members land in RI Exchanger", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -98,6 +101,7 @@ func TestMigration_SeedRIExchangerGroup(t *testing.T) { }) t.Run("backfill is idempotent and leaves no duplicate entry", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireMigratedDatabase(ctx, t, migrationsPath) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/000098_backfill_execution_step_number_test.go b/internal/database/postgres/migrations/000098_backfill_execution_step_number_test.go index 5d0f41b43..400bbac82 100644 --- a/internal/database/postgres/migrations/000098_backfill_execution_step_number_test.go +++ b/internal/database/postgres/migrations/000098_backfill_execution_step_number_test.go @@ -57,6 +57,7 @@ func stepNumberOf(ctx context.Context, t *testing.T, pool *pgxpool.Pool, execID // steps) are retargeted at the step they will actually complete, while // correctly-stamped rows and terminal rows are left alone. func TestMigration_BackfillExecutionStepNumber(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000099_user_deactivated_at_test.go b/internal/database/postgres/migrations/000099_user_deactivated_at_test.go index 98f3f182d..aa04a9d6c 100644 --- a/internal/database/postgres/migrations/000099_user_deactivated_at_test.go +++ b/internal/database/postgres/migrations/000099_user_deactivated_at_test.go @@ -51,6 +51,7 @@ func deactivatedAtOf(ctx context.Context, t *testing.T, pool *pgxpool.Pool, user // could self-reactivate via password reset regardless of how it went // inactive. func TestMigration_UserDeactivatedAtBackfill(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000100_hash_approval_tokens_at_rest_test.go b/internal/database/postgres/migrations/000100_hash_approval_tokens_at_rest_test.go index 1f357493d..e21e80d7b 100644 --- a/internal/database/postgres/migrations/000100_hash_approval_tokens_at_rest_test.go +++ b/internal/database/postgres/migrations/000100_hash_approval_tokens_at_rest_test.go @@ -112,6 +112,7 @@ func runUpSQL(ctx context.Context, t *testing.T, pool *pgxpool.Pool, migrationsP // destroying migrated hashes; and the no-op down keeps the hashes so a // down-then-up is lossless. func TestMigration_HashApprovalTokensAtRest(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -184,6 +185,7 @@ func TestMigration_HashApprovalTokensAtRest(t *testing.T) { // or execution history, pending lists and the scheduler break. A row the new // store writes must also stay readable by pre-#103 code. func TestMigration_HashApprovalTokensAtRest_StoreReadsTokenlessRows(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000102_purchase_plan_update_token_test.go b/internal/database/postgres/migrations/000102_purchase_plan_update_token_test.go index 2959b58dc..04c8075fe 100644 --- a/internal/database/postgres/migrations/000102_purchase_plan_update_token_test.go +++ b/internal/database/postgres/migrations/000102_purchase_plan_update_token_test.go @@ -13,6 +13,7 @@ import ( ) func TestMigration_PurchasePlanUpdateToken(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireMigratedDatabase(ctx, t, path) diff --git a/internal/database/postgres/migrations/000103_api_key_password_version_test.go b/internal/database/postgres/migrations/000103_api_key_password_version_test.go index 321688272..8197e48fe 100644 --- a/internal/database/postgres/migrations/000103_api_key_password_version_test.go +++ b/internal/database/postgres/migrations/000103_api_key_password_version_test.go @@ -12,6 +12,7 @@ import ( ) func TestMigration_APIKeyPasswordVersion(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireMigratedDatabase(ctx, t, path) diff --git a/internal/database/postgres/migrations/000104_user_mfa_last_totp_counter_test.go b/internal/database/postgres/migrations/000104_user_mfa_last_totp_counter_test.go index 0383a9392..59c9bb8fc 100644 --- a/internal/database/postgres/migrations/000104_user_mfa_last_totp_counter_test.go +++ b/internal/database/postgres/migrations/000104_user_mfa_last_totp_counter_test.go @@ -15,6 +15,7 @@ import ( // Migration 000104 adds users.mfa_last_totp_counter. Existing rows start at 0 // (no code accepted yet), and the down migration removes the column. func TestMigration_UserMFALastTOTPCounter(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000105_purchase_history_listing_attempt_test.go b/internal/database/postgres/migrations/000105_purchase_history_listing_attempt_test.go index bb66bcb9e..656523e5b 100644 --- a/internal/database/postgres/migrations/000105_purchase_history_listing_attempt_test.go +++ b/internal/database/postgres/migrations/000105_purchase_history_listing_attempt_test.go @@ -17,6 +17,7 @@ import ( // purchase_history: existing rows have no unresolved attempt, and the down // migration removes both columns. func TestMigration_PurchaseHistoryListingAttempt(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000106_invalidate_azure_recs_lookback_units_test.go b/internal/database/postgres/migrations/000106_invalidate_azure_recs_lookback_units_test.go index 43b13c608..e61f50c99 100644 --- a/internal/database/postgres/migrations/000106_invalidate_azure_recs_lookback_units_test.go +++ b/internal/database/postgres/migrations/000106_invalidate_azure_recs_lookback_units_test.go @@ -16,6 +16,7 @@ import ( ) func TestMigration_InvalidateAzureRecsLookbackUnits(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireEmptyDatabase(ctx, t) diff --git a/internal/database/postgres/migrations/000107_ladder_tranche_amendments_test.go b/internal/database/postgres/migrations/000107_ladder_tranche_amendments_test.go index c47a927b2..94f1c181a 100644 --- a/internal/database/postgres/migrations/000107_ladder_tranche_amendments_test.go +++ b/internal/database/postgres/migrations/000107_ladder_tranche_amendments_test.go @@ -13,6 +13,7 @@ import ( ) func TestMigration_LadderTrancheAmendments(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireEmptyDatabase(ctx, t) @@ -87,6 +88,7 @@ func TestMigration_LadderTrancheAmendments(t *testing.T) { } func TestMigration_LadderTrancheAmendmentsRollbackWhenEmpty(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireEmptyDatabase(ctx, t) diff --git a/internal/database/postgres/migrations/000109_recommendations_availability_zone_test.go b/internal/database/postgres/migrations/000109_recommendations_availability_zone_test.go index f9b780c3c..b7436524e 100644 --- a/internal/database/postgres/migrations/000109_recommendations_availability_zone_test.go +++ b/internal/database/postgres/migrations/000109_recommendations_availability_zone_test.go @@ -26,6 +26,7 @@ const insertRecForMigration109 = ` // both payload shapes, the 8-column index is kept so the current upsert still // works, and rollback removes the column and the new index. func TestMigration_RecommendationsAvailabilityZone(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000110_drop_recommendations_8col_natural_key_test.go b/internal/database/postgres/migrations/000110_drop_recommendations_8col_natural_key_test.go index a0e8ed062..66bfe28b0 100644 --- a/internal/database/postgres/migrations/000110_drop_recommendations_8col_natural_key_test.go +++ b/internal/database/postgres/migrations/000110_drop_recommendations_8col_natural_key_test.go @@ -25,6 +25,7 @@ const insertRecForMigration110 = ` // the 8-column index is gone, the 9-column index still dedupes per AZ, and the // rollback recreates the 8-column index after collapsing AZ-only duplicates. func TestMigration_DropRecommendations8ColNaturalKey(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000111_recommendations_platform_tenancy_test.go b/internal/database/postgres/migrations/000111_recommendations_platform_tenancy_test.go index 940660641..acfbf1cf0 100644 --- a/internal/database/postgres/migrations/000111_recommendations_platform_tenancy_test.go +++ b/internal/database/postgres/migrations/000111_recommendations_platform_tenancy_test.go @@ -27,6 +27,7 @@ const insertRecForMigration111 = ` // upsert still works, the 11-column index is added, and rollback removes only // the new column pair and index. func TestMigration_RecommendationsPlatformTenancy(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000112_plan_step_cap_auto_purchase_switch_test.go b/internal/database/postgres/migrations/000112_plan_step_cap_auto_purchase_switch_test.go index 9e5706c2e..edb406952 100644 --- a/internal/database/postgres/migrations/000112_plan_step_cap_auto_purchase_switch_test.go +++ b/internal/database/postgres/migrations/000112_plan_step_cap_auto_purchase_switch_test.go @@ -19,6 +19,7 @@ import ( // switch defaults to FALSE, the CHECK rejects a non-positive cap, and the down // migration removes both columns. func TestMigration_PlanStepCapAndAutoPurchaseSwitch(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000113_purchase_executions_no_submission_test.go b/internal/database/postgres/migrations/000113_purchase_executions_no_submission_test.go index 4f0a8ac02..88245fa88 100644 --- a/internal/database/postgres/migrations/000113_purchase_executions_no_submission_test.go +++ b/internal/database/postgres/migrations/000113_purchase_executions_no_submission_test.go @@ -20,6 +20,7 @@ import ( // (no proof, fail closed), the up statement is idempotent, and the down // migration drops the column. func TestMigration_PurchaseExecutionsNoSubmission(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000114_ladder_tranches_status_reason_test.go b/internal/database/postgres/migrations/000114_ladder_tranches_status_reason_test.go index d3909616c..b74d833ad 100644 --- a/internal/database/postgres/migrations/000114_ladder_tranches_status_reason_test.go +++ b/internal/database/postgres/migrations/000114_ladder_tranches_status_reason_test.go @@ -19,6 +19,7 @@ import ( // PostgreSQL: a pre-existing tranche gets a NULL reason, the up statement is // idempotent, and the down migration drops the column. func TestMigration_LadderTranchesStatusReason(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000115_auto_purchase_switch_stamps_test.go b/internal/database/postgres/migrations/000115_auto_purchase_switch_stamps_test.go index e505f769c..7b8ee00ae 100644 --- a/internal/database/postgres/migrations/000115_auto_purchase_switch_stamps_test.go +++ b/internal/database/postgres/migrations/000115_auto_purchase_switch_stamps_test.go @@ -20,6 +20,7 @@ import ( // NULL stamps, the up statement is idempotent, and the down migration drops // the three columns. func TestMigration_AutoPurchaseSwitchStamps(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000117_ladder_tranches_settled_at_test.go b/internal/database/postgres/migrations/000117_ladder_tranches_settled_at_test.go index 1987d43a1..49c1cdb3b 100644 --- a/internal/database/postgres/migrations/000117_ladder_tranches_settled_at_test.go +++ b/internal/database/postgres/migrations/000117_ladder_tranches_settled_at_test.go @@ -22,6 +22,7 @@ import ( // row still works (the ON DELETE SET NULL updates re-evaluate the CHECK), and // the up statement is idempotent. func TestMigration_LadderTranchesSettledAt(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireEmptyDatabase(ctx, t) diff --git a/internal/database/postgres/migrations/000118_purchase_plans_ramp_baseline_test.go b/internal/database/postgres/migrations/000118_purchase_plans_ramp_baseline_test.go index 87fae5bd5..b84ef7a53 100644 --- a/internal/database/postgres/migrations/000118_purchase_plans_ramp_baseline_test.go +++ b/internal/database/postgres/migrations/000118_purchase_plans_ramp_baseline_test.go @@ -19,6 +19,7 @@ import ( // PostgreSQL: existing plans get a NULL baseline (no backfill), the up // statement is idempotent, and down removes the column. func TestMigration_PurchasePlansRampBaseline(t *testing.T) { + t.Parallel() ctx := context.Background() path := getMigrationsPath() container := testhelpers.RequireEmptyDatabase(ctx, t) diff --git a/internal/database/postgres/migrations/000119_drop_recommendations_9col_natural_key_test.go b/internal/database/postgres/migrations/000119_drop_recommendations_9col_natural_key_test.go index 3fceb3aab..c10183fcd 100644 --- a/internal/database/postgres/migrations/000119_drop_recommendations_9col_natural_key_test.go +++ b/internal/database/postgres/migrations/000119_drop_recommendations_9col_natural_key_test.go @@ -25,6 +25,7 @@ const insertRecForMigration119 = ` // the 9-column index is gone, the 11-column index still dedupes, and the // rollback recreates the 9-column index after collapsing those duplicates. func TestMigration_DropRecommendations9ColNaturalKey(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000120_recommendations_source_account_test.go b/internal/database/postgres/migrations/000120_recommendations_source_account_test.go index e2cd6dbd3..91aa9ca5f 100644 --- a/internal/database/postgres/migrations/000120_recommendations_source_account_test.go +++ b/internal/database/postgres/migrations/000120_recommendations_source_account_test.go @@ -16,6 +16,7 @@ import ( ) func TestMigration_RecommendationsSourceAccount(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/000121_ladder_tranche_amount_provenance_test.go b/internal/database/postgres/migrations/000121_ladder_tranche_amount_provenance_test.go index e934aea13..c933c5a34 100644 --- a/internal/database/postgres/migrations/000121_ladder_tranche_amount_provenance_test.go +++ b/internal/database/postgres/migrations/000121_ladder_tranche_amount_provenance_test.go @@ -12,6 +12,7 @@ import ( ) func TestMigration_LadderTrancheAmountProvenance(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() diff --git a/internal/database/postgres/migrations/000123_purchase_execution_fan_out_shapes_test.go b/internal/database/postgres/migrations/000123_purchase_execution_fan_out_shapes_test.go index 966d315ad..cd69d9770 100644 --- a/internal/database/postgres/migrations/000123_purchase_execution_fan_out_shapes_test.go +++ b/internal/database/postgres/migrations/000123_purchase_execution_fan_out_shapes_test.go @@ -21,6 +21,7 @@ import ( // "multi", a lineage without one is left unpinned, the up statement is // idempotent, and the down migration drops the table. func TestMigration_PurchaseExecutionFanOutShapes(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go b/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go index 4e7152bc2..bd4fdd79e 100644 --- a/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go +++ b/internal/database/postgres/migrations/000125_lock_default_user_groups_test.go @@ -12,6 +12,7 @@ import ( ) func TestMigration_LockDefaultUserGroups(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() diff --git a/internal/database/postgres/migrations/000126_purchase_history_active_expiry_idx_test.go b/internal/database/postgres/migrations/000126_purchase_history_active_expiry_idx_test.go index d5d6e3b68..ab465bc3c 100644 --- a/internal/database/postgres/migrations/000126_purchase_history_active_expiry_idx_test.go +++ b/internal/database/postgres/migrations/000126_purchase_history_active_expiry_idx_test.go @@ -16,6 +16,7 @@ import ( // value still inserts once the index exists, and that the down migration // removes it. func TestMigration_PurchaseHistoryActiveExpiryIndex(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() diff --git a/internal/database/postgres/migrations/000128_executions_detach_keeps_updated_at_test.go b/internal/database/postgres/migrations/000128_executions_detach_keeps_updated_at_test.go index 01dbb23f5..e6c3d7730 100644 --- a/internal/database/postgres/migrations/000128_executions_detach_keeps_updated_at_test.go +++ b/internal/database/postgres/migrations/000128_executions_detach_keeps_updated_at_test.go @@ -18,6 +18,7 @@ import ( // account keeps updated_at, every other UPDATE still stamps NOW(), and the // down migration restores the stamp-everything trigger (issue #265). func TestMigration_ExecutionsDetachKeepsUpdatedAt(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() diff --git a/internal/database/postgres/migrations/000129_recommendations_state_partial_test.go b/internal/database/postgres/migrations/000129_recommendations_state_partial_test.go index fecb4f5d2..c8e77ae6a 100644 --- a/internal/database/postgres/migrations/000129_recommendations_state_partial_test.go +++ b/internal/database/postgres/migrations/000129_recommendations_state_partial_test.go @@ -19,6 +19,7 @@ import ( // the logs), the seeded singleton row starts out NULL (= complete), and the // down migration removes the column cleanly. func TestMigration_RecommendationsStatePartial(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000130_purchase_executions_created_by_system_test.go b/internal/database/postgres/migrations/000130_purchase_executions_created_by_system_test.go index ad8e5f1d5..ca20cbc6e 100644 --- a/internal/database/postgres/migrations/000130_purchase_executions_created_by_system_test.go +++ b/internal/database/postgres/migrations/000130_purchase_executions_created_by_system_test.go @@ -13,6 +13,7 @@ import ( // TestMigration_PurchaseExecutionsCreatedBySystem covers 000130 up, down and the CHECK (#233). func TestMigration_PurchaseExecutionsCreatedBySystem(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() diff --git a/internal/database/postgres/migrations/000131_ri_exchange_direct_executions_test.go b/internal/database/postgres/migrations/000131_ri_exchange_direct_executions_test.go index a5c7fe093..f787bdc3e 100644 --- a/internal/database/postgres/migrations/000131_ri_exchange_direct_executions_test.go +++ b/internal/database/postgres/migrations/000131_ri_exchange_direct_executions_test.go @@ -21,6 +21,7 @@ import ( // idempotent, the approval flow's ri_exchange_history is untouched, and the // down migration drops the table. func TestMigration_RIExchangeDirectExecutions(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/000132_savings_partition_utc_bounds_test.go b/internal/database/postgres/migrations/000132_savings_partition_utc_bounds_test.go index fbc11fbf7..176e0b628 100644 --- a/internal/database/postgres/migrations/000132_savings_partition_utc_bounds_test.go +++ b/internal/database/postgres/migrations/000132_savings_partition_utc_bounds_test.go @@ -114,6 +114,7 @@ func utc(s string) time.Time { // created under any session TimeZone span UTC months, and partitions that the // old function cut at local midnight are rebuilt with their rows re-routed. func TestMigration_SavingsPartitionUTCBounds(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() @@ -206,6 +207,7 @@ func TestMigration_SavingsPartitionUTCBounds(t *testing.T) { // not committed, waits and then succeeds instead of failing with "relation // already exists". func TestMigration_SavingsPartitionConcurrentCreate(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/000133_purchase_history_reverify_ec2_offering_class_test.go b/internal/database/postgres/migrations/000133_purchase_history_reverify_ec2_offering_class_test.go index 0e6eadd7a..38f3cbf3a 100644 --- a/internal/database/postgres/migrations/000133_purchase_history_reverify_ec2_offering_class_test.go +++ b/internal/database/postgres/migrations/000133_purchase_history_reverify_ec2_offering_class_test.go @@ -49,6 +49,7 @@ func offeringClassOf(ctx context.Context, t *testing.T, pool *pgxpool.Pool, purc // Fail-before: with the UPDATE removed from the up file, the cleared rows keep // 'convertible' and the NULL assertions fail. func TestMigration133_ClearsConvertibleEC2Stamps(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() @@ -100,6 +101,7 @@ func TestMigration133_ClearsConvertibleEC2Stamps(t *testing.T) { // commits, the UPDATE re-checks its WHERE clause on the new row version and // must leave the authoritative 'standard' in place rather than clearing it. func TestMigration133_ConcurrentStandardStampWins(t *testing.T) { + t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() diff --git a/internal/database/postgres/migrations/000134_purchase_suppressions_variant_test.go b/internal/database/postgres/migrations/000134_purchase_suppressions_variant_test.go index 314c48ac4..1eb558d38 100644 --- a/internal/database/postgres/migrations/000134_purchase_suppressions_variant_test.go +++ b/internal/database/postgres/migrations/000134_purchase_suppressions_variant_test.go @@ -45,6 +45,7 @@ func requireUniqueViolation134(t *testing.T, err error, constraint string) { // execution and 6-tuple), old-code inserts still working, and a down that // merges variant rows back into one row per execution and 6-tuple. func TestMigration_PurchaseSuppressionsVariant(t *testing.T) { + t.Parallel() ctx := context.Background() container := testhelpers.RequireEmptyDatabase(ctx, t) pool, path := container.DB.Pool(), getMigrationsPath() diff --git a/internal/database/postgres/migrations/backfill_admin_group_ids_test.go b/internal/database/postgres/migrations/backfill_admin_group_ids_test.go index 8408a2015..de86ad42e 100644 --- a/internal/database/postgres/migrations/backfill_admin_group_ids_test.go +++ b/internal/database/postgres/migrations/backfill_admin_group_ids_test.go @@ -24,6 +24,7 @@ import ( // re-run migrations with NO admin email so only the SQL migration can repair // the row. A pass therefore proves the migration (not the Go path) did it. func TestMigration_BackfillAdminGroupIDs(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() const adminEmail = "restore-path@test.example" diff --git a/internal/database/postgres/migrations/ensure_admin_user_test.go b/internal/database/postgres/migrations/ensure_admin_user_test.go index 19af8105c..fbcbf57b5 100644 --- a/internal/database/postgres/migrations/ensure_admin_user_test.go +++ b/internal/database/postgres/migrations/ensure_admin_user_test.go @@ -42,11 +42,13 @@ func queryAdminGroupIDs(t *testing.T, ctx context.Context, pool *pgxpool.Pool, e // drift must self-heal on the next boot, and operator-customized // group_ids must be preserved. func TestEnsureAdminUser_GroupAssignment(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() const adminEmail = "admin@test.example" t.Run("fresh insert no password", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -58,6 +60,7 @@ func TestEnsureAdminUser_GroupAssignment(t *testing.T) { }) t.Run("fresh insert with password", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -69,6 +72,7 @@ func TestEnsureAdminUser_GroupAssignment(t *testing.T) { }) t.Run("re-run with existing correct admin (post-057 idempotency)", func(t *testing.T) { + t.Parallel() // Migration 000057 added a CHECK constraint (users_min_one_group) // that prevents group_ids from being NULL or empty, so the // pre-057 drift scenario (empty group_ids on an existing admin row) @@ -97,6 +101,7 @@ func TestEnsureAdminUser_GroupAssignment(t *testing.T) { }) t.Run("idempotency", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -113,6 +118,7 @@ func TestEnsureAdminUser_GroupAssignment(t *testing.T) { }) t.Run("operator customisation preserved", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/helpers_test.go b/internal/database/postgres/migrations/helpers_test.go index 3f73842fe..ec63b6af2 100644 --- a/internal/database/postgres/migrations/helpers_test.go +++ b/internal/database/postgres/migrations/helpers_test.go @@ -69,6 +69,7 @@ func previousMigrationVersion(t *testing.T, version uint) uint { // captureStdout redirects os.Stdout to a pipe and returns a function that // closes the pipe, restores stdout, and returns everything written to it. +// os.Stdout is process-wide, so a test using it must not call t.Parallel. // // Mirrors the helper of the same name in migrate_security_test.go; the // duplication is forced by the package boundary (that file lives in @@ -93,7 +94,9 @@ func captureStdout(t *testing.T) func() string { } // captureLogOutput redirects the standard logger to a buffer for the duration -// of the test, restoring the original flags and writer on cleanup. +// of the test, restoring the original flags and writer on cleanup. Every +// RunMigrations call logs through that logger, so a test using it must not +// call t.Parallel. // // Mirrors the helper of the same name in migrate_security_test.go for the // same package-boundary reason described on captureStdout. diff --git a/internal/database/postgres/migrations/migrate_autoheal_test.go b/internal/database/postgres/migrations/migrate_autoheal_test.go index bb4df2e09..35e75f2d9 100644 --- a/internal/database/postgres/migrations/migrate_autoheal_test.go +++ b/internal/database/postgres/migrations/migrate_autoheal_test.go @@ -47,6 +47,7 @@ func markDirty(ctx context.Context, t *testing.T, container *testhelpers.Postgre // conservative choice is to refuse to auto-clear and let an operator confirm // the schema state before forcing. func TestMigrations_AutoHealDirty(t *testing.T) { + // Not parallel: the subtests t.Setenv CUDLY_MIGRATION_AUTOHEAL, which RunMigrations reads process-wide. ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/migrate_cancel_integration_test.go b/internal/database/postgres/migrations/migrate_cancel_integration_test.go index c6d9c6da0..6d0dd266b 100644 --- a/internal/database/postgres/migrations/migrate_cancel_integration_test.go +++ b/internal/database/postgres/migrations/migrate_cancel_integration_test.go @@ -100,6 +100,7 @@ func requireNoBackend(t *testing.T, pool *pgxpool.Pool, db, prefix string) { } func TestRunMigrations_DeadlineStopsMigrationSQLBlockedByForeignLock(t *testing.T) { + // Not parallel: asserts a 1s deadline is honored within fixed wall-clock bounds. pool, db := migrationDB(t) ctx := context.Background() _, err := pool.Exec(ctx, "CREATE TABLE locktarget (id int)") @@ -137,6 +138,7 @@ func TestRunMigrations_DeadlineStopsMigrationSQLBlockedByForeignLock(t *testing. } func TestRunMigrations_DeadlineStopsAdvisoryLockWait(t *testing.T) { + // Not parallel: asserts a 1s deadline is honored within a fixed wall-clock bound. pool, db := migrationDB(t) ctx := context.Background() @@ -160,6 +162,7 @@ func TestRunMigrations_DeadlineStopsAdvisoryLockWait(t *testing.T) { } func TestRunMigrations_CancelStopsSetupBlockedOnVersionTable(t *testing.T) { + // Not parallel: asserts a 1s deadline is honored within a fixed wall-clock bound. pool, db := migrationDB(t) ctx := context.Background() _, err := pool.Exec(ctx, "CREATE TABLE schema_migrations (version bigint NOT NULL PRIMARY KEY, dirty boolean NOT NULL)") @@ -181,6 +184,7 @@ func TestRunMigrations_CancelStopsSetupBlockedOnVersionTable(t *testing.T) { } func TestRunMigrations_CompletesWithinDeadline(t *testing.T) { + // Not parallel: asserts the run finishes inside a fixed wall-clock deadline. pool, _ := migrationDB(t) dir := migrationDir(t, "CREATE TABLE widgets (id int);") @@ -198,6 +202,7 @@ func TestRunMigrations_CompletesWithinDeadline(t *testing.T) { // is a clean no-op, so the replacement for migrate.New changes nothing for the // normal path. func TestRunMigrations_AppliesFullSchemaThroughCancelableMigrator(t *testing.T) { + t.Parallel() pool, _ := migrationDB(t) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() diff --git a/internal/database/postgres/migrations/migrate_down_all_test.go b/internal/database/postgres/migrations/migrate_down_all_test.go index c47de6cec..96fece35b 100644 --- a/internal/database/postgres/migrations/migrate_down_all_test.go +++ b/internal/database/postgres/migrations/migrate_down_all_test.go @@ -18,6 +18,7 @@ import ( // its own up against the schema the later migrations left behind; a down that // assumes an object a later migration removed leaves the DB dirty (issue #205). func TestMigrations_DownAllRoundTrip(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/migrate_idempotency_test.go b/internal/database/postgres/migrations/migrate_idempotency_test.go index 9aa74e2e8..31ff34b91 100644 --- a/internal/database/postgres/migrations/migrate_idempotency_test.go +++ b/internal/database/postgres/migrations/migrate_idempotency_test.go @@ -31,6 +31,7 @@ import ( // CUDLY_FORCE_MIGRATION_VERSION. Per-migration re-run safety is therefore not // an invariant that auto-heal depends on, and is tracked separately. func TestMigrations_FullStackIdempotent(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() diff --git a/internal/database/postgres/migrations/migrate_pgpassword_integration_test.go b/internal/database/postgres/migrations/migrate_pgpassword_integration_test.go index 9318eca1d..57f225973 100644 --- a/internal/database/postgres/migrations/migrate_pgpassword_integration_test.go +++ b/internal/database/postgres/migrations/migrate_pgpassword_integration_test.go @@ -44,6 +44,7 @@ func hostilePasswords() map[string]string { // // A wrong PGPASSWORD must fail, proving the server really checks it. func TestPGPasswordAndKeywordDSN_ConnectWithHostilePassword(t *testing.T) { + // Not parallel: t.Setenv("PGPASSWORD") changes the process environment. ctx := context.Background() // Dedicated server: ALTER ROLE ... PASSWORD changes the cluster-wide role that // every shared-server test database logs in as. diff --git a/internal/database/postgres/migrations/migrate_security_integration_test.go b/internal/database/postgres/migrations/migrate_security_integration_test.go index d1abdf4f9..6e5d632d5 100644 --- a/internal/database/postgres/migrations/migrate_security_integration_test.go +++ b/internal/database/postgres/migrations/migrate_security_integration_test.go @@ -25,6 +25,7 @@ import ( // This test therefore exercises the "Admin user created" log path, which // is emitted on the first cold-start bootstrap and must reach stderr. func TestAdminBootstrap_LogsToStderr_NotStdout(t *testing.T) { + // Not parallel: it swaps os.Stdout and the standard logger, which other tests write to. ctx := context.Background() migrationsPath := getMigrationsPath() const adminEmail = "stderr-bootstrap@test.example" diff --git a/internal/database/postgres/migrations/savings_snapshots_pk_test.go b/internal/database/postgres/migrations/savings_snapshots_pk_test.go index ab6f2d15d..fd5a12dd2 100644 --- a/internal/database/postgres/migrations/savings_snapshots_pk_test.go +++ b/internal/database/postgres/migrations/savings_snapshots_pk_test.go @@ -22,10 +22,12 @@ import ( // EXISTS guard makes 000027 safe to apply regardless of whether 000018 has // already created the constraint. func TestMigration_SavingsSnapshotsPK(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() t.Run("full migration from scratch succeeds (000018 + 000027 coexist)", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -49,6 +51,7 @@ func TestMigration_SavingsSnapshotsPK(t *testing.T) { }) t.Run("rollback to 000026 then re-apply 000027 succeeds", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() diff --git a/internal/database/postgres/migrations/split_savingsplans_test.go b/internal/database/postgres/migrations/split_savingsplans_test.go index adca2c26f..c7a99d06f 100644 --- a/internal/database/postgres/migrations/split_savingsplans_test.go +++ b/internal/database/postgres/migrations/split_savingsplans_test.go @@ -19,6 +19,7 @@ import ( // #71's sagemaker row, and fresh install. Round-trips up/down to verify // no row identity is lost in the lossy down migration. func TestMigration_SplitSavingsPlans(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath() @@ -54,6 +55,7 @@ func TestMigration_SplitSavingsPlans(t *testing.T) { } t.Run("scenario 1: umbrella only", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -111,6 +113,7 @@ func TestMigration_SplitSavingsPlans(t *testing.T) { }) t.Run("scenario 2: umbrella + PR #71 sagemaker row (different values)", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -158,6 +161,7 @@ func TestMigration_SplitSavingsPlans(t *testing.T) { }) t.Run("scenario 3: fresh install (no SP rows)", func(t *testing.T) { + t.Parallel() container := testhelpers.RequireEmptyDatabase(ctx, t) pool := container.DB.Pool() @@ -189,6 +193,7 @@ func TestMigration_SplitSavingsPlans(t *testing.T) { // source key should be removed. Plans without an SP entry should be // untouched. func TestMigration_SplitSavingsPlans_PurchasePlansJSONB(t *testing.T) { + t.Parallel() ctx := context.Background() migrationsPath := getMigrationsPath()