Skip to content

Commit 26729d2

Browse files
committed
test(plugin-auth): pin invite-member's unknown-role refusal code (#24750 repro, red)
Claude-Session: https://claude.ai/code/session_01SKHckexWmsVZ6qZ1sioSkD Co-authored-by: Claude <noreply@anthropic.com>
1 parent c56115b commit 26729d2

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

‎packages/plugins/plugin-auth/src/accept-invitation-adopt-membership.test.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,3 +448,70 @@ describe('#7725 — the delegated-admin issuance scope is UNCHANGED (ADR-0090 D1
448448
expect(invitationsFor(engine, 'scoped.denied@example.com')).toHaveLength(0);
449449
});
450450
});
451+
452+
describe('#24750 — invite-member refuses an unknown role with the ADR-0112 code ROLE_NOT_FOUND', () => {
453+
// The spec's invitation `role` docstring (`identity/organization.zod.ts`)
454+
// promises "refused at the door (`ROLE_NOT_FOUND`) before any invitation row
455+
// exists". better-auth's invite route throws that refusal with a message and
456+
// no `code`; `update-member-role` throws the same refusal WITH one. These pin
457+
// the door's answer, the untouched valid-role path, and the control door.
458+
beforeEach(() => {
459+
vi.spyOn(console, 'info').mockImplementation(() => {});
460+
vi.spyOn(console, 'warn').mockImplementation(() => {});
461+
});
462+
463+
afterEach(() => {
464+
vi.restoreAllMocks();
465+
});
466+
467+
it('an unknown role is refused 400 with code ROLE_NOT_FOUND, the message names the role, and no invitation row exists', async () => {
468+
const { engine, manager, owner } = await bootWithOwner();
469+
470+
const res = await post(
471+
manager,
472+
'/organization/invite-member',
473+
{ email: 'qa-invitee@example.com', role: 'no_such_role', organizationId: DEFAULT_ORG },
474+
owner.cookie,
475+
);
476+
expect(res.status).toBe(400);
477+
const body: any = await res.json();
478+
expect(body.code).toBe('ROLE_NOT_FOUND');
479+
expect(body.message).toContain('no_such_role');
480+
expect(invitationsFor(engine, 'qa-invitee@example.com')).toHaveLength(0);
481+
});
482+
483+
it('CONTROL: a valid role is unchanged — 200 and one pending row carrying that role', async () => {
484+
const { engine, manager, owner } = await bootWithOwner();
485+
486+
const res = await post(
487+
manager,
488+
'/organization/invite-member',
489+
{ email: 'qa-valid@example.com', role: 'member', organizationId: DEFAULT_ORG },
490+
owner.cookie,
491+
);
492+
expect(res.status, await res.clone().text()).toBe(200);
493+
const rows = invitationsFor(engine, 'qa-valid@example.com');
494+
expect(rows).toHaveLength(1);
495+
expect(rows[0]!.status).toBe('pending');
496+
expect(rows[0]!.role).toBe('member');
497+
});
498+
499+
it('CONTROL: update-member-role keeps the code it already answered with — 400 ROLE_NOT_FOUND, role unchanged', async () => {
500+
const { engine, manager, owner } = await bootWithOwner();
501+
const colleague = await signUp(manager, engine, 'colleague@example.com');
502+
const [membership] = membersOf(engine, DEFAULT_ORG, colleague.userId);
503+
expect(membership, 'the reconciler did not bind the colleague').toBeDefined();
504+
505+
const res = await post(
506+
manager,
507+
'/organization/update-member-role',
508+
{ memberId: String(membership!.id), role: 'no_such_role', organizationId: DEFAULT_ORG },
509+
owner.cookie,
510+
);
511+
expect(res.status).toBe(400);
512+
const body: any = await res.json();
513+
expect(body.code).toBe('ROLE_NOT_FOUND');
514+
expect(body.message).toContain('no_such_role');
515+
expect(membersOf(engine, DEFAULT_ORG, colleague.userId)[0]!.role).toBe('member');
516+
});
517+
});

0 commit comments

Comments
 (0)