feat(threads): governed group settings — rename, list members, remove participant
- MessageService.renameThread / removeParticipant / listParticipants, each fail-closed via OPA (kernel stays generic — no dm/group branching) - dev OPA: iios.thread.update + iios.thread.participant.remove rules (group requires ADMIN; ungoverned threads unchanged) - REST: PATCH /v1/threads/:id, GET + DELETE /v1/threads/:id/participants - tests: admin renames/removes, member is denied, member list carries roles (message.spec + dev-opa.port.spec) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -124,6 +124,28 @@ describe('Governed membership + replies (v1.1, policy-enforced)', () => {
|
||||
await expect(s.addParticipant(threadId, bob, 'carol')).rejects.toBeInstanceOf(PolicyDeniedError); // bob is MEMBER
|
||||
});
|
||||
|
||||
it('group settings: admin renames + lists members + removes; a plain member cannot rename/remove', async () => {
|
||||
const s = gov();
|
||||
const { threadId } = await s.openThread(null, alice, { membership: 'group', creatorRole: 'ADMIN', subject: 'Design' });
|
||||
await s.addParticipant(threadId, alice, 'bob');
|
||||
|
||||
// admin renames
|
||||
expect((await s.renameThread(threadId, alice, 'Design Team')).subject).toBe('Design Team');
|
||||
// a plain member cannot rename
|
||||
await expect(s.renameThread(threadId, bob, 'Hacked')).rejects.toBeInstanceOf(PolicyDeniedError);
|
||||
|
||||
// member list carries roles
|
||||
const members = await s.listParticipants(threadId, alice);
|
||||
expect(members.map((m) => m.userId).sort()).toEqual(['alice', 'bob']);
|
||||
expect(members.find((m) => m.userId === 'alice')?.role).toBe('ADMIN');
|
||||
|
||||
// a plain member cannot remove
|
||||
await expect(s.removeParticipant(threadId, bob, 'alice')).rejects.toBeInstanceOf(PolicyDeniedError);
|
||||
// admin removes bob
|
||||
expect((await s.removeParticipant(threadId, alice, 'bob')).participantCount).toBe(1);
|
||||
expect(await prisma.iiosThreadParticipant.count({ where: { threadId } })).toBe(1);
|
||||
});
|
||||
|
||||
it('self-join is governed: a non-member cannot open a thread by id; after being added, they can', async () => {
|
||||
const s = gov();
|
||||
const { threadId } = await s.openThread(null, alice, { membership: 'group', creatorRole: 'ADMIN' });
|
||||
|
||||
Reference in New Issue
Block a user