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>
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { findHighestUserSpaceRole } from './utils';
|
|
import { SpaceRole } from '../../../common/helpers/types/permission';
|
|
import { UserSpaceRole } from './types';
|
|
|
|
// Pins the space-role precedence used by SpaceAbilityFactory: ADMIN (3) >
|
|
// WRITER (2) > READER (1). A precedence inversion would let a writer/reader be
|
|
// resolved as the highest role and silently gain admin/writer abilities, so we
|
|
// assert the exact winning role for mixed inputs regardless of array order.
|
|
|
|
const role = (r: SpaceRole): UserSpaceRole => ({ userId: 'u1', role: r });
|
|
|
|
describe('findHighestUserSpaceRole', () => {
|
|
it('returns ADMIN as the highest among reader, admin, writer', () => {
|
|
const roles = [
|
|
role(SpaceRole.READER),
|
|
role(SpaceRole.ADMIN),
|
|
role(SpaceRole.WRITER),
|
|
];
|
|
|
|
expect(findHighestUserSpaceRole(roles)).toBe(SpaceRole.ADMIN);
|
|
});
|
|
|
|
it('returns WRITER over READER', () => {
|
|
const roles = [role(SpaceRole.READER), role(SpaceRole.WRITER)];
|
|
|
|
expect(findHighestUserSpaceRole(roles)).toBe(SpaceRole.WRITER);
|
|
});
|
|
|
|
it('is independent of array order (admin last still wins)', () => {
|
|
const roles = [role(SpaceRole.WRITER), role(SpaceRole.ADMIN)];
|
|
|
|
expect(findHighestUserSpaceRole(roles)).toBe(SpaceRole.ADMIN);
|
|
});
|
|
|
|
it('returns the only role when a single membership is present', () => {
|
|
expect(findHighestUserSpaceRole([role(SpaceRole.READER)])).toBe(
|
|
SpaceRole.READER,
|
|
);
|
|
expect(findHighestUserSpaceRole([role(SpaceRole.WRITER)])).toBe(
|
|
SpaceRole.WRITER,
|
|
);
|
|
expect(findHighestUserSpaceRole([role(SpaceRole.ADMIN)])).toBe(
|
|
SpaceRole.ADMIN,
|
|
);
|
|
});
|
|
|
|
it('returns undefined for an empty array (no membership)', () => {
|
|
expect(findHighestUserSpaceRole([])).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when given null', () => {
|
|
expect(findHighestUserSpaceRole(null as any)).toBeUndefined();
|
|
});
|
|
|
|
it('returns undefined when given undefined', () => {
|
|
expect(findHighestUserSpaceRole(undefined as any)).toBeUndefined();
|
|
});
|
|
});
|