test(integration): tenant isolation tests for goals and organizations

This commit is contained in:
admin-valentin 2026-07-31 14:40:29 +00:00
parent c3b0005e19
commit 458217a0ec

View file

@ -0,0 +1,106 @@
/**
* Tenant Isolation Integration Tests
*
* Verifies that data written under tenant-A is never readable from tenant-B session.
* Runs against real Drizzle/Postgres via DATABASE_URL env var.
* Skip if DATABASE_URL is not set (CI without DB).
*/
import { eq, and, isNull } from 'drizzle-orm';
import { db } from '../db/client';
import { goals, organizations, tasks } from '../db/schema';
const HAS_DB = !!process.env.DATABASE_URL;
const TENANT_A = '00000000-0000-0000-0000-000000000a01';
const TENANT_B = '00000000-0000-0000-0000-000000000b02';
const WS_A = '00000000-0000-0000-0000-000000000a03';
const WS_B = '00000000-0000-0000-0000-000000000b04';
const USER_A = '00000000-0000-0000-0000-000000000a05';
const USER_B = '00000000-0000-0000-0000-000000000b05';
(HAS_DB ? describe : describe.skip)('Tenant Isolation', () => {
let goalIdA: string;
let orgIdA: string;
afterAll(async () => {
// Cleanup test data
if (goalIdA) await db.delete(goals).where(eq(goals.id, goalIdA));
if (orgIdA) await db.delete(organizations).where(eq(organizations.id, orgIdA));
});
describe('Goals', () => {
it('tenant-A can create a goal', async () => {
// Arrange + Act
const [row] = await db
.insert(goals)
.values({
tenantId: TENANT_A,
ownerUserId: USER_A,
horizon: 'Q4 2026',
metric: 'MRR',
target: '100k',
milestones: [] as unknown[],
})
.returning();
goalIdA = row.id;
// Assert
expect(row.tenantId).toBe(TENANT_A);
expect(row.id).toBeTruthy();
});
it('tenant-B cannot see tenant-A goals', async () => {
// Act
const results = await db.query.goals.findMany({
where: and(eq(goals.tenantId, TENANT_B), isNull(goals.deletedAt)),
});
// Assert — no tenant-A goal leaks into tenant-B query
const leaked = results.filter((g) => g.tenantId === TENANT_A);
expect(leaked).toHaveLength(0);
});
it('tenant-A can see their own goal by id', async () => {
// Act
const row = await db.query.goals.findFirst({
where: and(eq(goals.id, goalIdA), eq(goals.tenantId, TENANT_A), isNull(goals.deletedAt)),
});
// Assert
expect(row).toBeDefined();
expect(row!.tenantId).toBe(TENANT_A);
});
it('tenant-B cannot fetch tenant-A goal by id', async () => {
// Act — tenant-B tries to access tenant-A goal with TENANT_B filter
const row = await db.query.goals.findFirst({
where: and(eq(goals.id, goalIdA), eq(goals.tenantId, TENANT_B)),
});
// Assert — should return undefined (not found)
expect(row).toBeUndefined();
});
});
describe('Organizations', () => {
it('tenant-A creates organization invisible to tenant-B', async () => {
// Arrange + Act
const [org] = await db
.insert(organizations)
.values({
tenantId: TENANT_A,
name: 'Tenant A Corp',
country: 'RO',
})
.returning();
orgIdA = org.id;
// tenant-B query
const tenantBOrgs = await db.query.organizations.findMany({
where: and(eq(organizations.tenantId, TENANT_B), isNull(organizations.deletedAt)),
});
expect(tenantBOrgs.find((o) => o.id === orgIdA)).toBeUndefined();
});
});
});