feat(documents): block rename/move/delete on system folders
assertNotSystemManaged centralises the guard so the three mutation paths surface identical ConflictError shapes. System roots and per- entity subfolders are immutable through the rep-facing API; the only way for system_managed to flip back to false is the entity-hard- delete demotion path (next task). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,18 +104,13 @@ describe('document-folders service · ensureEntityFolder', () => {
|
||||
const all = await db
|
||||
.select()
|
||||
.from(documentFolders)
|
||||
.where(
|
||||
and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)),
|
||||
);
|
||||
.where(and(eq(documentFolders.entityType, 'client'), eq(documentFolders.entityId, clientId)));
|
||||
expect(all).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('appends a numeric suffix on name collision with an existing folder', async () => {
|
||||
// Insert a second client with the exact same fullName as the first.
|
||||
const [firstClient] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.id, clientId));
|
||||
const [firstClient] = await db.select().from(clients).where(eq(clients.id, clientId));
|
||||
const sharedName = firstClient!.fullName;
|
||||
|
||||
const [collidingClient] = await db
|
||||
@@ -138,3 +133,58 @@ describe('document-folders service · ensureEntityFolder', () => {
|
||||
).rejects.toThrow(/entity type/i);
|
||||
});
|
||||
});
|
||||
|
||||
import {
|
||||
deleteFolderSoftRescue,
|
||||
moveFolder,
|
||||
renameFolder,
|
||||
} from '@/lib/services/document-folders.service';
|
||||
|
||||
describe('document-folders service · system folder protection', () => {
|
||||
let portId: string;
|
||||
let rootId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const port = await makePort();
|
||||
portId = port.id;
|
||||
await db.delete(documentFolders).where(eq(documentFolders.portId, portId));
|
||||
const roots = await ensureSystemRoots(portId, TEST_USER_ID);
|
||||
rootId = roots.find((r) => r.name === 'Clients')!.id;
|
||||
});
|
||||
|
||||
it('rejects rename of a system-managed root', async () => {
|
||||
await expect(renameFolder(portId, rootId, 'Customers', TEST_USER_ID)).rejects.toThrow(
|
||||
/system folder/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects move of a system-managed root', async () => {
|
||||
const other = await ensureSystemRoots(portId, TEST_USER_ID);
|
||||
const companies = other.find((r) => r.name === 'Companies')!;
|
||||
await expect(moveFolder(portId, rootId, companies.id, TEST_USER_ID)).rejects.toThrow(
|
||||
/system folder/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects delete of a system-managed root', async () => {
|
||||
await expect(deleteFolderSoftRescue(portId, rootId, TEST_USER_ID)).rejects.toThrow(
|
||||
/system folder/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('allows rename/delete of a user folder under a system root', async () => {
|
||||
const user = await db
|
||||
.insert(documentFolders)
|
||||
.values({
|
||||
portId,
|
||||
parentId: rootId,
|
||||
name: 'Templates',
|
||||
systemManaged: false,
|
||||
createdBy: TEST_USER_ID,
|
||||
})
|
||||
.returning();
|
||||
await expect(
|
||||
renameFolder(portId, user[0]!.id, 'My Templates', TEST_USER_ID),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user