/** * BI-P002 DoD: "Database migrations + persistence + tenant isolation + * restore tests." * * REAL Postgres, same honest-degrade convention as * tests/p0/source-registry-postgres.realinfra.test.ts: gated on * ANALYTICS_TEST_DATABASE_URL, SKIPS HONESTLY when unset, never fabricates. * * docker run -d --name opt-bi-pg -p 15455:5432 \ * -e POSTGRES_USER=opt -e POSTGRES_PASSWORD=opt_bi_throwaway \ * -e POSTGRES_DB=opt_bi_test postgres:16-alpine * ANALYTICS_TEST_DATABASE_URL=postgres://opt:opt_bi_throwaway@localhost:15455/opt_bi_test \ * pnpm vitest run tests/analytics/analytics-domain-postgres.realinfra.test.ts * * The RESTORE half is the part that catches migration-chain rot: it replays * sql/001..008 in order into a FRESH database, exactly as * docs/runbook/backup-restore.md describes, and only then runs the * persistence assertions. sql/003_datasets.sql exists because that same drill * found a table no file ever created; this suite keeps that class of defect * from coming back through sql/008. */ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import postgres from 'postgres'; import { ANALYTICS_DOMAIN_ENTITIES, ANALYTICS_ENTITY_TABLES, IMMUTABLE_VERSION_ENTITIES, } from '@/src/analytics/domain/entities'; import { AnalyticsDomainRepository } from '@/src/analytics/repositories/analytics-domain-repository'; import { contractHash, type DatasetContract } from '@/src/analytics/domain/contract'; const TEST_DATABASE_URL = process.env.ANALYTICS_TEST_DATABASE_URL; const run = TEST_DATABASE_URL ? describe : describe.skip; /** A schema of its own so the drill starts from genuinely nothing. */ const SCHEMA = `bi_p002_${Date.now().toString(36)}`; let sql: ReturnType | undefined; let reachable = false; let unreachableReason = ''; /** Which sql/*.sql files the restore drill actually applied. */ const applied: string[] = []; const ALPHA = { tenant_id: 't_alpha', project_id: 'p_main' }; const BETA = { tenant_id: 't_beta', project_id: 'p_main' }; function repo(): AnalyticsDomainRepository { return new AnalyticsDomainRepository(sql!); } function contract(datasetId: string): DatasetContract { return { dataset_id: datasetId, version: '1.0.0', tier: 'CURATED', fields: [{ name: 'id', type: 'string', nullable: false, role: 'identifier', primary_identifier: true }], primary_identifiers: ['id'], dimensions: [], measures: [], freshness_slo_seconds: 3600, retention_days: 365, pii_classification: 'none', source_lineage: ['src_x'], quality_rules: [], allowed_purposes: ['analytics'], }; } beforeAll(async () => { if (!TEST_DATABASE_URL) { console.warn( '[realinfra] ANALYTICS_TEST_DATABASE_URL not set — skipping honestly, not fabricating success.', ); return; } try { sql = postgres(TEST_DATABASE_URL, { prepare: false, ssl: false, max: 3 }); await Promise.race([ sql`SELECT 1`, new Promise((_, reject) => setTimeout(() => reject(new Error('connect timeout')), 5000)), ]); // RESTORE DRILL: a brand-new schema, then every migration in order. await sql.unsafe(`CREATE SCHEMA ${SCHEMA}`); await sql.unsafe(`SET search_path TO ${SCHEMA}, public`); const dir = path.join(process.cwd(), 'sql'); for (const file of fs .readdirSync(dir) .filter((f) => f.endsWith('.sql')) .sort()) { const body = fs.readFileSync(path.join(dir, file), 'utf8'); try { await sql.unsafe(`SET search_path TO ${SCHEMA}, public; ${body}`); applied.push(file); } catch (err) { // 001 needs the `vector` extension, which a stock postgres:16-alpine // does not carry. Record the skip rather than hiding it — the files // sql/008 actually depends on must still apply. applied.push(`${file} (FAILED: ${String(err).slice(0, 120)})`); } } reachable = true; } catch (err) { reachable = false; unreachableReason = err instanceof Error ? err.message : String(err); } }, 60_000); afterAll(async () => { if (sql && reachable) { await sql.unsafe(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => undefined); } await sql?.end({ timeout: 5 }); }); run('BI-P002 analytics domain model against a LIVE Postgres', () => { it('restore drill: sql/008 applies cleanly on top of the replayed chain', (ctx) => { if (!reachable) ctx.skip(); expect(unreachableReason).toBe(''); const p008 = applied.find((f) => f.startsWith('008_')); expect(p008, `sql/008 was not applied. Applied: ${applied.join(', ')}`).toBe( '008_analytics_domain_model.sql', ); }); it('all nineteen entity tables really exist after the restore', async (ctx) => { if (!reachable) ctx.skip(); const rows = await sql!<{ table_name: string }[]>` SELECT table_name FROM information_schema.tables WHERE table_schema = ${SCHEMA}`; const present = new Set(rows.map((r) => r.table_name)); const missing = ANALYTICS_DOMAIN_ENTITIES.filter((e) => !present.has(ANALYTICS_ENTITY_TABLES[e])); expect(missing).toEqual([]); }); it('replaying sql/008 a second time is a no-op, not an error', async (ctx) => { if (!reachable) ctx.skip(); const body = fs.readFileSync(path.join(process.cwd(), 'sql', '008_analytics_domain_model.sql'), 'utf8'); await expect(sql!.unsafe(`SET search_path TO ${SCHEMA}, public; ${body}`)).resolves.toBeDefined(); }); it('persists a dataset and reads it back through the repository', async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); const created = await r.createDataset(ALPHA, { dataset_id: 'ds_alpha_1', name: 'Alpha listings', tier: 'CURATED', schema_contract_id: 'dc_alpha', schema_contract_version: '1.0.0', owner_id: 'team_data', source_ref: 'src_feed', freshness_slo_seconds: 3600, quality_rule_ids: [], retention_days: 365, classification: 'none', status: 'active', created_by: 'usr_1', }); expect(created.tenant_id).toBe('t_alpha'); const read = await r.getDataset(ALPHA, 'ds_alpha_1'); expect(read?.name).toBe('Alpha listings'); expect(read?.freshness_slo_seconds).toBe(3600); // Cross-check with direct SQL — proving the row is really in the table, // not an artifact of the repository's own bookkeeping. const direct = await sql!<{ c: string }[]>` SELECT count(*)::text AS c FROM opt_analytics_dataset WHERE dataset_id = 'ds_alpha_1'`; expect(Number(direct[0].c)).toBe(1); }); it("TENANT ISOLATION: t_beta cannot read t_alpha's dataset, even with the exact id", async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); expect(await r.getDataset(BETA, 'ds_alpha_1')).toBeUndefined(); expect(await r.listDatasets(BETA)).toEqual([]); // And the row IS there — the empty result is scoping, not absence. const direct = await sql!<{ c: string }[]>`SELECT count(*)::text AS c FROM opt_analytics_dataset`; expect(Number(direct[0].c)).toBeGreaterThanOrEqual(1); }); it("TENANT ISOLATION: a version cannot be minted against another tenant's dataset", async (ctx) => { if (!reachable) ctx.skip(); await expect( repo().createDatasetVersion(BETA, { dataset_id: 'ds_alpha_1', contract_hash: 'deadbeef', schema_contract_version: '1.0.0', }), ).rejects.toMatchObject({ code: 'NOT_FOUND' }); }); it('dataset versions are monotonic and carry the contract hash', async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); const c = contract('ds_alpha_1'); const v1 = await r.createDatasetVersion(ALPHA, { dataset_id: 'ds_alpha_1', contract_hash: contractHash(c), schema_contract_version: '1.0.0', }); const v2 = await r.createDatasetVersion(ALPHA, { dataset_id: 'ds_alpha_1', contract_hash: contractHash({ ...c, version: '2.0.0' }), schema_contract_version: '2.0.0', }); expect(v1.version).toBe(1); expect(v2.version).toBe(2); expect(v2.contract_hash).not.toBe(v1.contract_hash); expect((await r.listDatasetVersions(ALPHA, 'ds_alpha_1')).map((v) => v.version)).toEqual([1, 2]); }); it('IMMUTABILITY is enforced by the DATABASE: UPDATE and DELETE on a version row are refused', async (ctx) => { if (!reachable) ctx.skip(); // Direct SQL, bypassing every TypeScript `readonly` — the only test that // proves the guarantee survives a psql session. await expect( sql!.unsafe( `UPDATE ${SCHEMA}.opt_analytics_dataset_version SET contract_hash = 'tampered' WHERE dataset_id = 'ds_alpha_1'`, ), ).rejects.toThrow(/immutable/i); await expect( sql!.unsafe(`DELETE FROM ${SCHEMA}.opt_analytics_dataset_version WHERE dataset_id = 'ds_alpha_1'`), ).rejects.toThrow(/immutable/i); // The rows are untouched. const rows = await sql!<{ contract_hash: string }[]>` SELECT contract_hash FROM opt_analytics_dataset_version WHERE dataset_id = 'ds_alpha_1'`; expect(rows).toHaveLength(2); for (const r of rows) expect(r.contract_hash).not.toBe('tampered'); }); it('all four immutable entities refuse an UPDATE, not just the one that was tested first', async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); await r.createDataset(ALPHA, { dataset_id: 'ds_imm', name: 'Immutability fixture', tier: 'SERVING', schema_contract_id: 'dc_i', schema_contract_version: '1.0.0', owner_id: 'team_data', source_ref: 'src', freshness_slo_seconds: 60, quality_rule_ids: [], retention_days: 30, classification: 'none', status: 'active', }); const metric = await r.createMetric(ALPHA, { metric_id: 'm_imm', name: 'immutable metric', dataset_id: 'ds_imm', aggregation: 'sum', measure_field: 'value', allowed_dimension_ids: [], owner_id: 'team_data', }); await r.createMetricVersion(ALPHA, { metric_id: metric.metric_id, definition_json: '{"v":1}' }); await r.createDatasetVersion(ALPHA, { dataset_id: 'ds_imm', contract_hash: 'h1', schema_contract_version: '1.0.0', }); await sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_semantic_model (semantic_model_id, tenant_id, project_id, name, version, model_json, model_hash, owner_id) VALUES ('sm_1','t_alpha','p_main','model_a',1,'{}','h','team_data')`); await sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dashboard_definition (dashboard_id, tenant_id, project_id, name, owner_id) VALUES ('db_1','t_alpha','p_main','dash','team_data')`); await sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dashboard_version (dashboard_version_id, dashboard_id, tenant_id, project_id, version, layout_json, layout_hash) VALUES ('dv_1','db_1','t_alpha','p_main',1,'{}','h')`); const updates: Record = { DatasetVersion: `UPDATE ${SCHEMA}.opt_analytics_dataset_version SET contract_hash='x' WHERE dataset_id='ds_imm'`, MetricVersion: `UPDATE ${SCHEMA}.opt_analytics_metric_version SET definition_json='{}' WHERE metric_id='m_imm'`, SemanticModel: `UPDATE ${SCHEMA}.opt_analytics_semantic_model SET model_json='{}' WHERE semantic_model_id='sm_1'`, DashboardVersion: `UPDATE ${SCHEMA}.opt_analytics_dashboard_version SET layout_json='{}' WHERE dashboard_version_id='dv_1'`, }; for (const entity of IMMUTABLE_VERSION_ENTITIES) { await expect(sql!.unsafe(updates[entity]), `${entity} is NOT immutable`).rejects.toThrow(/immutable/i); } }); it('a metric version after the first must state WHY the number changed meaning', async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); await expect( r.createMetricVersion(ALPHA, { metric_id: 'm_imm', definition_json: '{"v":2}' }), ).rejects.toThrow(); const v2 = await r.createMetricVersion(ALPHA, { metric_id: 'm_imm', definition_json: '{"v":2}', change_reason: 'excluded internal test accounts from the numerator', }); expect(v2.version).toBe(2); }); it('a DatasetGrant without a purpose is refused by the DATABASE (purpose limitation)', async (ctx) => { if (!reachable) ctx.skip(); // Regression pin: the first version of this constraint was written // `array_length(allowed_purposes, 1) >= 1`, which is NULL for '{}' — and a // CHECK evaluating to NULL PASSES in Postgres. It could never fail. This // test found that; keep it. await expect( sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dataset_grant (grant_id, dataset_id, tenant_id, project_id, subject_id, subject_type, level, allowed_purposes) VALUES ('g_1','ds_imm','t_alpha','p_main','usr_1','user','read', '{}')`), ).rejects.toThrow(/purposes_nonempty/); // A grant WITH a purpose is accepted, so the constraint is not simply // rejecting everything. await expect( sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dataset_grant (grant_id, dataset_id, tenant_id, project_id, subject_id, subject_type, level, allowed_purposes) VALUES ('g_ok','ds_imm','t_alpha','p_main','usr_1','user','read', ARRAY['market_analytics'])`), ).resolves.toBeDefined(); }); it('a BIApplicationBinding without a purpose is refused too, and carries no credential column', async (ctx) => { if (!reachable) ctx.skip(); await expect( sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_bi_application_binding (binding_id, tenant_id, project_id, kind, name, principal_id, allowed_purposes) VALUES ('b_1','t_alpha','p_main','superset','sup','svc_1','{}')`), ).rejects.toThrow(/purposes_nonempty/); const cols = await sql!<{ column_name: string }[]>` SELECT column_name FROM information_schema.columns WHERE table_schema = ${SCHEMA} AND table_name = 'opt_analytics_bi_application_binding'`; const names = cols.map((c) => c.column_name.toLowerCase()); for (const forbidden of ['password', 'secret', 'dsn', 'token', 'connection_string', 'clickhouse_url']) { expect(names, `BI binding must never store ${forbidden}`).not.toContain(forbidden); } }); it('the dimension catalog is tenant-scoped and FAILS CLOSED for a tenant with none', async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); await r.createDimension(ALPHA, { dimension_id: 'dim_1', name: 'make', dataset_id: 'ds_imm', field_name: 'vehicle_make', field_type: 'string', classification: 'none', }); expect(await r.dimensionCatalog(ALPHA)).toEqual({ make: 'vehicle_make' }); // t_beta declared nothing, so it may reference nothing. expect(await r.dimensionCatalog(BETA)).toEqual({}); }); it('a dimension whose physical field is not identifier-safe is refused by the DATABASE', async (ctx) => { if (!reachable) ctx.skip(); await expect( sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dimension_definition (dimension_id, tenant_id, project_id, name, dataset_id, field_name, field_type) VALUES ('dim_bad','t_alpha','p_main','evil','ds_imm', 'x''; DROP TABLE opt_analytics_dataset; --','string')`), ).rejects.toThrow(); }); it('the repository refuses to act without a verified tenant scope', async (ctx) => { if (!reachable) ctx.skip(); const r = repo(); await expect(r.getDataset({ tenant_id: '', project_id: 'p' }, 'ds_imm')).rejects.toMatchObject({ code: 'UNAUTHENTICATED', }); await expect(r.listDatasets({ tenant_id: 't_alpha', project_id: ' ' })).rejects.toMatchObject({ code: 'UNAUTHENTICATED', }); }); });