import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { join } from "node:path"; import { Pool } from "pg"; import { pool, withTenant, withSystem } from "./client.js"; import { runMigrations } from "./migrate.js"; import { PublishingExecutionRepository } from "@tactical-media-broadcast-hub/publishing-execution"; /** * Audit-finding acceptance tests against a live PostgreSQL. * Migrations + cleanup run via the owner pool (SUPER_DATABASE_URL); * the assertions run as sp_app so FORCE RLS binds honestly. * Skipped without DATABASE_URL. */ const dbAvailable = process.env.DATABASE_URL !== undefined; const ownerPool = new Pool({ connectionString: process.env.SUPER_DATABASE_URL ?? process.env.DATABASE_URL }); async function asOwner(fn: (client: import("pg").PoolClient) => Promise): Promise { const client = await ownerPool.connect(); try { return await fn(client); } finally { client.release(); } } describe.skipIf(!dbAvailable)("tenancy + audit chain (live DB)", () => { let wsA: string; let wsB: string; beforeAll(async () => { await runMigrations(process.env.MIGRATIONS_DIR ?? join(process.cwd(), "migrations")); await asOwner(async (client) => { await client.query("DELETE FROM audit_log"); await client.query("DELETE FROM memberships"); await client.query("DELETE FROM connections"); await client.query("DELETE FROM credentials"); await client.query("DELETE FROM workspaces"); const a = await client.query("INSERT INTO workspaces (name, slug) VALUES ('A', $1) RETURNING id", [`a-${Date.now()}`]); const b = await client.query("INSERT INTO workspaces (name, slug) VALUES ('B', $1) RETURNING id", [`b-${Date.now()}`]); wsA = a.rows[0].id as string; wsB = b.rows[0].id as string; }); }); afterAll(async () => { await asOwner(async (client) => { await client.query("DELETE FROM workspaces WHERE id IN ($1, $2)", [wsA, wsB]); }); await pool.end(); await ownerPool.end(); }); it("RLS isolates tenants: A sees only A's connections (FORCE applies to owner)", async () => { await withTenant(wsA, async (client) => { await client.query("INSERT INTO connections (workspace_id, provider, channel_id) VALUES ($1, 'mock-social', 'ch-a')", [wsA]); }); await withTenant(wsB, async (client) => { await client.query("INSERT INTO connections (workspace_id, provider, channel_id) VALUES ($1, 'mock-social', 'ch-b')", [wsB]); }); const seenFromA = await withTenant(wsA, async (client) => { return (await client.query("SELECT channel_id FROM connections")).rows.map((r) => r.channel_id); }); expect(seenFromA).toEqual(["ch-a"]); const seenFromSystem = await withSystem(async (client) => { return (await client.query("SELECT count(*)::int AS n FROM connections")).rows[0]?.n; }); expect(seenFromSystem).toBe(0); }); it("SECURITY DEFINER tenant assertion: cross-tenant call rejected (finding #3)", async () => { await expect( withTenant(wsB, async (client) => { await client.query("SELECT audit_append($1, NULL, 'system', 'x', 'y', 'z', NULL)", [wsA]); }), ).rejects.toThrow(/tenant context mismatch/); }); it("audit branch: tenant context => tenant chain even for service callers (findings #1/#6)", async () => { await withTenant(wsA, async (client) => { const r = await client.query("SELECT audit_append($1, NULL, 'sp_scheduler', 'job.ran', 'job', 'j1', NULL) AS id", [wsA]); expect(r.rows[0]?.id).toBeTruthy(); const row = await client.query("SELECT workspace_id, actor_role FROM audit_log WHERE id = $1", [r.rows[0].id]); expect(row.rows[0]?.workspace_id).toBe(wsA); expect(row.rows[0]?.actor_role).toBe("sp_scheduler"); // system append inside tenant context must be refused await expect(client.query("SELECT audit_append_system(NULL, 'x', 'y', 'z', NULL)")).rejects.toThrow(); }); }); it("membership change bumps authz_epoch => sessions fail closed (finding #2)", async () => { const before = Number(await withSystem(async (c) => (await c.query("SELECT authz_epoch FROM workspaces WHERE id = $1", [wsA])).rows[0]?.authz_epoch)); await asOwner(async (c) => { const u = await c.query("INSERT INTO users (external_id, email, display_name) VALUES ($1, 'e@x', 'E') RETURNING id", [`ext-${Date.now()}`]); await withTenant(wsA, async (tenantClient) => { await tenantClient.query("INSERT INTO memberships (workspace_id, user_id, role) VALUES ($1, $2, 'Publisher')", [wsA, u.rows[0].id]); }); }); const after = Number(await withSystem(async (c) => (await c.query("SELECT authz_epoch FROM workspaces WHERE id = $1", [wsA])).rows[0]?.authz_epoch)); expect(after).toBeGreaterThan(before); }); it("hash chain detects tampering", async () => { await withTenant(wsA, async (client) => { await client.query("SELECT audit_append($1, NULL, 'system', 't.1', 't', '1', NULL)", [wsA]); await client.query("SELECT audit_append($1, NULL, 'system', 't.2', 't', '2', NULL)", [wsA]); }); const ok = await withTenant(wsA, async (c) => (await c.query("SELECT audit_verify_chain($1) AS ok", [wsA])).rows[0]?.ok); expect(ok).toBe(true); // direct tamper (as owner, inside the tenant context, bypassing the append fn) await withTenant(wsA, async (c) => { await c.query("UPDATE audit_log SET action = 'forged' WHERE action = 't.1'"); }); const broken = await withTenant(wsA, async (c) => (await c.query("SELECT audit_verify_chain($1) AS ok", [wsA])).rows[0]?.ok); expect(broken).toBe(false); }); it("verifies a legacy v1 row followed by v2 and detects a changed v2 diff", async () => { await withTenant(wsB, async (client) => { await client.query( `WITH stamp AS MATERIALIZED (SELECT clock_timestamp() AS at) INSERT INTO audit_log (workspace_id, action, target_type, target_id, diff, at, prev_hash, hash, hash_version) SELECT $1, 'legacy', 'test', 'legacy', '{"value":1}'::jsonb, stamp.at, NULL, audit_hash(NULL, NULL, 'legacy', 'legacy', stamp.at), 1 FROM stamp`, [wsB], ); await client.query("SELECT audit_append($1, NULL, 'system', 'modern', 'test', 'modern', $2)", [wsB, { value: 2 }]); const verified = await client.query("SELECT audit_verify_chain($1) AS ok", [wsB]); expect(verified.rows[0]?.ok).toBe(true); await client.query("UPDATE audit_log SET diff = $2 WHERE workspace_id = $1 AND action = 'modern'", [wsB, { value: 3 }]); const broken = await client.query("SELECT audit_verify_chain($1) AS ok", [wsB]); expect(broken.rows[0]?.ok).toBe(false); }); }); it("scheduler claim refuses a pending schedule whose draft is not approved", async () => { const repository = new PublishingExecutionRepository(pool); try { const user = await asOwner(async (client) => { const inserted = await client.query( "INSERT INTO users (external_id, email, display_name) VALUES ($1, 'claim@example.com', 'Claim') RETURNING id", [`claim-${Date.now()}`], ); return inserted.rows[0]?.id as string; }); const key = `claim-${Date.now()}`; await withTenant(wsA, async (client) => { const connection = await client.query( "INSERT INTO connections (workspace_id, provider, channel_id) VALUES ($1, 'mock-social', 'claim') RETURNING id", [wsA], ); const draft = await client.query( "INSERT INTO drafts (workspace_id, author_user_id, post, state) VALUES ($1, $2, $3, 'in_review') RETURNING id", [wsA, user, { text: "unapproved", mediaAssetIds: [], visibility: "public", providerMetadata: {} }], ); await client.query( "INSERT INTO scheduled_publications (workspace_id, draft_id, connection_id, publish_at, idempotency_key) VALUES ($1, $2, $3, now(), $4)", [wsA, draft.rows[0]?.id, connection.rows[0]?.id, key], ); }); const schedule = await withTenant(wsA, (client) => client.query("SELECT id, generation FROM scheduled_publications WHERE idempotency_key = $1", [key])); await expect(repository.claim({ workspaceId: wsA, scheduleId: String(schedule.rows[0]?.id), generation: String(schedule.rows[0]?.generation), idempotencyKey: key })).resolves.toMatchObject({ kind: "failed", code: "EXECUTION_FENCE_MISSING" }); const state = await withTenant(wsA, (client) => client.query("SELECT state FROM scheduled_publications WHERE idempotency_key = $1", [key])); expect(state.rows[0]?.state).toBe("pending"); } finally { // The shared repository uses the caller-owned pool, closed by suite cleanup. } }); it("workspace_id is immutable on tenant rows (finding #8)", async () => { await expect( withTenant(wsA, async (c) => { await c.query("UPDATE connections SET workspace_id = $1 WHERE channel_id = 'ch-a'", [wsB]); }), ).rejects.toThrow(/workspace_id is immutable/); }); });