access.test.ts93 lines · main
1import { describe, expect, test } from 'bun:test';
2
3import { effectiveRole, hasRoleAtLeast } from './access.js';
4
5describe('effectiveRole', () => {
6 test('returns null when both roles are null', () => {
7 expect(effectiveRole(null, null)).toBeNull();
8 });
9
10 test('returns the project role when only project role is set', () => {
11 expect(effectiveRole(null, 'admin')).toBe('admin');
12 expect(effectiveRole(null, 'developer')).toBe('developer');
13 expect(effectiveRole(null, 'viewer')).toBe('viewer');
14 expect(effectiveRole(null, 'owner')).toBe('owner');
15 });
16
17 test('returns the org role when only org role is set', () => {
18 expect(effectiveRole('viewer', null)).toBe('viewer');
19 expect(effectiveRole('admin', null)).toBe('admin');
20 });
21
22 test('takes the higher-rank role when both are set', () => {
23 expect(effectiveRole('viewer', 'admin')).toBe('admin');
24 expect(effectiveRole('admin', 'viewer')).toBe('admin');
25 expect(effectiveRole('owner', 'developer')).toBe('owner');
26 expect(effectiveRole('developer', 'owner')).toBe('owner');
27 });
28
29 test('returns either when ranks are equal', () => {
30 expect(effectiveRole('admin', 'admin')).toBe('admin');
31 expect(effectiveRole('developer', 'developer')).toBe('developer');
32 });
33});
34
35describe('hasRoleAtLeast', () => {
36 test('owner satisfies every minimum', () => {
37 expect(hasRoleAtLeast('owner', 'viewer')).toBe(true);
38 expect(hasRoleAtLeast('owner', 'developer')).toBe(true);
39 expect(hasRoleAtLeast('owner', 'admin')).toBe(true);
40 expect(hasRoleAtLeast('owner', 'owner')).toBe(true);
41 });
42
43 test('admin covers admin and below, not owner', () => {
44 expect(hasRoleAtLeast('admin', 'viewer')).toBe(true);
45 expect(hasRoleAtLeast('admin', 'developer')).toBe(true);
46 expect(hasRoleAtLeast('admin', 'admin')).toBe(true);
47 expect(hasRoleAtLeast('admin', 'owner')).toBe(false);
48 });
49
50 test('developer covers developer and viewer', () => {
51 expect(hasRoleAtLeast('developer', 'viewer')).toBe(true);
52 expect(hasRoleAtLeast('developer', 'developer')).toBe(true);
53 expect(hasRoleAtLeast('developer', 'admin')).toBe(false);
54 expect(hasRoleAtLeast('developer', 'owner')).toBe(false);
55 });
56
57 test('viewer covers only viewer', () => {
58 expect(hasRoleAtLeast('viewer', 'viewer')).toBe(true);
59 expect(hasRoleAtLeast('viewer', 'developer')).toBe(false);
60 expect(hasRoleAtLeast('viewer', 'admin')).toBe(false);
61 expect(hasRoleAtLeast('viewer', 'owner')).toBe(false);
62 });
63});
64
65/**
66 * Owner-tier gating scaffolding — narrative tests.
67 *
68 * No route in the API today gates at `owner` (admin is the practical
69 * ceiling). When a future destructive route lands (project hard-delete,
70 * ownership transfer, etc.), it should chain `requireProjectRole('owner')`
71 * after `requireProjectAuth`. These tests pin the rank semantics so a
72 * regression in `ROLE_RANK` would fail before such a route ships.
73 */
74describe('owner-tier gating', () => {
75 test('admin cannot pass an owner gate', () => {
76 // The most-privileged non-owner role still falls below the owner gate.
77 expect(hasRoleAtLeast('admin', 'owner')).toBe(false);
78 });
79
80 test('owner passes the owner gate', () => {
81 expect(hasRoleAtLeast('owner', 'owner')).toBe(true);
82 });
83
84 test('an api key can never reach the owner gate', () => {
85 // `routes/api-keys.ts:createKeySchema` rejects 'owner' as an
86 // assignable role (only viewer/developer/admin); the existing
87 // `services/api-keys.ts:isAssignableKeyRole` enforces the same.
88 // So no resolved api-key role can satisfy `requireProjectRole('owner')`.
89 expect(hasRoleAtLeast('admin', 'owner')).toBe(false);
90 expect(hasRoleAtLeast('developer', 'owner')).toBe(false);
91 expect(hasRoleAtLeast('viewer', 'owner')).toBe(false);
92 });
93});