Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
177 lines
6.6 KiB
TypeScript
177 lines
6.6 KiB
TypeScript
/**
|
|
* Task 3 - document-folders service: listTree + createFolder (TDD).
|
|
* Task 4 - renameFolder + moveFolder (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,
|
|
renameFolder,
|
|
moveFolder,
|
|
} 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);
|
|
});
|
|
});
|
|
|
|
describe('document-folders service · renameFolder', () => {
|
|
let portId: string;
|
|
|
|
beforeEach(async () => {
|
|
const port = await makePort();
|
|
portId = port.id;
|
|
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
|
|
});
|
|
|
|
it('renames a folder and bumps updatedAt', async () => {
|
|
const folder = await createFolder(portId, TEST_USER_ID, { name: 'Old', parentId: null });
|
|
const before = folder.updatedAt.getTime();
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
const renamed = await renameFolder(portId, folder.id, 'New', TEST_USER_ID);
|
|
expect(renamed.name).toBe('New');
|
|
expect(renamed.updatedAt.getTime()).toBeGreaterThan(before);
|
|
});
|
|
|
|
it('rejects rename to an existing sibling name', async () => {
|
|
await createFolder(portId, TEST_USER_ID, { name: 'Existing', parentId: null });
|
|
const folder = await createFolder(portId, TEST_USER_ID, { name: 'Mine', parentId: null });
|
|
await expect(renameFolder(portId, folder.id, 'Existing', TEST_USER_ID)).rejects.toThrow(
|
|
/already exists/i,
|
|
);
|
|
});
|
|
|
|
it('throws NotFound when the folder belongs to another port', async () => {
|
|
const otherPort = await makePort();
|
|
const folder = await createFolder(otherPort.id, TEST_USER_ID, { name: 'X', parentId: null });
|
|
await expect(renameFolder(portId, folder.id, 'Y', TEST_USER_ID)).rejects.toThrow(
|
|
/couldn't find/i,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('document-folders service · moveFolder', () => {
|
|
let portId: string;
|
|
|
|
beforeEach(async () => {
|
|
const port = await makePort();
|
|
portId = port.id;
|
|
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
|
|
});
|
|
|
|
it('moves a folder under a new parent', async () => {
|
|
const root = await createFolder(portId, TEST_USER_ID, { name: 'Root', parentId: null });
|
|
const orphan = await createFolder(portId, TEST_USER_ID, { name: 'Orphan', parentId: null });
|
|
const moved = await moveFolder(portId, orphan.id, root.id, TEST_USER_ID);
|
|
expect(moved.parentId).toBe(root.id);
|
|
});
|
|
|
|
it('moves a folder back to root with parentId=null', async () => {
|
|
const root = await createFolder(portId, TEST_USER_ID, { name: 'Root', parentId: null });
|
|
const child = await createFolder(portId, TEST_USER_ID, { name: 'Child', parentId: root.id });
|
|
const moved = await moveFolder(portId, child.id, null, TEST_USER_ID);
|
|
expect(moved.parentId).toBeNull();
|
|
});
|
|
|
|
it('rejects a move that would create a cycle', async () => {
|
|
const a = await createFolder(portId, TEST_USER_ID, { name: 'A', parentId: null });
|
|
const b = await createFolder(portId, TEST_USER_ID, { name: 'B', parentId: a.id });
|
|
const c = await createFolder(portId, TEST_USER_ID, { name: 'C', parentId: b.id });
|
|
// moving A under C would create A → B → C → A
|
|
await expect(moveFolder(portId, a.id, c.id, TEST_USER_ID)).rejects.toThrow(/cycle/i);
|
|
});
|
|
|
|
it('rejects moving a folder under itself', async () => {
|
|
const a = await createFolder(portId, TEST_USER_ID, { name: 'A', parentId: null });
|
|
await expect(moveFolder(portId, a.id, a.id, TEST_USER_ID)).rejects.toThrow(/cycle/i);
|
|
});
|
|
});
|