From aea898d57047e7ddff591776df94980c9daa2dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pa=C5=9B?= Date: Wed, 21 Oct 2026 14:05:03 +0200 Subject: [PATCH] test(testing): boot each kyc e2e app on its own database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The re-kyc-on-deposit test failed about one run in three since at-least-once delivery. Its two apps shared one database, and every booted app runs an outbox relay over that database that publishes into its own Redis. When the stock app's relay claimed the deposit event the gated app wrote, the stock app's compliance handler, which has no re-kyc thresholds, handled it and the gated app never saw it. Each app now gets its own test database, seeded right after it boots. The bootTestApp doc no longer claims apps cannot compete for each other's events. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Mateusz Paś --- .../testing/src/__tests__/kyc.e2e.test.ts | 38 ++++++++++++------- packages/testing/src/app.ts | 8 +++- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/packages/testing/src/__tests__/kyc.e2e.test.ts b/packages/testing/src/__tests__/kyc.e2e.test.ts index 9541882a7..f5502aa48 100644 --- a/packages/testing/src/__tests__/kyc.e2e.test.ts +++ b/packages/testing/src/__tests__/kyc.e2e.test.ts @@ -16,6 +16,7 @@ import { registerAndMaterializePlayer, asAdmin, seedMinimal, + type BootTestAppConfig, type TestDb, type TestApp, } from '../index.js'; @@ -30,7 +31,10 @@ import { * test with a mocked DB cannot: real routing, real authz, real DB state, and the * real (fire-and-forget) event -> audit pipeline. * - * Two app instances share one database: + * Two app instances, each on its own database: an app's outbox relay publishes every + * pending event in its database into its own broker, so on a shared database an event + * written through `appGated` could be handled only by `appDefault`, which has no re-KYC + * thresholds. * - `appDefault` boots the stock stack (MockKycAdapter, no PLATFORM_CONFIG bound - the * withdrawal gate is therefore off) to prove the default submit/admin-read path. * - `appGated` adds a test-only overlay (fixtures/test-kyc-config-plugin.ts) binding @@ -44,7 +48,8 @@ import { const KYC_WEBHOOK_SECRET = 'e2e-kyc-webhook-secret'; -let db: TestDb; +let dbDefault: TestDb; +let dbGated: TestDb; let appDefault: TestApp; let appGated: TestApp; @@ -90,6 +95,15 @@ async function seedLegacyVerifiedStatus(container: Container, .where(eq(player.userId, userId)); } +async function bootSeededApp( + plugins: BootTestAppConfig['plugins'], + databaseUrl: string, +): Promise { + const app = await bootTestApp({ plugins, databaseUrl }); + await seedMinimal(app.container, { playerCount: 0 }); + return app; +} + beforeAll(async () => { process.env['KYC_WEBHOOK_SECRET'] = KYC_WEBHOOK_SECRET; process.env['BETTER_AUTH_SECRET'] ??= 'e2e-test-better-auth-secret-please-change-000000'; @@ -97,28 +111,26 @@ beforeAll(async () => { process.env['WITHDRAWAL_PIN_HMAC_SECRET'] ??= 'e2e-test-withdrawal-pin-hmac-secret-000000'; process.env['NODE_ENV'] ??= 'test'; - db = await setupTestDb(); const basePlugins = await loadExtensions(); - appDefault = await bootTestApp({ plugins: basePlugins, databaseUrl: db.url }); + dbDefault = await setupTestDb(); + appDefault = await bootSeededApp(basePlugins, dbDefault.url); const fixturePath = fileURLToPath( new URL('./fixtures/test-kyc-config-plugin.ts', import.meta.url), ); - appGated = await bootTestApp({ - plugins: [...basePlugins, { id: 'test-kyc-config', path: fixturePath }], - databaseUrl: db.url, - }); - - // Both apps share one database - seed the admin fixture once via appDefault's - // container; appGated's asAdmin() logs into the same underlying `user` row. - await seedMinimal(appDefault.container, { playerCount: 0 }); + dbGated = await setupTestDb(); + appGated = await bootSeededApp( + [...basePlugins, { id: 'test-kyc-config', path: fixturePath }], + dbGated.url, + ); }, 60_000); afterAll(async () => { await appDefault?.close(); await appGated?.close(); - await db?.dispose(); + await dbDefault?.dispose(); + await dbGated?.dispose(); }); describe('KYC submit (default MockKycAdapter) + admin read + authz', () => { diff --git a/packages/testing/src/app.ts b/packages/testing/src/app.ts index fde5babf2..b7022ac89 100644 --- a/packages/testing/src/app.ts +++ b/packages/testing/src/app.ts @@ -53,8 +53,12 @@ export type BootTestAppConfig = Pick & { * BullMQ, Redis cache, Redis rate limiter and Redis Pub/Sub - so event fan-out, job * retries, throttling and realtime push are exercised for real rather than through * an in-process stand-in. Each booted app claims its own Redis logical database, so - * several apps in one test file cannot compete for each other's events, jobs, cache - * entries or channels. + * several apps in one test file cannot compete for each other's jobs, cache entries or + * channels. Outbox events (`emitInTransaction`) are the exception when two apps share a + * database: every app's relay publishes the pending events of its database into its own + * broker, so such an event reaches the handlers of whichever app claimed it first, not + * of the app that wrote it. Boot each app on its own `setupTestDb()` when their handlers + * behave differently. * * The broker reads from `startId: '0'` here, unlike production's `$`: a test acts * within milliseconds of boot, and a group created lazily at `$` would silently drop