Files
pn-new-crm/tests/integration/document-folders-crud.test.ts
Matt 5c5ab49218 fix(documents): port-scope folder test cleanup + tighten parent-validation message
Code-review followups on 4b31f01:
- beforeEach now scopes the documentFolders cleanup to the test port
  via .where(eq(documentFolders.portId, portId)) so parallel suites
  don't wipe each other's fixtures.
- Cross-port parent guard message changed from "Parent folder not
  found in this port" (read like a 404) to "Invalid parent folder"
  to match the ValidationError type that already maps to 400.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:36:31 +02:00

99 lines
3.5 KiB
TypeScript

/**
* Task 3 — document-folders service: listTree + createFolder (TDD).
*
* Uses the makePort factory (not a "setupTestPort" helper — that name
* doesn't exist in this codebase). TEST_USER_ID is resolved once via
* beforeAll from any seeded user, matching the pattern in
* alerts-tenant-isolation.test.ts and gdpr-export.test.ts.
*/
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { eq } from 'drizzle-orm';
import { db } from '@/lib/db';
import { documentFolders } from '@/lib/db/schema/documents';
import { user } from '@/lib/db/schema/users';
import { listTree, createFolder } from '@/lib/services/document-folders.service';
import { makePort } 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;
});
describe('document-folders service · listTree', () => {
let portId: string;
beforeEach(async () => {
const port = await makePort();
portId = port.id;
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
});
it('returns an empty array when no folders exist', async () => {
const tree = await listTree(portId);
expect(tree).toEqual([]);
});
it('returns root folders with children nested under them', async () => {
const root = await createFolder(portId, TEST_USER_ID, { name: 'Deals 2026', parentId: null });
const child = await createFolder(portId, TEST_USER_ID, {
name: 'Q1',
parentId: root.id,
});
const tree = await listTree(portId);
expect(tree).toHaveLength(1);
expect(tree[0]?.id).toBe(root.id);
expect(tree[0]?.children).toHaveLength(1);
expect(tree[0]?.children[0]?.id).toBe(child.id);
});
it('only returns folders for the requested port', async () => {
const otherPort = await makePort();
await createFolder(otherPort.id, TEST_USER_ID, { name: 'Other Port', parentId: null });
const tree = await listTree(portId);
expect(tree).toEqual([]);
});
});
describe('document-folders service · createFolder unique-sibling guard', () => {
let portId: string;
beforeEach(async () => {
const port = await makePort();
portId = port.id;
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
});
it('rejects a duplicate sibling name (case-insensitive)', async () => {
await createFolder(portId, TEST_USER_ID, { name: 'Deals 2026', parentId: null });
await expect(
createFolder(portId, TEST_USER_ID, { name: 'deals 2026', parentId: null }),
).rejects.toThrow(/already exists/i);
});
it('allows the same name under different parents', async () => {
const a = await createFolder(portId, TEST_USER_ID, { name: 'A', parentId: null });
const b = await createFolder(portId, TEST_USER_ID, { name: 'B', parentId: null });
await createFolder(portId, TEST_USER_ID, { name: 'Drafts', parentId: a.id });
await expect(
createFolder(portId, TEST_USER_ID, { name: 'Drafts', parentId: b.id }),
).resolves.toBeDefined();
});
it('rejects a parentId from another port', async () => {
const otherPort = await makePort();
const otherFolder = await createFolder(otherPort.id, TEST_USER_ID, {
name: 'Other',
parentId: null,
});
await expect(
createFolder(portId, TEST_USER_ID, { name: 'Should fail', parentId: otherFolder.id }),
).rejects.toThrow(/invalid parent/i);
});
});