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() 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 41aabf957..9f28cd8af 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. @@ -162,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 @@ -188,11 +219,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 +259,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 +279,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{