Files
0pt/tests/analytics/analytics-domain-postgres.realinfra.test.ts
T

370 lines
16 KiB
TypeScript

/**
* 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<typeof postgres> | 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<string, string> = {
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',
});
});
});