/** * test/tenant-isolation.test.mjs — Tenant isolation tests */ import { describe, it } from 'node:test'; import assert from 'node:assert'; // Mock pool that tracks tenant isolation function createMockPool() { const executedQueries = []; return { executedQueries, query: async (sql, params) => { executedQueries.push({ sql, params }); // Verify tenant_id is first param for most queries return { rows: [] }; }, connect: async () => ({ query: async (sql, params) => { executedQueries.push({ sql, params, client: true }); return { rows: [] }; }, release: () => {} }) }; } // Tenant context builder (same as in index.mjs) function buildCtx(req, entityType = null, entityId = null, decisionSource = 'user') { return { trace_id: req.headers?.['x-trace-id'] || 'test-trace', correlation_id: req.headers?.['x-correlation-id'] || 'test-correlation', tenant_id: req.headers?.['x-tenant-id'] || req.body?.tenant_id || 'wavult-group', user_id: req.headers?.['x-user-id'] || req.body?.user_id || 'system', entity_type: entityType, entity_id: entityId, decision_source: decisionSource, }; } describe('Tenant isolation', () => { it('should use tenant_id from headers', () => { const req = { headers: { 'x-tenant-id': 'tenant-a' }, body: {} }; const ctx = buildCtx(req); assert.strictEqual(ctx.tenant_id, 'tenant-a'); }); it('should fallback to body tenant_id if header missing', () => { const req = { headers: {}, body: { tenant_id: 'tenant-b' } }; const ctx = buildCtx(req); assert.strictEqual(ctx.tenant_id, 'tenant-b'); }); it('should use default tenant when no tenant specified', () => { const req = { headers: {}, body: {} }; const ctx = buildCtx(req); assert.strictEqual(ctx.tenant_id, 'wavult-group'); }); it('should include tenant_id in SQL queries', async () => { const pool = createMockPool(); const tenantId = 'tenant-test'; // Simulate a query that should include tenant_id await pool.query( 'SELECT * FROM ledger_accounts WHERE tenant_id = $1', [tenantId] ); const query = pool.executedQueries[0]; assert.strictEqual(query.params[0], tenantId); assert.ok(query.sql.includes('tenant_id')); }); it('should prevent cross-tenant data access', () => { const tenantA = 'tenant-a'; const tenantB = 'tenant-b'; // Simulate query for tenant A const queryA = { sql: 'SELECT * FROM ledger_journal_entries WHERE tenant_id = $1', params: [tenantA] }; // Ensure tenant B cannot access tenant A's data const isIsolated = queryA.params[0] !== tenantB; assert.strictEqual(isIsolated, true); }); it('should include tenant_id in all journal entry operations', () => { const operations = [ 'SELECT * FROM ledger_journal_entries WHERE tenant_id = $1', 'INSERT INTO ledger_journal_entries (tenant_id, ...) VALUES ($1, ...)', 'UPDATE ledger_journal_entries SET ... WHERE id = $2 AND tenant_id = $1', 'DELETE FROM ledger_journal_entries WHERE id = $1 AND tenant_id = $2' ]; for (const sql of operations) { assert.ok(sql.includes('tenant_id'), `Missing tenant_id in: ${sql}`); } }); }); describe('Multi-tenant scenarios', () => { it('should maintain separate contexts for concurrent requests', () => { const req1 = { headers: { 'x-tenant-id': 'tenant-1' }, body: {} }; const req2 = { headers: { 'x-tenant-id': 'tenant-2' }, body: {} }; const ctx1 = buildCtx(req1); const ctx2 = buildCtx(req2); assert.strictEqual(ctx1.tenant_id, 'tenant-1'); assert.strictEqual(ctx2.tenant_id, 'tenant-2'); assert.notStrictEqual(ctx1.tenant_id, ctx2.tenant_id); }); it('should propagate trace context across operations', () => { const req = { headers: { 'x-trace-id': 'trace-123', 'x-correlation-id': 'corr-456', 'x-tenant-id': 'tenant-x' }, body: {} }; const ctx = buildCtx(req, 'journal_entry', 'entry-1'); assert.strictEqual(ctx.trace_id, 'trace-123'); assert.strictEqual(ctx.correlation_id, 'corr-456'); assert.strictEqual(ctx.tenant_id, 'tenant-x'); assert.strictEqual(ctx.entity_type, 'journal_entry'); assert.strictEqual(ctx.entity_id, 'entry-1'); }); });