/** * tests/analytics/dashboard-release-postgres.realinfra.test.ts * ============================================================= * sql/010 (BI-P061) against a REAL Postgres. * * The interesting claim in sql/010 is not "there is a state machine" — it is * "TWO PEOPLE CANNOT PUBLISH AT ONCE". That claim lives in a partial unique * index, and an index nobody raced is an index nobody has tested. So the * concurrency test runs BOTH directions, in two separate schemas so neither * can contaminate the other: * * 1. WITHOUT the index (a schema where it is dropped): two sessions publish * a release of the same dashboard concurrently, each having first read * "nothing is live" — the exact interleaving an application-level check * cannot see. The database ends up with TWO published releases. This is * the counterfactual: it proves the read-then-write check in * DashboardReleaseService.publish() is NOT the guarantee, and that this * test can detect the failure it claims to prevent. * 2. WITH the index: the same race, and the loser fails with * unique_violation (23505). Exactly one published release remains. * * The transition trigger is attacked the same way: every illegal transition is * attempted against the database, not against the TypeScript. * * Gated on ANALYTICS_TEST_DATABASE_URL, matching the convention in * analytics-governance-postgres.realinfra.test.ts. With it set and Postgres * down this FAILS rather than skipping. * * docker run -d --name pg -p 25441:5432 -e POSTGRES_PASSWORD=test \ * -e POSTGRES_DB=opt postgres:16-alpine * ANALYTICS_TEST_DATABASE_URL=postgres://postgres:test@127.0.0.1:25441/opt \ * pnpm vitest run tests/analytics/dashboard-release-postgres.realinfra.test.ts */ import fs from 'node:fs'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import postgres from 'postgres'; const TEST_DATABASE_URL = process.env.ANALYTICS_TEST_DATABASE_URL; const run = TEST_DATABASE_URL ? describe : describe.skip; const STAMP = Date.now().toString(36); const SCHEMA = `bi_p061_${STAMP}`; /** The counterfactual lives in its own schema: no cleanup, no contamination. */ const SCHEMA_NOGUARD = `bi_p061_noguard_${STAMP}`; const HASH_A = 'a'.repeat(64); const HASH_B = 'b'.repeat(64); const MIGRATIONS = ['sql/008_analytics_domain_model.sql', 'sql/010_analytics_dashboard_releases.sql']; let sql: ReturnType; function connect(schema: string, max = 1): ReturnType { return postgres(TEST_DATABASE_URL!, { max, onnotice: () => undefined, connection: { search_path: schema }, }); } async function refusalOf(fn: () => Promise): Promise { try { await fn(); return undefined; } catch (e) { return String((e as Error).message); } } async function migrate(conn: ReturnType): Promise { for (const file of MIGRATIONS) { await conn.unsafe(fs.readFileSync(path.join(process.cwd(), file), 'utf8')); } } async function seedDashboard(conn: ReturnType, id: string): Promise { await conn.unsafe(` INSERT INTO opt_analytics_dashboard_definition (dashboard_id, tenant_id, project_id, name, current_version, owner_id) VALUES ('${id}', 't_alpha', 'p_main', '${id}', 1, 'u_1') ON CONFLICT DO NOTHING`); } async function seedVersion( conn: ReturnType, versionId: string, dashboardId: string, version: number, hash = HASH_A, ): Promise { await conn.unsafe(` INSERT INTO opt_analytics_dashboard_version (dashboard_version_id, dashboard_id, tenant_id, project_id, version, layout_json, layout_hash) VALUES ('${versionId}', '${dashboardId}', 't_alpha', 'p_main', ${version}, '{}', '${hash}')`); } async function seedDraftRelease( conn: ReturnType, releaseId: string, versionId: string, dashboardId: string, version: number, hash = HASH_A, ): Promise { await conn.unsafe(` INSERT INTO opt_analytics_dashboard_release (release_id, dashboard_id, dashboard_version_id, tenant_id, project_id, version, state, document_hash) VALUES ('${releaseId}', '${dashboardId}', '${versionId}', 't_alpha', 'p_main', ${version}, 'draft', '${hash}')`); } async function publishedCount(conn: ReturnType, dashboardId: string): Promise { const rows = (await conn.unsafe( `SELECT count(*)::int AS n FROM opt_analytics_dashboard_release WHERE dashboard_id = '${dashboardId}' AND state = 'published'`, )) as unknown as Array<{ n: number }>; return rows[0].n; } /** * Two sessions publish a release of the SAME dashboard at the same time, each * having first read "nothing is live". This is the create-and-publish path: * a new version is minted and released in one transaction, which is what a * "save and publish" button does. */ async function racePublish(schema: string, dashboardId: string): Promise<{ ok: number; codes: string[] }> { const a = connect(schema); const b = connect(schema); const publish = (conn: ReturnType, n: number) => conn.begin(async (tx) => { await tx.unsafe( `SELECT release_id FROM opt_analytics_dashboard_release WHERE dashboard_id = '${dashboardId}' AND state = 'published'`, ); // A tiny pause so both transactions have certainly done their read // before either writes. Without it the race may not interleave and the // counterfactual could pass for the wrong reason. await new Promise((r) => setTimeout(r, 60)); await tx.unsafe(` INSERT INTO opt_analytics_dashboard_release (release_id, dashboard_id, dashboard_version_id, tenant_id, project_id, version, state, document_hash, published_at) VALUES ('rel_race_${n}', '${dashboardId}', 'dv_race_${n}', 't_alpha', 'p_main', ${400 + n}, 'published', '${n === 1 ? HASH_A : HASH_B}', now())`); }); const results = await Promise.allSettled([publish(a, 1), publish(b, 2)]); await a.end({ timeout: 5 }); await b.end({ timeout: 5 }); return { ok: results.filter((r) => r.status === 'fulfilled').length, codes: results.flatMap((r) => r.status === 'rejected' ? [String((r.reason as { code?: string })?.code ?? r.reason)] : [], ), }; } beforeAll(async () => { if (!TEST_DATABASE_URL) return; const admin = postgres(TEST_DATABASE_URL, { max: 1, onnotice: () => undefined }); await admin.unsafe(`CREATE SCHEMA IF NOT EXISTS ${SCHEMA}`); await admin.unsafe(`CREATE SCHEMA IF NOT EXISTS ${SCHEMA_NOGUARD}`); await admin.end({ timeout: 5 }); sql = connect(SCHEMA, 3); await migrate(sql); for (const d of ['dash_txn', 'dash_race', 'dash_draft']) await seedDashboard(sql, d); }, 120_000); afterAll(async () => { if (!TEST_DATABASE_URL || !sql) return; await sql.unsafe(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`); await sql.unsafe(`DROP SCHEMA IF EXISTS ${SCHEMA_NOGUARD} CASCADE`); await sql.end({ timeout: 5 }); }); run('sql/010 applies and replays', () => { it('is idempotent — a replay onto an already-migrated schema succeeds', async () => { const text = fs.readFileSync( path.join(process.cwd(), 'sql/010_analytics_dashboard_releases.sql'), 'utf8', ); await expect(sql.unsafe(text)).resolves.toBeDefined(); }); it('has no column that could hold a document body — the document lives in the version row', async () => { const rows = (await sql.unsafe( `SELECT column_name FROM information_schema.columns WHERE table_schema = '${SCHEMA}' AND table_name = 'opt_analytics_dashboard_release'`, )) as unknown as Array<{ column_name: string }>; const names = new Set(rows.map((r) => r.column_name)); for (const forbidden of ['layout_json', 'document', 'definition', 'payload', 'sql']) { expect(names.has(forbidden), forbidden).toBe(false); } expect(names.has('document_hash')).toBe(true); }); }); run('BI-P061 transitions are enforced by the database', () => { beforeAll(async () => { await seedVersion(sql, 'dv_t1', 'dash_txn', 101); await seedVersion(sql, 'dv_t3', 'dash_txn', 103, HASH_B); await seedDraftRelease(sql, 'rel_t1', 'dv_t1', 'dash_txn', 101); }); it('draft -> published is allowed', async () => { await sql.unsafe( `UPDATE opt_analytics_dashboard_release SET state='published', published_at=now() WHERE release_id='rel_t1'`, ); const rows = (await sql.unsafe( `SELECT state FROM opt_analytics_dashboard_release WHERE release_id='rel_t1'`, )) as unknown as Array<{ state: string }>; expect(rows[0].state).toBe('published'); }); it('published -> draft (un-publishing) is REFUSED', async () => { const msg = await refusalOf(() => sql.unsafe( `UPDATE opt_analytics_dashboard_release SET state='draft', published_at=NULL WHERE release_id='rel_t1'`, ), ); expect(msg).toMatch(/illegal dashboard release transition published -> draft/); }); it('repointing a release at a different version is REFUSED', async () => { const msg = await refusalOf(() => sql.unsafe( `UPDATE opt_analytics_dashboard_release SET dashboard_version_id='dv_t3' WHERE release_id='rel_t1'`, ), ); expect(msg).toMatch(/immutable apart from its state/); }); it('rewriting document_hash on a release is REFUSED', async () => { const msg = await refusalOf(() => sql.unsafe( `UPDATE opt_analytics_dashboard_release SET document_hash='${HASH_B}' WHERE release_id='rel_t1'`, ), ); expect(msg).toMatch(/immutable apart from its state/); }); it('rewriting published_at is REFUSED — a published timestamp is history', async () => { const msg = await refusalOf(() => sql.unsafe( `UPDATE opt_analytics_dashboard_release SET published_at = now() - interval '10 days' WHERE release_id='rel_t1'`, ), ); expect(msg).toMatch(/published_at is history/); }); it('published -> superseded is allowed, and superseded -> published is REFUSED', async () => { await sql.unsafe( `UPDATE opt_analytics_dashboard_release SET state='superseded', superseded_at=now() WHERE release_id='rel_t1'`, ); const msg = await refusalOf(() => sql.unsafe(`UPDATE opt_analytics_dashboard_release SET state='published' WHERE release_id='rel_t1'`), ); expect(msg).toMatch(/illegal dashboard release transition superseded -> published/); }); it('deleting a release is REFUSED — the ledger has no eraser', async () => { const msg = await refusalOf(() => sql.unsafe(`DELETE FROM opt_analytics_dashboard_release WHERE release_id='rel_t1'`), ); expect(msg).toMatch(/ledger; delete is refused/); }); it('a published release with no published_at is REFUSED by the CHECK', async () => { const msg = await refusalOf(() => sql.unsafe(` INSERT INTO opt_analytics_dashboard_release (release_id, dashboard_id, dashboard_version_id, tenant_id, project_id, version, state, document_hash) VALUES ('rel_bad', 'dash_txn', 'dv_t3', 't_alpha', 'p_main', 900, 'published', '${HASH_A}')`), ); expect(msg).toMatch(/opt_analytics_dashboard_release_timestamps/); }); it('a document_hash that is not a sha256 digest is REFUSED', async () => { const msg = await refusalOf(() => sql.unsafe(` INSERT INTO opt_analytics_dashboard_release (release_id, dashboard_id, dashboard_version_id, tenant_id, project_id, version, state, document_hash) VALUES ('rel_bad2', 'dash_txn', 'dv_t3', 't_alpha', 'p_main', 901, 'draft', 'not-a-hash')`), ); expect(msg).toMatch(/document_hash/); }); it('the sql/008 immutability trigger still forbids editing the frozen document', async () => { // Assert the row exists FIRST: an UPDATE that matches nothing raises // nothing, which would read as "refused" to a careless test. const before = (await sql.unsafe( `SELECT count(*)::int AS n FROM opt_analytics_dashboard_version WHERE dashboard_version_id='dv_t3'`, )) as unknown as Array<{ n: number }>; expect(before[0].n).toBe(1); const msg = await refusalOf(() => sql.unsafe( `UPDATE opt_analytics_dashboard_version SET layout_json='{"tampered":true}' WHERE dashboard_version_id='dv_t3'`, ), ); expect(msg).toMatch(/rows are immutable/); }); it('a second open draft for one dashboard is REFUSED', async () => { await seedVersion(sql, 'dv_d1', 'dash_draft', 301); await seedVersion(sql, 'dv_d2', 'dash_draft', 302); await seedDraftRelease(sql, 'rel_d1', 'dv_d1', 'dash_draft', 301); const msg = await refusalOf(() => seedDraftRelease(sql, 'rel_d2', 'dv_d2', 'dash_draft', 302)); expect(msg).toMatch(/one_draft/); }); }); run('BI-P061 concurrency: the partial unique index is the guarantee', () => { let noguard: ReturnType; beforeAll(async () => { noguard = connect(SCHEMA_NOGUARD, 3); await migrate(noguard); await noguard.unsafe(`DROP INDEX opt_analytics_dashboard_release_one_published`); await seedDashboard(noguard, 'dash_race'); await seedVersion(noguard, 'dv_race_1', 'dash_race', 401); await seedVersion(noguard, 'dv_race_2', 'dash_race', 402, HASH_B); await seedVersion(sql, 'dv_race_1', 'dash_race', 401); await seedVersion(sql, 'dv_race_2', 'dash_race', 402, HASH_B); }, 60_000); afterAll(async () => { if (noguard) await noguard.end({ timeout: 5 }); }); it('WITHOUT the index, two concurrent publishes BOTH succeed — two live dashboards', async () => { const race = await racePublish(SCHEMA_NOGUARD, 'dash_race'); expect(race.ok).toBe(2); expect(await publishedCount(noguard, 'dash_race')).toBe(2); }, 30_000); it('WITH the index, the same race leaves exactly one published release', async () => { expect(await publishedCount(sql, 'dash_race')).toBe(0); const race = await racePublish(SCHEMA, 'dash_race'); expect(race.ok).toBe(1); expect(race.codes.join(' ')).toContain('23505'); expect(await publishedCount(sql, 'dash_race')).toBe(1); }, 30_000); });