test(server): batch 5 authorization, transclusion, search & comment coverage
Test-only. Fills the authorization / data-integrity gaps from the strategy report. Full server suite: 100 suites / 1031 passed + 1 todo, green. Authorization (privilege-escalation catches): - workspace/space ability factories: exact can/cannot per (action,subject) — admin cannot Manage Audit, writer/reader cannot Manage Settings/Member, etc. - findHighestUserSpaceRole, isAdminActingOnOwner. - WorkspaceService role guards: last-owner lockout, admin-over-owner, self-target. - SpaceMemberService.validateLastAdmin: never orphan a space without an admin. - GroupService: default-group immutability, name uniqueness. Access / data integrity: - PageAccessService: restriction-vs-space-ability branches for view/edit/comment. - TransclusionService.unsyncReference: cross-workspace/NotFound boundary asserts NO attachment write or ref-row delete on rejection; lookupWithAccessSet positional status mapping; listReferences drops private/cross-ws/deleted refs; syncPageTransclusions/References diff (no-op on unchanged content). - SearchService.searchPage: query-mode scoping; leakage modes return empty before executing the query. - CommentService: reply-to-reply guard, agent provenance, self-mention filter, no double-notify. Pure helpers: - prosemirror extractors (mention dedup-key id-vs-entityId, attachment UUID validation, removeMarkTypeFromDoc), collaboration.util (getPageId, isEmptyParagraphDoc, stripUnknownNodes unwrap, prosemirrorNodeToYElement). Reviewed (APPROVE WITH SUGGESTIONS): mutation-resistant, not vacuous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
getAttachmentFolderPath,
|
||||
validateFileType,
|
||||
} from './attachment.utils';
|
||||
import { AttachmentType } from './attachment.constants';
|
||||
|
||||
// Pins where each AttachmentType is stored and the file-type allow-list.
|
||||
// A wrong folder mapping would scatter uploads (e.g. avatars landing in /files),
|
||||
// and a broken validateFileType would let disallowed extensions bypass the
|
||||
// check, so we assert the exact path per type and the throw/no-throw behaviour.
|
||||
|
||||
const WORKSPACE = 'ws-123';
|
||||
|
||||
describe('getAttachmentFolderPath', () => {
|
||||
it('maps Avatar to <workspaceId>/avatars', () => {
|
||||
expect(getAttachmentFolderPath(AttachmentType.Avatar, WORKSPACE)).toBe(
|
||||
`${WORKSPACE}/avatars`,
|
||||
);
|
||||
});
|
||||
|
||||
it('maps WorkspaceIcon to <workspaceId>/workspace-logos', () => {
|
||||
expect(
|
||||
getAttachmentFolderPath(AttachmentType.WorkspaceIcon, WORKSPACE),
|
||||
).toBe(`${WORKSPACE}/workspace-logos`);
|
||||
});
|
||||
|
||||
it('maps SpaceIcon to <workspaceId>/space-logos', () => {
|
||||
expect(getAttachmentFolderPath(AttachmentType.SpaceIcon, WORKSPACE)).toBe(
|
||||
`${WORKSPACE}/space-logos`,
|
||||
);
|
||||
});
|
||||
|
||||
it('maps File to <workspaceId>/files', () => {
|
||||
expect(getAttachmentFolderPath(AttachmentType.File, WORKSPACE)).toBe(
|
||||
`${WORKSPACE}/files`,
|
||||
);
|
||||
});
|
||||
|
||||
it('maps Chat to <workspaceId>/chat-files', () => {
|
||||
expect(getAttachmentFolderPath(AttachmentType.Chat, WORKSPACE)).toBe(
|
||||
`${WORKSPACE}/chat-files`,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to <workspaceId>/files for an unknown type', () => {
|
||||
expect(
|
||||
getAttachmentFolderPath('totally-unknown' as AttachmentType, WORKSPACE),
|
||||
).toBe(`${WORKSPACE}/files`);
|
||||
});
|
||||
|
||||
it('covers every AttachmentType enum value with a non-fallback folder except File', () => {
|
||||
// Guards against a new AttachmentType silently inheriting the /files default.
|
||||
const expected: Record<AttachmentType, string> = {
|
||||
[AttachmentType.Avatar]: `${WORKSPACE}/avatars`,
|
||||
[AttachmentType.WorkspaceIcon]: `${WORKSPACE}/workspace-logos`,
|
||||
[AttachmentType.SpaceIcon]: `${WORKSPACE}/space-logos`,
|
||||
[AttachmentType.File]: `${WORKSPACE}/files`,
|
||||
[AttachmentType.Chat]: `${WORKSPACE}/chat-files`,
|
||||
};
|
||||
|
||||
for (const type of Object.values(AttachmentType)) {
|
||||
expect(getAttachmentFolderPath(type, WORKSPACE)).toBe(expected[type]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFileType', () => {
|
||||
const allowed = ['.png', '.jpg', '.jpeg'];
|
||||
|
||||
it('does not throw when the extension is in the allow-list', () => {
|
||||
expect(() => validateFileType('.png', allowed)).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws "Invalid file type" when the extension is not allowed', () => {
|
||||
expect(() => validateFileType('.exe', allowed)).toThrow('Invalid file type');
|
||||
});
|
||||
|
||||
it('is case-sensitive on the extension (uppercase is rejected)', () => {
|
||||
// The check uses Array.includes with no normalization, so ".PNG" !== ".png".
|
||||
expect(() => validateFileType('.PNG', allowed)).toThrow('Invalid file type');
|
||||
});
|
||||
|
||||
it('throws against an empty allow-list', () => {
|
||||
expect(() => validateFileType('.png', [])).toThrow('Invalid file type');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user