/** * Task 8 — aggregated projection (TDD). * * Tests for: * 1. listFilesAggregatedByEntity (4 cases) * 2. listInflightWorkflowsAggregatedByEntity (1 case) * 3. applyEntityFkFromFolder (3 cases) * * Fixture convention: makePort / makeClient / makeCompany / makeYacht from * helpers/factories; TEST_USER_ID resolved once via beforeAll from a seeded * user — same pattern as document-folders-system-folders.test.ts. */ import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; import { and, eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { files, documents, documentFolders } from '@/lib/db/schema/documents'; import { clients } from '@/lib/db/schema/clients'; import { user } from '@/lib/db/schema/users'; import { yachts } from '@/lib/db/schema/yachts'; import { ensureSystemRoots, ensureEntityFolder } from '@/lib/services/document-folders.service'; import { listFilesAggregatedByEntity, applyEntityFkFromFolder } from '@/lib/services/files'; import { listInflightWorkflowsAggregatedByEntity } from '@/lib/services/documents.service'; import { makePort, makeClient, makeCompany, makeYacht, makeMembership } from '../helpers/factories'; let TEST_USER_ID = ''; beforeAll(async () => { const [u] = await db.select({ id: user.id }).from(user).limit(1); if (!u) throw new Error('No user available; run pnpm db:seed first'); TEST_USER_ID = u.id; }); // ─── Helper to insert a file row directly ──────────────────────────────────── async function insertFile( portId: string, overrides: { clientId?: string | null; companyId?: string | null; yachtId?: string | null; folderId?: string | null; } = {}, ) { const [row] = await db .insert(files) .values({ portId, clientId: overrides.clientId ?? null, companyId: overrides.companyId ?? null, yachtId: overrides.yachtId ?? null, folderId: overrides.folderId ?? null, filename: `file-${crypto.randomUUID().slice(0, 8)}.pdf`, originalName: `original-${crypto.randomUUID().slice(0, 8)}.pdf`, mimeType: 'application/pdf', sizeBytes: '1024', storagePath: `test/${crypto.randomUUID()}.pdf`, storageBucket: 'crm-files', uploadedBy: TEST_USER_ID, }) .returning(); return row!; } // ─── listFilesAggregatedByEntity ───────────────────────────────────────────── describe('files service · listFilesAggregatedByEntity', () => { describe('groups DIRECTLY ATTACHED + FROM COMPANY + FROM YACHT for a client view', () => { let portId: string; let clientId: string; let companyId: string; let yachtId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; const client = await makeClient({ portId }); clientId = client.id; const company = await makeCompany({ portId }); companyId = company.id; await makeMembership({ companyId, clientId }); const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: clientId }); yachtId = yacht.id; // Three files: directly on client, on company, on yacht await insertFile(portId, { clientId }); await insertFile(portId, { companyId }); await insertFile(portId, { yachtId }); }); it('returns DIRECTLY ATTACHED, FROM COMPANY, and FROM YACHT groups', async () => { const result = await listFilesAggregatedByEntity(portId, 'client', clientId); const labels = result.groups.map((g) => g.label); expect(labels).toContain('DIRECTLY ATTACHED'); expect(labels.some((l) => l.startsWith('FROM COMPANY'))).toBe(true); expect(labels.some((l) => l.startsWith('FROM YACHT'))).toBe(true); }); it('each group has the correct source tag', async () => { const result = await listFilesAggregatedByEntity(portId, 'client', clientId); const sourceMap: Record = {}; for (const g of result.groups) { sourceMap[g.label] = g.source; } expect(sourceMap['DIRECTLY ATTACHED']).toBe('direct'); const companyGroup = result.groups.find((g) => g.label.startsWith('FROM COMPANY')); expect(companyGroup?.source).toBe('company'); const yachtGroup = result.groups.find((g) => g.label.startsWith('FROM YACHT')); expect(yachtGroup?.source).toBe('yacht'); }); it('file rows carry signedFromDocumentId=null when no workflow references them', async () => { const result = await listFilesAggregatedByEntity(portId, 'client', clientId); const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED'); expect(directGroup).toBeDefined(); for (const f of directGroup!.files) { expect(f).toHaveProperty('signedFromDocumentId'); expect(f.signedFromDocumentId).toBeNull(); } }); }); describe('caps each group at 20 rows + surfaces total', () => { let portId: string; let clientId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; const client = await makeClient({ portId }); clientId = client.id; // Insert 25 files all with clientId await Promise.all(Array.from({ length: 25 }, () => insertFile(portId, { clientId }))); }); it('DIRECTLY ATTACHED group has 20 files and total=25', async () => { const result = await listFilesAggregatedByEntity(portId, 'client', clientId); const group = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED'); expect(group).toBeDefined(); expect(group!.files).toHaveLength(20); expect(group!.total).toBe(25); }); }); describe('file-FK snapshot survives yacht transfer', () => { let portId: string; let johnId: string; let maryId: string; let yachtId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; const john = await makeClient({ portId, overrides: { fullName: 'John Smith' } }); johnId = john.id; const mary = await makeClient({ portId, overrides: { fullName: 'Mary Jones' } }); maryId = mary.id; const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: johnId }); yachtId = yacht.id; // File attached to the yacht at the time john owns it await insertFile(portId, { yachtId, clientId: johnId }); // Transfer yacht to Mary (update currentOwner in place — simulates transfer) await db .update(yachts) .set({ currentOwnerType: 'client', currentOwnerId: maryId }) .where(and(eq(yachts.id, yachtId), eq(yachts.portId, portId))); }); it("John's view still shows the file via yachtId FK", async () => { const result = await listFilesAggregatedByEntity(portId, 'client', johnId); // clientId=johnId → DIRECTLY ATTACHED group const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED'); expect(directGroup).toBeDefined(); expect(directGroup!.files.length).toBeGreaterThan(0); }); it("Mary's view does NOT see john's file (it has clientId=john, not mary)", async () => { const result = await listFilesAggregatedByEntity(portId, 'client', maryId); // Mary owns the yacht now, so FROM YACHT group will appear — but the // file has clientId=johnId (snapshotted FK), so it WON'T appear under // Mary's DIRECTLY ATTACHED. The FROM YACHT group WILL appear since the // file still has yachtId set. // The key invariant: there is no DIRECTLY ATTACHED for Mary. const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED'); expect(directGroup).toBeUndefined(); }); }); describe('cross-port leakage rejected', () => { let portId: string; let otherPortId: string; let otherClientId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; const otherPort = await makePort(); otherPortId = otherPort.id; const otherClient = await makeClient({ portId: otherPortId }); otherClientId = otherClient.id; // File for other client in other port await insertFile(otherPortId, { clientId: otherClientId }); }); it('returns empty groups when entity belongs to a different port', async () => { const result = await listFilesAggregatedByEntity(portId, 'client', otherClientId); expect(result.groups).toHaveLength(0); }); }); describe('signedFromDocumentId reverse-link', () => { let portId: string; let clientId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; const client = await makeClient({ portId }); clientId = client.id; }); it('surfaces signedFromDocumentId when a workflow references the file', async () => { const fileRow = await insertFile(portId, { clientId }); // Insert a document row with signedFileId pointing at the file const [doc] = await db .insert(documents) .values({ portId, clientId, documentType: 'contract', title: 'Signed Contract', status: 'completed', signedFileId: fileRow.id, createdBy: TEST_USER_ID, }) .returning(); const result = await listFilesAggregatedByEntity(portId, 'client', clientId); const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED'); expect(directGroup).toBeDefined(); const linkedFile = directGroup!.files.find((f) => f.id === fileRow.id); expect(linkedFile).toBeDefined(); expect(linkedFile!.signedFromDocumentId).toBe(doc!.id); }); }); }); // ─── listInflightWorkflowsAggregatedByEntity ───────────────────────────────── describe('documents service · listInflightWorkflowsAggregatedByEntity', () => { let portId: string; let clientId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; const client = await makeClient({ portId }); clientId = client.id; }); it('returns in-flight workflows in DIRECTLY ATTACHED group, hides completed', async () => { // Insert two documents: one in-flight (status='sent'), one completed await db.insert(documents).values([ { portId, clientId, documentType: 'contract', title: 'In-flight Doc', status: 'sent', createdBy: TEST_USER_ID, }, { portId, clientId, documentType: 'contract', title: 'Completed Doc', status: 'completed', createdBy: TEST_USER_ID, }, ]); const result = await listInflightWorkflowsAggregatedByEntity(portId, 'client', clientId); const directGroup = result.groups.find((g) => g.label === 'DIRECTLY ATTACHED'); expect(directGroup).toBeDefined(); expect(directGroup!.workflows).toHaveLength(1); expect(directGroup!.workflows[0]!.status).toBe('sent'); expect(directGroup!.workflows[0]!.title).toBe('In-flight Doc'); }); it('rejects cross-port leakage with defense-in-depth port filter', async () => { const otherPort = await makePort(); const [otherClient] = await db .insert(clients) .values({ portId: otherPort.id, fullName: 'Other Port Client' }) .returning(); const result = await listInflightWorkflowsAggregatedByEntity(portId, 'client', otherClient!.id); expect(result.groups).toEqual([]); }); }); // ─── applyEntityFkFromFolder ────────────────────────────────────────────────── describe('files service · applyEntityFkFromFolder', () => { let portId: string; let clientId: string; let entityFolderId: string; beforeEach(async () => { const port = await makePort(); portId = port.id; await db.delete(documentFolders).where(eq(documentFolders.portId, portId)); await ensureSystemRoots(portId, TEST_USER_ID); const client = await makeClient({ portId }); clientId = client.id; const folder = await ensureEntityFolder(portId, 'client', clientId, TEST_USER_ID); entityFolderId = folder.id; }); it('sets clientId when uploading into a client entity folder', async () => { const out = await applyEntityFkFromFolder(portId, { folderId: entityFolderId, clientId: null }); expect(out.clientId).toBe(clientId); }); it('preserves existing entity FK when already set', async () => { const out = await applyEntityFkFromFolder(portId, { folderId: entityFolderId, clientId: 'pre-existing-id', }); expect(out.clientId).toBe('pre-existing-id'); }); it('is a no-op for non-system folders', async () => { const [userFolder] = await db .insert(documentFolders) .values({ portId, parentId: null, name: 'My templates', createdBy: TEST_USER_ID, }) .returning(); const out = await applyEntityFkFromFolder(portId, { folderId: userFolder!.id, clientId: null, }); expect(out.clientId).toBeNull(); }); });