orgs-authz.test.ts43 lines · main
| 1 | // apps/api/src/routes/orgs-authz.test.ts |
| 2 | // |
| 3 | // Zone-2 regression test for the org-member-mutation privilege-escalation fix. |
| 4 | // |
| 5 | // Bug: PATCH /v1/orgs/:id/members/:userId (role change), DELETE …/members/:userId |
| 6 | // (member removal) and POST …/invitations gated on `isOrgMember` ONLY — so any |
| 7 | // member (including a viewer) could change roles, remove the owner, or invite. |
| 8 | // |
| 9 | // Fix: those three routes now gate on the caller's org role via |
| 10 | // `getOrgRole(...)` + `hasRoleAtLeast(role, 'admin')` — a viewer/developer is |
| 11 | // rejected, admin/owner allowed. This test pins that exact predicate (the |
| 12 | // route's authorisation decision) without needing a DB. |
| 13 | |
| 14 | import { describe, expect, it } from 'bun:test'; |
| 15 | |
| 16 | import { hasRoleAtLeast } from '../services/access.js'; |
| 17 | import { orgRole } from '../db/schema.js'; |
| 18 | |
| 19 | describe('org-member-mutation admin gate', () => { |
| 20 | // Mirrors the route guard: `!actorRole || !hasRoleAtLeast(actorRole, 'admin')` |
| 21 | const allowed = (role: (typeof orgRole)[number] | null): boolean => |
| 22 | Boolean(role) && hasRoleAtLeast(role as (typeof orgRole)[number], 'admin'); |
| 23 | |
| 24 | it('denies a viewer', () => { |
| 25 | expect(allowed('viewer')).toBe(false); |
| 26 | }); |
| 27 | |
| 28 | it('denies a developer', () => { |
| 29 | expect(allowed('developer')).toBe(false); |
| 30 | }); |
| 31 | |
| 32 | it('allows an admin', () => { |
| 33 | expect(allowed('admin')).toBe(true); |
| 34 | }); |
| 35 | |
| 36 | it('allows an owner', () => { |
| 37 | expect(allowed('owner')).toBe(true); |
| 38 | }); |
| 39 | |
| 40 | it('denies a non-member (null role)', () => { |
| 41 | expect(allowed(null)).toBe(false); |
| 42 | }); |
| 43 | }); |