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:
claude_code
2026-06-21 18:40:07 +03:00
parent 0b2af34029
commit 4df79aafd3
18 changed files with 3846 additions and 0 deletions
@@ -0,0 +1,200 @@
import { BadRequestException } from '@nestjs/common';
import { GroupService } from './group.service';
// Direct-instantiation unit tests for GroupService's integrity guards:
// - the DEFAULT (system) group cannot be updated or deleted;
// - group names are unique on create and on rename;
// - renaming a group to its OWN current name is allowed (no false positive).
// Each rejection test also asserts that no destructive repo write fired.
//
// Constructor arg order (8 positional deps) is pinned: groupRepo, groupUserRepo,
// spaceMemberRepo, groupUserService, watcherRepo, favoriteRepo, db,
// auditService.
const WORKSPACE_ID = 'ws-1';
function buildService(opts?: {
// group returned by groupRepo.findById (the target being updated/deleted)
group?: any;
// group returned by groupRepo.findByName (a name-collision probe)
byName?: any;
}) {
const groupRepo = {
findById: jest.fn().mockResolvedValue(opts?.group ?? null),
findByName: jest.fn().mockResolvedValue(opts?.byName ?? null),
insertGroup: jest
.fn()
.mockResolvedValue({ id: 'g-new', name: 'New Group', description: null }),
update: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
};
const groupUserRepo = {
getUserIdsByGroupId: jest.fn().mockResolvedValue([]),
};
const spaceMemberRepo = {
getSpaceIdsByGroupId: jest.fn().mockResolvedValue([]),
};
const groupUserService = {
addUsersToGroupBatch: jest.fn().mockResolvedValue(undefined),
};
const watcherRepo = {
deleteByUsersWithoutSpaceAccess: jest.fn().mockResolvedValue(undefined),
};
const favoriteRepo = {
deleteByUsersWithoutSpaceAccess: jest.fn().mockResolvedValue(undefined),
};
const db = {
transaction: jest.fn().mockReturnValue({
execute: jest.fn(async (cb: any) => cb({} as any)),
}),
};
const auditService = { log: jest.fn() };
const service = new GroupService(
groupRepo as any, // groupRepo
groupUserRepo as any, // groupUserRepo
spaceMemberRepo as any, // spaceMemberRepo
groupUserService as any, // groupUserService
watcherRepo as any, // watcherRepo
favoriteRepo as any, // favoriteRepo
db as any, // db
auditService as any, // auditService
);
return { service, groupRepo, auditService };
}
const authUser = { id: 'auth-1' } as any;
describe('GroupService.createGroup duplicate-name guard', () => {
it('rejects creating a group with an existing name (no insert)', async () => {
const { service, groupRepo } = buildService({
byName: { id: 'g-existing', name: 'Engineering' },
});
await expect(
service.createGroup(authUser, WORKSPACE_ID, {
name: 'Engineering',
} as any),
).rejects.toBeInstanceOf(BadRequestException);
expect(groupRepo.insertGroup).not.toHaveBeenCalled();
});
it('creates a group when the name is free', async () => {
const { service, groupRepo } = buildService({ byName: null });
await service.createGroup(authUser, WORKSPACE_ID, {
name: 'Engineering',
} as any);
expect(groupRepo.insertGroup).toHaveBeenCalledTimes(1);
// isDefault must always be false for a user-created group.
expect(groupRepo.insertGroup.mock.calls[0][0]).toMatchObject({
name: 'Engineering',
isDefault: false,
workspaceId: WORKSPACE_ID,
});
});
});
describe('GroupService.updateGroup guards', () => {
it('rejects updating a DEFAULT group with BadRequest (no update)', async () => {
const { service, groupRepo } = buildService({
group: {
id: 'g-default',
name: 'Everyone',
description: null,
isDefault: true,
},
});
await expect(
service.updateGroup(WORKSPACE_ID, {
groupId: 'g-default',
name: 'Renamed',
} as any),
).rejects.toBeInstanceOf(BadRequestException);
expect(groupRepo.update).not.toHaveBeenCalled();
});
it('rejects renaming to a name owned by a DIFFERENT group (no update)', async () => {
const { service, groupRepo } = buildService({
group: {
id: 'g-1',
name: 'Engineering',
description: null,
isDefault: false,
},
// A different group already holds the target name.
byName: { id: 'g-2', name: 'Design' },
});
await expect(
service.updateGroup(WORKSPACE_ID, {
groupId: 'g-1',
name: 'Design',
} as any),
).rejects.toBeInstanceOf(BadRequestException);
expect(groupRepo.update).not.toHaveBeenCalled();
});
it('allows renaming a group to its OWN current name (no false collision)', async () => {
// findByName returns the same group; group.name === existingGroup.name, so
// the duplicate guard must NOT fire.
const sameGroup = {
id: 'g-1',
name: 'Engineering',
description: null,
isDefault: false,
};
const { service, groupRepo } = buildService({
group: { ...sameGroup },
byName: { ...sameGroup },
});
await service.updateGroup(WORKSPACE_ID, {
groupId: 'g-1',
name: 'Engineering',
} as any);
expect(groupRepo.update).toHaveBeenCalledTimes(1);
});
});
describe('GroupService.deleteGroup guard', () => {
it('rejects deleting a DEFAULT group with BadRequest (no delete)', async () => {
const { service, groupRepo } = buildService({
group: {
id: 'g-default',
name: 'Everyone',
description: null,
isDefault: true,
},
});
await expect(
service.deleteGroup('g-default', WORKSPACE_ID),
).rejects.toBeInstanceOf(BadRequestException);
expect(groupRepo.delete).not.toHaveBeenCalled();
});
it('deletes a non-default group', async () => {
const { service, groupRepo } = buildService({
group: {
id: 'g-1',
name: 'Engineering',
description: null,
isDefault: false,
},
});
await service.deleteGroup('g-1', WORKSPACE_ID);
expect(groupRepo.delete).toHaveBeenCalledTimes(1);
});
});