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

195 lines
7.4 KiB
TypeScript

/**
* BI-P002 (domain model) + BI-P003 (data tiers), the parts provable without
* infrastructure. The Postgres half — real tables, real immutability
* triggers, real tenant scoping — is
* tests/analytics/analytics-domain-postgres.realinfra.test.ts.
*
* The registry-vs-migration test below is the one that earns its place: it
* reads sql/008_analytics_domain_model.sql from disk and asserts a CREATE
* TABLE exists for every one of the nineteen canonical entities. A future
* rename, or an entity quietly dropped from the migration, fails HERE rather
* than at 3am against a database that turns out to be missing a table.
*/
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import {
ANALYTICS_DOMAIN_ENTITIES,
ANALYTICS_ENTITY_TABLES,
IMMUTABLE_VERSION_ENTITIES,
} from '@/src/analytics/domain/entities';
import {
ANALYTICS_TIERS,
assertReadableByBiSurface,
assertTierLineageSane,
BI_READABLE_TIERS,
isAnalyticsTier,
TIER_RANK,
} from '@/src/analytics/domain/tiers';
const MIGRATION = fs.readFileSync(path.join(process.cwd(), 'sql', '008_analytics_domain_model.sql'), 'utf8');
describe('BI-P002 canonical entity set', () => {
it('lists exactly the nineteen entities the program names, verbatim', () => {
expect(ANALYTICS_DOMAIN_ENTITIES).toEqual([
'AnalyticsDataset',
'DatasetVersion',
'DatasetField',
'DatasetGrant',
'MetricDefinition',
'MetricVersion',
'DimensionDefinition',
'SemanticModel',
'AnalyticsQuery',
'QueryExecution',
'QueryResult',
'DashboardDefinition',
'DashboardVersion',
'ReportDefinition',
'ReportRun',
'DataQualityRule',
'DataQualityRun',
'LineageReference',
'BIApplicationBinding',
]);
expect(ANALYTICS_DOMAIN_ENTITIES).toHaveLength(19);
});
it('sql/008 defines a table for every entity (no entity is a type without storage)', () => {
const missing: string[] = [];
for (const entity of ANALYTICS_DOMAIN_ENTITIES) {
const table = ANALYTICS_ENTITY_TABLES[entity];
if (!new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\b`).test(MIGRATION)) {
missing.push(`${entity} -> ${table}`);
}
}
expect(missing).toEqual([]);
});
it('every tenant-owned table carries tenant_id AND project_id', () => {
// Split on the table definitions and check each block.
const blocks = MIGRATION.split(/CREATE TABLE IF NOT EXISTS /).slice(1);
const offenders: string[] = [];
for (const block of blocks) {
const name = block.split(/[\s(]/)[0];
if (!Object.values(ANALYTICS_ENTITY_TABLES).includes(name)) continue;
const body = block.slice(0, block.indexOf(');'));
if (!/\btenant_id TEXT NOT NULL\b/.test(body) || !/\bproject_id TEXT NOT NULL\b/.test(body)) {
offenders.push(name);
}
}
expect(offenders).toEqual([]);
});
it('the four immutable version entities have a BEFORE UPDATE OR DELETE trigger in sql/008', () => {
expect(IMMUTABLE_VERSION_ENTITIES).toEqual([
'DatasetVersion',
'MetricVersion',
'SemanticModel',
'DashboardVersion',
]);
for (const entity of IMMUTABLE_VERSION_ENTITIES) {
const table = ANALYTICS_ENTITY_TABLES[entity];
expect(
new RegExp(`BEFORE UPDATE OR DELETE ON ${table}\\b`).test(MIGRATION),
`${entity} (${table}) has no immutability trigger`,
).toBe(true);
}
});
it('BIApplicationBinding storage carries NO credential column — the ownership boundary is structural', () => {
const block = MIGRATION.split('CREATE TABLE IF NOT EXISTS opt_analytics_bi_application_binding')[1] ?? '';
const body = block.slice(0, block.indexOf(');'));
for (const forbidden of ['password', 'dsn', 'secret', 'connection_string', 'clickhouse_url', 'token']) {
expect(body.toLowerCase()).not.toContain(forbidden);
}
});
it('sql/008 is idempotent-shaped: every CREATE is IF NOT EXISTS / OR REPLACE', () => {
const creates =
MIGRATION.match(/^CREATE (TABLE|INDEX|TRIGGER|FUNCTION|OR REPLACE FUNCTION)[^\n]*/gm) ?? [];
const nonIdempotent = creates.filter(
(line) => !/IF NOT EXISTS/.test(line) && !/OR REPLACE/.test(line) && !/^CREATE TRIGGER/.test(line),
);
expect(nonIdempotent).toEqual([]);
// Triggers cannot take IF NOT EXISTS in Postgres, so each one must be
// preceded by a DROP TRIGGER IF EXISTS to stay replayable.
const triggers = MIGRATION.match(/^CREATE TRIGGER (\w+)/gm) ?? [];
for (const t of triggers) {
const name = t.replace('CREATE TRIGGER ', '');
expect(MIGRATION).toContain(`DROP TRIGGER IF EXISTS ${name}`);
}
});
});
describe('BI-P003 analytics data tiers', () => {
it('canonicalizes exactly RAW / NORMALIZED / CURATED / SERVING', () => {
expect(ANALYTICS_TIERS).toEqual(['RAW', 'NORMALIZED', 'CURATED', 'SERVING']);
expect(isAnalyticsTier('CURATED')).toBe(true);
expect(isAnalyticsTier('raw')).toBe(false);
expect(isAnalyticsTier('BRONZE')).toBe(false);
});
it('no business dashboard reads a RAW/NORMALIZED dataset by default', () => {
expect(BI_READABLE_TIERS).toEqual(['SERVING', 'CURATED']);
for (const tier of ['RAW', 'NORMALIZED'] as const) {
expect(() =>
assertReadableByBiSurface({ dataset_id: 'ds_1', tier }, 'dashboard:exec-overview'),
).toThrowError(/may not read/);
}
for (const tier of ['SERVING', 'CURATED'] as const) {
expect(() => assertReadableByBiSurface({ dataset_id: 'ds_1', tier }, 'dashboard:x')).not.toThrow();
}
});
it('a recorded exception is the only way a RAW dataset reaches a BI surface', () => {
expect(() =>
assertReadableByBiSurface(
{
dataset_id: 'ds_1',
tier: 'RAW',
raw_access_exception: 'INC-2026-09-06 forensic review, expires 2026-10-01',
},
'dashboard:incident',
),
).not.toThrow();
// An empty/whitespace "exception" is not an exception.
expect(() =>
assertReadableByBiSurface(
{ dataset_id: 'ds_1', tier: 'RAW', raw_access_exception: ' ' },
'dashboard:x',
),
).toThrowError(/may not read/);
});
it('tier order is enforced: a CURATED dataset cannot be built from SERVING', () => {
expect(TIER_RANK.RAW).toBeLessThan(TIER_RANK.NORMALIZED);
expect(TIER_RANK.NORMALIZED).toBeLessThan(TIER_RANK.CURATED);
expect(TIER_RANK.CURATED).toBeLessThan(TIER_RANK.SERVING);
expect(() => assertTierLineageSane('CURATED', 'SERVING')).toThrowError(/tier order/);
expect(() => assertTierLineageSane('SERVING', 'CURATED')).not.toThrow();
expect(() => assertTierLineageSane('CURATED', 'CURATED')).not.toThrow();
});
it('sql/008 constrains tier at the DATABASE level, not only in TypeScript', () => {
expect(MIGRATION).toContain("CHECK (tier IN ('RAW','NORMALIZED','CURATED','SERVING'))");
});
it("BI-P003's eight required declarations are all NOT NULL in sql/008", () => {
const block = MIGRATION.split('CREATE TABLE IF NOT EXISTS opt_analytics_dataset ')[1] ?? '';
const body = block.slice(0, block.indexOf(');'));
for (const col of [
'tier',
'schema_contract_id',
'owner_id',
'source_ref',
'freshness_slo_seconds',
'retention_days',
'classification',
'quality_rule_ids',
]) {
expect(new RegExp(`\\b${col}\\b[^,]*NOT NULL`).test(body), `${col} is nullable`).toBe(true);
}
});
});