@brika/auth 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +207 -0
  2. package/package.json +50 -0
  3. package/src/__tests__/AuthClient.test.ts +736 -0
  4. package/src/__tests__/AuthService.test.ts +140 -0
  5. package/src/__tests__/ScopeService.test.ts +156 -0
  6. package/src/__tests__/SessionService.test.ts +311 -0
  7. package/src/__tests__/UserService-avatar.test.ts +277 -0
  8. package/src/__tests__/UserService.test.ts +223 -0
  9. package/src/__tests__/canAccess.test.ts +166 -0
  10. package/src/__tests__/disabledScopes.test.ts +101 -0
  11. package/src/__tests__/middleware.test.ts +190 -0
  12. package/src/__tests__/plugin.test.ts +78 -0
  13. package/src/__tests__/requireSession.test.ts +78 -0
  14. package/src/__tests__/routes-auth.test.ts +248 -0
  15. package/src/__tests__/routes-profile.test.ts +403 -0
  16. package/src/__tests__/routes-scopes.test.ts +64 -0
  17. package/src/__tests__/routes-sessions.test.ts +235 -0
  18. package/src/__tests__/routes-users.test.ts +477 -0
  19. package/src/__tests__/serveImage.test.ts +277 -0
  20. package/src/__tests__/setup.test.ts +270 -0
  21. package/src/__tests__/verifyToken.test.ts +219 -0
  22. package/src/client/AuthClient.ts +312 -0
  23. package/src/client/http-client.ts +84 -0
  24. package/src/client/index.ts +19 -0
  25. package/src/config.ts +82 -0
  26. package/src/constants.ts +10 -0
  27. package/src/index.ts +16 -0
  28. package/src/lib/define-roles.ts +35 -0
  29. package/src/lib/define-scopes.ts +48 -0
  30. package/src/middleware/canAccess.ts +126 -0
  31. package/src/middleware/index.ts +13 -0
  32. package/src/middleware/requireAuth.ts +35 -0
  33. package/src/middleware/requireScope.ts +46 -0
  34. package/src/middleware/verifyToken.ts +52 -0
  35. package/src/plugin.ts +86 -0
  36. package/src/react/AuthProvider.tsx +105 -0
  37. package/src/react/hooks.ts +128 -0
  38. package/src/react/index.ts +51 -0
  39. package/src/react/withScopeGuard.tsx +73 -0
  40. package/src/roles.ts +40 -0
  41. package/src/schemas.ts +112 -0
  42. package/src/scopes.ts +60 -0
  43. package/src/server/index.ts +44 -0
  44. package/src/server/requireSession.ts +44 -0
  45. package/src/server/routes/auth.ts +102 -0
  46. package/src/server/routes/cookie.ts +7 -0
  47. package/src/server/routes/index.ts +32 -0
  48. package/src/server/routes/profile.ts +162 -0
  49. package/src/server/routes/scopes.ts +22 -0
  50. package/src/server/routes/sessions.ts +68 -0
  51. package/src/server/routes/setup.ts +50 -0
  52. package/src/server/routes/users.ts +175 -0
  53. package/src/server/serveImage.ts +91 -0
  54. package/src/services/AuthService.ts +80 -0
  55. package/src/services/ScopeService.ts +94 -0
  56. package/src/services/SessionService.ts +245 -0
  57. package/src/services/UserService.ts +245 -0
  58. package/src/setup.ts +99 -0
  59. package/src/tanstack/index.ts +15 -0
  60. package/src/tanstack/routeBuilder.ts +311 -0
  61. package/src/types.ts +118 -0
  62. package/tsconfig.json +8 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @brika/auth - UserService Avatar & deleteUser Tests
3
+ *
4
+ * Tests for avatar-related methods (processAvatar, setAvatar, getAvatarData,
5
+ * removeAvatar) and deleteUser. These complement the existing UserService.test.ts
6
+ * which covers createUser, getUser, getUserByEmail, listUsers, setPassword,
7
+ * verifyPassword, and hasAdmin.
8
+ */
9
+
10
+ import type { Database } from 'bun:sqlite';
11
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
12
+ import { deflateSync } from 'node:zlib';
13
+ import { processAvatar, UserService } from '../services/UserService';
14
+ import { openAuthDatabase } from '../setup';
15
+ import { Role } from '../types';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** Create a valid W×H RGB PNG buffer for testing. */
22
+ function makeTestPng(width: number, height: number = width): Buffer {
23
+ function crc32(buf: Buffer): number {
24
+ const table = new Uint32Array(256);
25
+ for (let i = 0; i < 256; i++) {
26
+ let c = i;
27
+ for (let j = 0; j < 8; j++) {
28
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
29
+ }
30
+ table[i] = c;
31
+ }
32
+ let c = 0xffffffff;
33
+ for (let i = 0; i < buf.length; i++) {
34
+ c = (c >>> 8) ^ (table[(c ^ (buf[i] ?? 0)) & 0xff] ?? 0);
35
+ }
36
+ return (c ^ 0xffffffff) >>> 0;
37
+ }
38
+
39
+ function chunk(type: string, data: Buffer): Buffer {
40
+ const len = Buffer.alloc(4);
41
+ len.writeUInt32BE(data.length);
42
+ const td = Buffer.concat([Buffer.from(type), data]);
43
+ const crc = Buffer.alloc(4);
44
+ crc.writeUInt32BE(crc32(td));
45
+ return Buffer.concat([len, td, crc]);
46
+ }
47
+
48
+ const rowBytes = 1 + width * 3; // filter byte + RGB per pixel
49
+ const raw = Buffer.alloc(height * rowBytes);
50
+ for (let y = 0; y < height; y++) {
51
+ raw[y * rowBytes] = 0; // filter=none
52
+ for (let x = 0; x < width; x++) {
53
+ raw[y * rowBytes + 1 + x * 3] = 255; // R
54
+ }
55
+ }
56
+
57
+ const ihdr = Buffer.alloc(13);
58
+ ihdr.writeUInt32BE(width, 0);
59
+ ihdr.writeUInt32BE(height, 4);
60
+ ihdr[8] = 8; // bit depth
61
+ ihdr[9] = 2; // colour type: RGB
62
+
63
+ return Buffer.concat([
64
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), // PNG signature
65
+ chunk('IHDR', ihdr),
66
+ chunk('IDAT', deflateSync(raw)),
67
+ chunk('IEND', Buffer.alloc(0)),
68
+ ]);
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // processAvatar (exported pure function)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('processAvatar', () => {
76
+ it('should return a Buffer', () => {
77
+ const input = makeTestPng(16);
78
+ const result = processAvatar(input);
79
+ expect(result).toBeInstanceOf(Buffer);
80
+ });
81
+
82
+ it('should produce a non-empty output', () => {
83
+ const input = makeTestPng(16);
84
+ const result = processAvatar(input);
85
+ expect(result.byteLength).toBeGreaterThan(0);
86
+ });
87
+
88
+ it('should produce WebP output (RIFF....WEBP magic bytes)', () => {
89
+ const input = makeTestPng(16);
90
+ const result = processAvatar(input);
91
+ // WebP files start with RIFF
92
+ expect(result[0]).toBe(0x52); // R
93
+ expect(result[1]).toBe(0x49); // I
94
+ expect(result[2]).toBe(0x46); // F
95
+ expect(result[3]).toBe(0x46); // F
96
+ // Bytes 8-11 are "WEBP"
97
+ expect(result[8]).toBe(0x57); // W
98
+ expect(result[9]).toBe(0x45); // E
99
+ expect(result[10]).toBe(0x42); // B
100
+ expect(result[11]).toBe(0x50); // P
101
+ });
102
+
103
+ it('should produce consistent output for the same input', () => {
104
+ const input = makeTestPng(16);
105
+ const r1 = processAvatar(input);
106
+ const r2 = processAvatar(input);
107
+ expect(Buffer.compare(r1, r2)).toBe(0);
108
+ });
109
+ });
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // UserService — avatar methods + deleteUser
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe('UserService — avatar & deleteUser', () => {
116
+ let db: Database;
117
+ let service: UserService;
118
+
119
+ beforeEach(() => {
120
+ db = openAuthDatabase(':memory:');
121
+ service = new UserService(db);
122
+ });
123
+
124
+ afterEach(() => {
125
+ db.close();
126
+ });
127
+
128
+ // -------------------------------------------------------------------------
129
+ // setAvatar
130
+ // -------------------------------------------------------------------------
131
+
132
+ describe('setAvatar', () => {
133
+ it('should return a hash string', () => {
134
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
135
+ const hash = service.setAvatar(user.id, makeTestPng(16));
136
+ expect(typeof hash).toBe('string');
137
+ expect(hash.length).toBeGreaterThan(0);
138
+ });
139
+
140
+ it('should return a short alphanumeric hash (base-36, max 8 chars)', () => {
141
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
142
+ const hash = service.setAvatar(user.id, makeTestPng(16));
143
+ expect(hash).toMatch(/^[0-9a-z]{1,8}$/);
144
+ });
145
+
146
+ it('should produce the same hash for the same image', () => {
147
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
148
+ const input = makeTestPng(16);
149
+ const h1 = service.setAvatar(user.id, input);
150
+ const h2 = service.setAvatar(user.id, input);
151
+ expect(h1).toBe(h2);
152
+ });
153
+ });
154
+
155
+ // -------------------------------------------------------------------------
156
+ // getAvatarData
157
+ // -------------------------------------------------------------------------
158
+
159
+ describe('getAvatarData', () => {
160
+ it('should retrieve avatar data after setAvatar', () => {
161
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
162
+ service.setAvatar(user.id, makeTestPng(16));
163
+
164
+ const result = service.getAvatarData(user.id);
165
+ expect(result).not.toBeNull();
166
+ // bun:sqlite returns BLOB columns as Uint8Array (Buffer is a Uint8Array subclass)
167
+ expect(result?.data).toBeInstanceOf(Uint8Array);
168
+ expect(result?.data.byteLength).toBeGreaterThan(0);
169
+ });
170
+
171
+ it('should return image/webp mimeType after setAvatar', () => {
172
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
173
+ service.setAvatar(user.id, makeTestPng(16));
174
+
175
+ const result = service.getAvatarData(user.id);
176
+ expect(result?.mimeType).toBe('image/webp');
177
+ });
178
+
179
+ it('should return null for user without avatar', () => {
180
+ const user = service.createUser('noavatar@example.com', 'No Avatar', Role.USER);
181
+ const result = service.getAvatarData(user.id);
182
+ expect(result).toBeNull();
183
+ });
184
+
185
+ it('should return null for non-existent user id', () => {
186
+ const result = service.getAvatarData('00000000-0000-0000-0000-000000000000');
187
+ expect(result).toBeNull();
188
+ });
189
+ });
190
+
191
+ // -------------------------------------------------------------------------
192
+ // removeAvatar
193
+ // -------------------------------------------------------------------------
194
+
195
+ describe('removeAvatar', () => {
196
+ it('should clear avatar so getAvatarData returns null', () => {
197
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
198
+ service.setAvatar(user.id, makeTestPng(16));
199
+
200
+ // Confirm avatar was set
201
+ expect(service.getAvatarData(user.id)).not.toBeNull();
202
+
203
+ service.removeAvatar(user.id);
204
+
205
+ expect(service.getAvatarData(user.id)).toBeNull();
206
+ });
207
+
208
+ it('should be a no-op for user without avatar', () => {
209
+ const user = service.createUser('noavatar@example.com', 'No Avatar', Role.USER);
210
+ // Should not throw even if no avatar is present
211
+ expect(service.removeAvatar(user.id)).toBeUndefined();
212
+ expect(service.getAvatarData(user.id)).toBeNull();
213
+ });
214
+
215
+ it('should clear avatarHash on user after removeAvatar', () => {
216
+ const user = service.createUser('avatar@example.com', 'Avatar User', Role.USER);
217
+ service.setAvatar(user.id, makeTestPng(16));
218
+
219
+ const before = service.getUser(user.id);
220
+ expect(before?.avatarHash).not.toBeNull();
221
+
222
+ service.removeAvatar(user.id);
223
+
224
+ const after = service.getUser(user.id);
225
+ expect(after?.avatarHash).toBeNull();
226
+ });
227
+ });
228
+
229
+ // -------------------------------------------------------------------------
230
+ // deleteUser
231
+ // -------------------------------------------------------------------------
232
+
233
+ describe('deleteUser', () => {
234
+ it('should delete an existing non-admin user', () => {
235
+ service.createUser('todelete@example.com', 'To Delete', Role.USER);
236
+
237
+ service.deleteUser('todelete@example.com');
238
+
239
+ const gone = service.getUserByEmail('todelete@example.com');
240
+ expect(gone).toBeNull();
241
+ });
242
+
243
+ it('should throw for a non-existent user', () => {
244
+ expect(() => service.deleteUser('ghost@example.com')).toThrow('User not found');
245
+ });
246
+
247
+ it('should throw when trying to delete an admin user', () => {
248
+ service.createUser('admin@example.com', 'Admin', Role.ADMIN);
249
+
250
+ expect(() => service.deleteUser('admin@example.com')).toThrow('Cannot delete admin user');
251
+
252
+ // User must still exist after the failed attempt
253
+ const still = service.getUserByEmail('admin@example.com');
254
+ expect(still).not.toBeNull();
255
+ });
256
+
257
+ it('should be case-insensitive for email lookup', () => {
258
+ service.createUser('mixed@example.com', 'Mixed Case', Role.USER);
259
+
260
+ service.deleteUser('MIXED@EXAMPLE.COM');
261
+
262
+ const gone = service.getUserByEmail('mixed@example.com');
263
+ expect(gone).toBeNull();
264
+ });
265
+
266
+ it('should delete GUEST and SERVICE roles as well', () => {
267
+ service.createUser('guest@example.com', 'Guest User', Role.GUEST);
268
+ service.createUser('svc@example.com', 'Service Account', Role.SERVICE);
269
+
270
+ service.deleteUser('guest@example.com');
271
+ service.deleteUser('svc@example.com');
272
+
273
+ expect(service.getUserByEmail('guest@example.com')).toBeNull();
274
+ expect(service.getUserByEmail('svc@example.com')).toBeNull();
275
+ });
276
+ });
277
+ });
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @brika/auth - UserService Tests
3
+ */
4
+
5
+ import type { Database } from 'bun:sqlite';
6
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
7
+ import { UserService } from '../services/UserService';
8
+ import { openAuthDatabase } from '../setup';
9
+ import { Role } from '../types';
10
+
11
+ describe('UserService', () => {
12
+ let service: UserService;
13
+ let db: Database;
14
+
15
+ beforeEach(() => {
16
+ db = openAuthDatabase(':memory:');
17
+ service = new UserService(db);
18
+ });
19
+
20
+ afterEach(() => {
21
+ db.close();
22
+ });
23
+
24
+ describe('createUser', () => {
25
+ it('should create a new user', () => {
26
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
27
+
28
+ expect(user).toBeDefined();
29
+ expect(user.email).toBe('test@example.com');
30
+ expect(user.name).toBe('Test User');
31
+ expect(user.role).toBe(Role.USER);
32
+ expect(user.isActive).toBe(true);
33
+ });
34
+
35
+ it('should lowercase email', () => {
36
+ const user = service.createUser('TEST@EXAMPLE.COM', 'Test User', Role.USER);
37
+
38
+ expect(user.email).toBe('test@example.com');
39
+ });
40
+
41
+ it('should reject duplicate email', () => {
42
+ service.createUser('test@example.com', 'User 1', Role.USER);
43
+
44
+ try {
45
+ service.createUser('test@example.com', 'User 2', Role.USER);
46
+ expect.unreachable('Should throw error');
47
+ } catch (error: unknown) {
48
+ expect((error as Error).message).toContain('UNIQUE constraint failed');
49
+ }
50
+ });
51
+
52
+ it('should generate unique IDs', () => {
53
+ const user1 = service.createUser('user1@example.com', 'User 1', Role.USER);
54
+ const user2 = service.createUser('user2@example.com', 'User 2', Role.USER);
55
+
56
+ expect(user1.id).not.toBe(user2.id);
57
+ });
58
+ });
59
+
60
+ describe('getUser', () => {
61
+ it('should retrieve user by ID', () => {
62
+ const created = service.createUser('test@example.com', 'Test User', Role.USER);
63
+
64
+ const retrieved = service.getUser(created.id);
65
+ expect(retrieved).toBeDefined();
66
+ expect(retrieved?.email).toBe('test@example.com');
67
+ });
68
+
69
+ it('should return null for unknown user', () => {
70
+ const user = service.getUser('unknown-id');
71
+ expect(user).toBeNull();
72
+ });
73
+ });
74
+
75
+ describe('getUserByEmail', () => {
76
+ it('should retrieve user by email', () => {
77
+ service.createUser('test@example.com', 'Test User', Role.USER);
78
+
79
+ const user = service.getUserByEmail('test@example.com');
80
+ expect(user).toBeDefined();
81
+ expect(user?.name).toBe('Test User');
82
+ });
83
+
84
+ it('should be case-insensitive', () => {
85
+ service.createUser('test@example.com', 'Test User', Role.USER);
86
+
87
+ const user = service.getUserByEmail('TEST@EXAMPLE.COM');
88
+ expect(user).toBeDefined();
89
+ expect(user?.email).toBe('test@example.com');
90
+ });
91
+
92
+ it('should return null for unknown email', () => {
93
+ const user = service.getUserByEmail('unknown@example.com');
94
+ expect(user).toBeNull();
95
+ });
96
+ });
97
+
98
+ describe('listUsers', () => {
99
+ it('should list all users', () => {
100
+ service.createUser('user1@example.com', 'User 1', Role.USER);
101
+ service.createUser('user2@example.com', 'User 2', Role.ADMIN);
102
+ service.createUser('user3@example.com', 'User 3', Role.GUEST);
103
+
104
+ const users = service.listUsers();
105
+ expect(users).toHaveLength(3);
106
+ });
107
+
108
+ it('should return empty array initially', () => {
109
+ const users = service.listUsers();
110
+ expect(users).toEqual([]);
111
+ });
112
+ });
113
+
114
+ describe('setPassword', () => {
115
+ it('should set password for user', async () => {
116
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
117
+
118
+ await service.setPassword(user.id, 'Password123!');
119
+
120
+ const valid = await service.verifyPassword(user.id, 'Password123!');
121
+ expect(valid).toBe(true);
122
+ });
123
+
124
+ it('should reject password too short', async () => {
125
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
126
+
127
+ try {
128
+ await service.setPassword(user.id, 'short');
129
+ expect.unreachable('Should throw error');
130
+ } catch (error: unknown) {
131
+ expect((error as Error).message).toContain('Min 8 characters');
132
+ }
133
+ });
134
+
135
+ it('should require uppercase', async () => {
136
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
137
+
138
+ try {
139
+ await service.setPassword(user.id, 'password123!');
140
+ expect.unreachable('Should throw error');
141
+ } catch (error: unknown) {
142
+ expect((error as Error).message).toContain('uppercase letter');
143
+ }
144
+ });
145
+
146
+ it('should require number', async () => {
147
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
148
+
149
+ try {
150
+ await service.setPassword(user.id, 'Password!!!!');
151
+ expect.unreachable('Should throw error');
152
+ } catch (error: unknown) {
153
+ expect((error as Error).message).toBeTruthy();
154
+ }
155
+ });
156
+
157
+ it('should require special character', async () => {
158
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
159
+
160
+ try {
161
+ await service.setPassword(user.id, 'Password12345');
162
+ expect.unreachable('Should throw error');
163
+ } catch (error: unknown) {
164
+ expect((error as Error).message).toBeTruthy();
165
+ }
166
+ });
167
+
168
+ it('should hash password and verify it', async () => {
169
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
170
+
171
+ await service.setPassword(user.id, 'Password123!');
172
+
173
+ const valid = await service.verifyPassword(user.id, 'Password123!');
174
+ expect(valid).toBe(true);
175
+
176
+ const invalid = await service.verifyPassword(user.id, 'WrongPassword1!');
177
+ expect(invalid).toBe(false);
178
+ });
179
+ });
180
+
181
+ describe('verifyPassword', () => {
182
+ it('should verify correct password', async () => {
183
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
184
+
185
+ await service.setPassword(user.id, 'Password123!');
186
+
187
+ const valid = await service.verifyPassword(user.id, 'Password123!');
188
+ expect(valid).toBe(true);
189
+ });
190
+
191
+ it('should reject wrong password', async () => {
192
+ const user = service.createUser('test@example.com', 'Test User', Role.USER);
193
+
194
+ await service.setPassword(user.id, 'Password123!');
195
+
196
+ const valid = await service.verifyPassword(user.id, 'WrongPassword123!');
197
+ expect(valid).toBe(false);
198
+ });
199
+
200
+ it('should return false for user without password', async () => {
201
+ const user = service.createUser('test@example.com', 'Test User', Role.SERVICE);
202
+
203
+ const valid = await service.verifyPassword(user.id, 'Password123!');
204
+ expect(valid).toBe(false);
205
+ });
206
+ });
207
+
208
+ describe('hasAdmin', () => {
209
+ it('should detect if admin exists', () => {
210
+ let hasAdmin = service.hasAdmin();
211
+ expect(hasAdmin).toBe(false);
212
+
213
+ service.createUser('admin@example.com', 'Admin', Role.ADMIN);
214
+ hasAdmin = service.hasAdmin();
215
+ expect(hasAdmin).toBe(true);
216
+ });
217
+
218
+ it('should return false initially', () => {
219
+ const hasAdmin = service.hasAdmin();
220
+ expect(hasAdmin).toBe(false);
221
+ });
222
+ });
223
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @brika/auth - canAccess Tests
3
+ */
4
+
5
+ import { describe, expect, it } from 'bun:test';
6
+ import {
7
+ canAccess,
8
+ canAccessAll,
9
+ createPermissionChecker,
10
+ Features,
11
+ } from '../middleware/canAccess';
12
+ import { Role, Scope } from '../types';
13
+
14
+ describe('canAccess', () => {
15
+ describe('canAccess function', () => {
16
+ it('should return false for null/undefined scopes', () => {
17
+ expect(canAccess(null, Scope.WORKFLOW_READ)).toBe(false);
18
+ expect(canAccess(undefined, Scope.WORKFLOW_READ)).toBe(false);
19
+ expect(canAccess([], Scope.WORKFLOW_READ)).toBe(false);
20
+ });
21
+
22
+ it('should check single scope', () => {
23
+ const scopes = [Scope.WORKFLOW_READ];
24
+ expect(canAccess(scopes, Scope.WORKFLOW_READ)).toBe(true);
25
+ expect(canAccess(scopes, Scope.WORKFLOW_WRITE)).toBe(false);
26
+ });
27
+
28
+ it('should check multiple scopes (any)', () => {
29
+ const scopes = [Scope.WORKFLOW_READ, Scope.BOARD_READ];
30
+ const required = [Scope.WORKFLOW_WRITE, Scope.WORKFLOW_READ];
31
+ expect(canAccess(scopes, required)).toBe(true);
32
+ });
33
+
34
+ it('should grant all to admin', () => {
35
+ const scopes = [Scope.ADMIN_ALL];
36
+ expect(canAccess(scopes, Scope.WORKFLOW_READ)).toBe(true);
37
+ expect(canAccess(scopes, Scope.PLUGIN_MANAGE)).toBe(true);
38
+ expect(canAccess(scopes, Scope.SETTINGS_WRITE)).toBe(true);
39
+ });
40
+
41
+ it('should handle array input', () => {
42
+ const scopes = [Scope.WORKFLOW_READ, Scope.WORKFLOW_WRITE];
43
+ const required = [Scope.WORKFLOW_WRITE, Scope.WORKFLOW_EXECUTE];
44
+ expect(canAccess(scopes, required)).toBe(true); // Has one
45
+ });
46
+ });
47
+
48
+ describe('canAccessAll function', () => {
49
+ it('should return false for null/undefined scopes', () => {
50
+ expect(canAccessAll(null, [Scope.WORKFLOW_READ])).toBe(false);
51
+ expect(canAccessAll(undefined, [Scope.WORKFLOW_READ])).toBe(false);
52
+ });
53
+
54
+ it('should check all required scopes', () => {
55
+ const scopes = [Scope.WORKFLOW_READ, Scope.WORKFLOW_WRITE];
56
+ expect(canAccessAll(scopes, [Scope.WORKFLOW_READ, Scope.WORKFLOW_WRITE])).toBe(true);
57
+ expect(canAccessAll(scopes, [Scope.WORKFLOW_READ, Scope.WORKFLOW_EXECUTE])).toBe(false);
58
+ });
59
+
60
+ it('should grant all to admin', () => {
61
+ const scopes = [Scope.ADMIN_ALL];
62
+ expect(
63
+ canAccessAll(scopes, [Scope.WORKFLOW_READ, Scope.WORKFLOW_WRITE, Scope.PLUGIN_MANAGE])
64
+ ).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('createPermissionChecker', () => {
69
+ it('should create feature permission object', () => {
70
+ const WorkflowPerms = createPermissionChecker('Workflow', {
71
+ read: Scope.WORKFLOW_READ,
72
+ write: Scope.WORKFLOW_WRITE,
73
+ execute: Scope.WORKFLOW_EXECUTE,
74
+ });
75
+
76
+ expect(typeof WorkflowPerms.read).toBe('function');
77
+ expect(typeof WorkflowPerms.write).toBe('function');
78
+ expect(typeof WorkflowPerms.execute).toBe('function');
79
+ });
80
+
81
+ it('should check permissions correctly', () => {
82
+ const WorkflowPerms = createPermissionChecker('Workflow', {
83
+ read: Scope.WORKFLOW_READ,
84
+ write: Scope.WORKFLOW_WRITE,
85
+ });
86
+
87
+ const userScopes = [Scope.WORKFLOW_READ];
88
+ const readCheck = WorkflowPerms.read;
89
+ const writeCheck = WorkflowPerms.write;
90
+ if (!readCheck || !writeCheck) {
91
+ throw new Error('Expected read and write permission checkers to be defined');
92
+ }
93
+ expect(readCheck(userScopes)).toBe(true);
94
+ expect(writeCheck(userScopes)).toBe(false);
95
+ });
96
+
97
+ it('should support array scopes', () => {
98
+ const AdminPerms = createPermissionChecker('Admin', {
99
+ fullAccess: [Scope.ADMIN_ALL],
100
+ userManagement: [Scope.ADMIN_ALL],
101
+ });
102
+
103
+ const adminScopes = [Scope.ADMIN_ALL];
104
+ const fullAccessCheck = AdminPerms.fullAccess;
105
+ const userManagementCheck = AdminPerms.userManagement;
106
+ if (!fullAccessCheck || !userManagementCheck) {
107
+ throw new Error('Expected fullAccess and userManagement permission checkers to be defined');
108
+ }
109
+ expect(fullAccessCheck(adminScopes)).toBe(true);
110
+ expect(userManagementCheck(adminScopes)).toBe(true);
111
+ });
112
+ });
113
+
114
+ describe('Features preset', () => {
115
+ it('should have Workflow feature', () => {
116
+ expect(Features.Workflow.read).toBeDefined();
117
+ expect(Features.Workflow.write).toBeDefined();
118
+ expect(Features.Workflow.execute).toBeDefined();
119
+ });
120
+
121
+ it('should check Workflow permissions', () => {
122
+ const userScopes = [Scope.WORKFLOW_READ, Scope.WORKFLOW_WRITE];
123
+ const readCheck = Features.Workflow.read;
124
+ const writeCheck = Features.Workflow.write;
125
+ const executeCheck = Features.Workflow.execute;
126
+ if (!readCheck || !writeCheck || !executeCheck) {
127
+ throw new Error('Expected Workflow permission checkers to be defined');
128
+ }
129
+ expect(readCheck(userScopes)).toBe(true);
130
+ expect(writeCheck(userScopes)).toBe(true);
131
+ expect(executeCheck(userScopes)).toBe(false);
132
+ });
133
+
134
+ it('should have Board feature', () => {
135
+ expect(Features.Board.read).toBeDefined();
136
+ expect(Features.Board.write).toBeDefined();
137
+ });
138
+
139
+ it('should have Plugin feature', () => {
140
+ expect(Features.Plugin.read).toBeDefined();
141
+ expect(Features.Plugin.manage).toBeDefined();
142
+ });
143
+
144
+ it('should have Settings feature', () => {
145
+ expect(Features.Settings.read).toBeDefined();
146
+ expect(Features.Settings.write).toBeDefined();
147
+ });
148
+
149
+ it('should have Admin feature', () => {
150
+ expect(Features.Admin.all).toBeDefined();
151
+ });
152
+
153
+ it('should work with admin scopes', () => {
154
+ const adminScopes = [Scope.ADMIN_ALL];
155
+ const executeCheck = Features.Workflow.execute;
156
+ const boardWriteCheck = Features.Board.write;
157
+ const adminAllCheck = Features.Admin.all;
158
+ if (!executeCheck || !boardWriteCheck || !adminAllCheck) {
159
+ throw new Error('Expected permission checkers to be defined');
160
+ }
161
+ expect(executeCheck(adminScopes)).toBe(true);
162
+ expect(boardWriteCheck(adminScopes)).toBe(true);
163
+ expect(adminAllCheck(adminScopes)).toBe(true);
164
+ });
165
+ });
166
+ });