@stonecrop/casl-middleware 0.7.1 → 0.7.3
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AbilityBuilder, PureAbility, subject } from '@casl/ability';
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { createAbility, detectSubjectType } from '../src/middleware/ability';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { createAbility, detectSubjectType, defaultAbilityBuilder, createDatabaseAbilityBuilder, createConfigBasedAbilityBuilder, createAbilityFactory, } from '../src/middleware/ability';
|
|
4
4
|
const Ability = PureAbility;
|
|
5
5
|
describe('Ability Creation', () => {
|
|
6
6
|
describe('createAbility', () => {
|
|
@@ -122,4 +122,281 @@ describe('Ability Creation', () => {
|
|
|
122
122
|
expect(ability.can('update', subject('Post', { authorId: '456' }))).toBe(false);
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
|
+
describe('detectSubjectType', () => {
|
|
126
|
+
it('should detect __caslSubjectType__ property', () => {
|
|
127
|
+
const obj = { __caslSubjectType__: 'CustomType', data: 'test' };
|
|
128
|
+
expect(detectSubjectType(obj)).toBe('CustomType');
|
|
129
|
+
});
|
|
130
|
+
it('should fallback to type property', () => {
|
|
131
|
+
const obj = { type: 'Post', title: 'Test' };
|
|
132
|
+
expect(detectSubjectType(obj)).toBe('Post');
|
|
133
|
+
});
|
|
134
|
+
it('should fallback to constructor name', () => {
|
|
135
|
+
class User {
|
|
136
|
+
name;
|
|
137
|
+
constructor(name) {
|
|
138
|
+
this.name = name;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const user = new User('John');
|
|
142
|
+
expect(detectSubjectType(user)).toBe('User');
|
|
143
|
+
});
|
|
144
|
+
it('should return Unknown for plain objects', () => {
|
|
145
|
+
const obj = { data: 'test' };
|
|
146
|
+
expect(detectSubjectType(obj)).toBe('Unknown');
|
|
147
|
+
});
|
|
148
|
+
it('should return Unknown for null or undefined', () => {
|
|
149
|
+
expect(detectSubjectType(null)).toBe('Unknown');
|
|
150
|
+
expect(detectSubjectType(undefined)).toBe('Unknown');
|
|
151
|
+
});
|
|
152
|
+
it('should return Unknown for primitives', () => {
|
|
153
|
+
expect(detectSubjectType('string')).toBe('Unknown');
|
|
154
|
+
expect(detectSubjectType(123)).toBe('Unknown');
|
|
155
|
+
expect(detectSubjectType(true)).toBe('Unknown');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('defaultAbilityBuilder', () => {
|
|
159
|
+
it('should create ability with only Query read access for no user', () => {
|
|
160
|
+
const ability = defaultAbilityBuilder();
|
|
161
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
162
|
+
expect(ability.can('read', 'Mutation')).toBe(false);
|
|
163
|
+
expect(ability.can('manage', 'all')).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
it('should create ability with only Query read access for user without roles', () => {
|
|
166
|
+
const user = { id: '123', roles: [] };
|
|
167
|
+
const ability = defaultAbilityBuilder(user);
|
|
168
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
169
|
+
expect(ability.can('read', 'Mutation')).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('createDatabaseAbilityBuilder', () => {
|
|
173
|
+
it('should build ability from database rules', async () => {
|
|
174
|
+
const mockFetchRules = vi.fn().mockResolvedValue([
|
|
175
|
+
{ action: 'read', subject: 'Post', inverted: false },
|
|
176
|
+
{ action: 'create', subject: 'Post', inverted: false },
|
|
177
|
+
{ action: 'update', subject: 'Post', fields: ['title'], inverted: false },
|
|
178
|
+
{ action: 'delete', subject: 'Post', inverted: true },
|
|
179
|
+
]);
|
|
180
|
+
const builder = createDatabaseAbilityBuilder(mockFetchRules);
|
|
181
|
+
const user = { id: '123', roles: ['user'] };
|
|
182
|
+
const ability = await builder(user);
|
|
183
|
+
expect(mockFetchRules).toHaveBeenCalledWith('123');
|
|
184
|
+
expect(ability.can('read', 'Post')).toBe(true);
|
|
185
|
+
expect(ability.can('create', 'Post')).toBe(true);
|
|
186
|
+
expect(ability.can('update', 'Post', 'title')).toBe(true);
|
|
187
|
+
expect(ability.can('delete', 'Post')).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
it('should handle rules with conditions', async () => {
|
|
190
|
+
const mockFetchRules = vi.fn().mockResolvedValue([
|
|
191
|
+
{
|
|
192
|
+
action: 'update',
|
|
193
|
+
subject: 'Post',
|
|
194
|
+
conditions: { authorId: '123' },
|
|
195
|
+
inverted: false,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
const builder = createDatabaseAbilityBuilder(mockFetchRules);
|
|
199
|
+
const user = { id: '123', roles: ['user'] };
|
|
200
|
+
const ability = await builder(user);
|
|
201
|
+
expect(ability.can('update', subject('Post', { authorId: '123' }))).toBe(true);
|
|
202
|
+
expect(ability.can('update', subject('Post', { authorId: '456' }))).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
it('should only allow Query read for user without id', async () => {
|
|
205
|
+
const mockFetchRules = vi.fn();
|
|
206
|
+
const builder = createDatabaseAbilityBuilder(mockFetchRules);
|
|
207
|
+
const ability = await builder();
|
|
208
|
+
expect(mockFetchRules).not.toHaveBeenCalled();
|
|
209
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
210
|
+
expect(ability.can('read', 'Post')).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
it('should handle empty rules array', async () => {
|
|
213
|
+
const mockFetchRules = vi.fn().mockResolvedValue([]);
|
|
214
|
+
const builder = createDatabaseAbilityBuilder(mockFetchRules);
|
|
215
|
+
const user = { id: '123', roles: [] };
|
|
216
|
+
const ability = await builder(user);
|
|
217
|
+
expect(mockFetchRules).toHaveBeenCalledWith('123');
|
|
218
|
+
expect(ability.can('read', 'Query')).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('createConfigBasedAbilityBuilder', () => {
|
|
222
|
+
it('should build ability from role-based config', async () => {
|
|
223
|
+
const config = {
|
|
224
|
+
roles: {
|
|
225
|
+
editor: [
|
|
226
|
+
{ action: 'read', subject: 'Post' },
|
|
227
|
+
{ action: 'create', subject: 'Post' },
|
|
228
|
+
{ action: 'update', subject: 'Post' },
|
|
229
|
+
],
|
|
230
|
+
viewer: [{ action: 'read', subject: 'Post' }],
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
234
|
+
const user = { id: '123', roles: ['editor'] };
|
|
235
|
+
const ability = await builder(user);
|
|
236
|
+
expect(ability.can('read', 'Post')).toBe(true);
|
|
237
|
+
expect(ability.can('create', 'Post')).toBe(true);
|
|
238
|
+
expect(ability.can('update', 'Post')).toBe(true);
|
|
239
|
+
expect(ability.can('delete', 'Post')).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
it('should apply default rules', async () => {
|
|
242
|
+
const config = {
|
|
243
|
+
roles: {},
|
|
244
|
+
defaultRules: [
|
|
245
|
+
{ action: 'read', subject: 'Query' },
|
|
246
|
+
{ action: 'read', subject: 'PublicData' },
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
250
|
+
const ability = await builder();
|
|
251
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
252
|
+
expect(ability.can('read', 'PublicData')).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
it('should combine multiple roles', async () => {
|
|
255
|
+
const config = {
|
|
256
|
+
roles: {
|
|
257
|
+
editor: [
|
|
258
|
+
{ action: 'create', subject: 'Post' },
|
|
259
|
+
{ action: 'update', subject: 'Post' },
|
|
260
|
+
],
|
|
261
|
+
moderator: [
|
|
262
|
+
{ action: 'delete', subject: 'Comment' },
|
|
263
|
+
{ action: 'update', subject: 'Comment' },
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
268
|
+
const user = { id: '123', roles: ['editor', 'moderator'] };
|
|
269
|
+
const ability = await builder(user);
|
|
270
|
+
expect(ability.can('create', 'Post')).toBe(true);
|
|
271
|
+
expect(ability.can('update', 'Post')).toBe(true);
|
|
272
|
+
expect(ability.can('delete', 'Comment')).toBe(true);
|
|
273
|
+
expect(ability.can('update', 'Comment')).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
it('should handle field-level permissions', async () => {
|
|
276
|
+
const config = {
|
|
277
|
+
roles: {
|
|
278
|
+
user: [
|
|
279
|
+
{ action: 'update', subject: 'User', fields: ['name', 'email'] },
|
|
280
|
+
{ action: 'update', subject: 'User', fields: 'role', inverted: true },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
285
|
+
const user = { id: '123', roles: ['user'] };
|
|
286
|
+
const ability = await builder(user);
|
|
287
|
+
expect(ability.can('update', 'User', 'name')).toBe(true);
|
|
288
|
+
expect(ability.can('update', 'User', 'email')).toBe(true);
|
|
289
|
+
expect(ability.can('update', 'User', 'role')).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
it('should process userId template variables in conditions', async () => {
|
|
292
|
+
const config = {
|
|
293
|
+
roles: {
|
|
294
|
+
user: [
|
|
295
|
+
{
|
|
296
|
+
action: 'update',
|
|
297
|
+
subject: 'Post',
|
|
298
|
+
conditions: { authorId: '{{userId}}' },
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
304
|
+
const user = { id: '123', roles: ['user'] };
|
|
305
|
+
const ability = await builder(user);
|
|
306
|
+
expect(ability.can('update', subject('Post', { authorId: '123' }))).toBe(true);
|
|
307
|
+
expect(ability.can('update', subject('Post', { authorId: '456' }))).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
it('should handle user without roles', async () => {
|
|
310
|
+
const config = {
|
|
311
|
+
roles: {
|
|
312
|
+
user: [{ action: 'read', subject: 'Post' }],
|
|
313
|
+
},
|
|
314
|
+
defaultRules: [{ action: 'read', subject: 'Query' }],
|
|
315
|
+
};
|
|
316
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
317
|
+
const ability = await builder({ id: '123', roles: [] });
|
|
318
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
319
|
+
expect(ability.can('read', 'Post')).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
it('should handle undefined user', async () => {
|
|
322
|
+
const config = {
|
|
323
|
+
roles: {
|
|
324
|
+
user: [{ action: 'read', subject: 'Post' }],
|
|
325
|
+
},
|
|
326
|
+
defaultRules: [{ action: 'read', subject: 'Query' }],
|
|
327
|
+
};
|
|
328
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
329
|
+
const ability = await builder();
|
|
330
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
331
|
+
expect(ability.can('read', 'Post')).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
it('should handle role not defined in config', async () => {
|
|
334
|
+
const config = {
|
|
335
|
+
roles: {
|
|
336
|
+
admin: [{ action: 'manage', subject: 'all' }],
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
340
|
+
const user = { id: '123', roles: ['unknownRole'] };
|
|
341
|
+
const ability = await builder(user);
|
|
342
|
+
expect(ability.can('manage', 'all')).toBe(false);
|
|
343
|
+
});
|
|
344
|
+
it('should handle conditions without userId in template', async () => {
|
|
345
|
+
const config = {
|
|
346
|
+
roles: {
|
|
347
|
+
user: [
|
|
348
|
+
{
|
|
349
|
+
action: 'update',
|
|
350
|
+
subject: 'Post',
|
|
351
|
+
conditions: { status: 'draft' },
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
357
|
+
const user = { id: '123', roles: ['user'] };
|
|
358
|
+
const ability = await builder(user);
|
|
359
|
+
expect(ability.can('update', subject('Post', { status: 'draft' }))).toBe(true);
|
|
360
|
+
expect(ability.can('update', subject('Post', { status: 'published' }))).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
describe('createAbilityFactory', () => {
|
|
364
|
+
it('should create a factory function', () => {
|
|
365
|
+
const builderFn = (user) => {
|
|
366
|
+
const { can, build } = new AbilityBuilder(Ability);
|
|
367
|
+
can('read', 'Query');
|
|
368
|
+
if (user?.roles?.includes('admin')) {
|
|
369
|
+
can('manage', 'all');
|
|
370
|
+
}
|
|
371
|
+
return build({ detectSubjectType });
|
|
372
|
+
};
|
|
373
|
+
const factory = createAbilityFactory(builderFn);
|
|
374
|
+
expect(typeof factory).toBe('function');
|
|
375
|
+
const ability1 = factory();
|
|
376
|
+
expect(ability1.can('read', 'Query')).toBe(true);
|
|
377
|
+
expect(ability1.can('manage', 'all')).toBe(false);
|
|
378
|
+
const ability2 = factory({ id: '123', roles: ['admin'] });
|
|
379
|
+
expect(ability2.can('manage', 'all')).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
describe('createAbility with different builders', () => {
|
|
383
|
+
it('should work with database builder', async () => {
|
|
384
|
+
const mockFetchRules = vi.fn().mockResolvedValue([{ action: 'read', subject: 'Post', inverted: false }]);
|
|
385
|
+
const builder = createDatabaseAbilityBuilder(mockFetchRules);
|
|
386
|
+
const user = { id: '123', roles: ['user'] };
|
|
387
|
+
const ability = await createAbility(user, builder);
|
|
388
|
+
expect(ability.can('read', 'Post')).toBe(true);
|
|
389
|
+
});
|
|
390
|
+
it('should work with config-based builder', async () => {
|
|
391
|
+
const config = {
|
|
392
|
+
roles: {
|
|
393
|
+
admin: [{ action: 'manage', subject: 'all' }],
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
397
|
+
const user = { id: '123', roles: ['admin'] };
|
|
398
|
+
const ability = await createAbility(user, builder);
|
|
399
|
+
expect(ability.can('manage', 'all')).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
125
402
|
});
|
package/dist/tests/jwt.test.js
CHANGED
|
@@ -369,3 +369,332 @@ describe('JWT Configuration Validation', () => {
|
|
|
369
369
|
}).not.toThrow();
|
|
370
370
|
});
|
|
371
371
|
});
|
|
372
|
+
describe('JWT Middleware - Advanced Tests', () => {
|
|
373
|
+
const SECRET = 'test-secret-key';
|
|
374
|
+
beforeEach(() => {
|
|
375
|
+
vi.clearAllMocks();
|
|
376
|
+
// Clear environment for tests
|
|
377
|
+
delete process.env.NODE_ENV;
|
|
378
|
+
});
|
|
379
|
+
describe('Configuration validation', () => {
|
|
380
|
+
it('should throw error when enabled without secret or publicKey', () => {
|
|
381
|
+
expect(() => createJWTMiddleware({ enabled: true })).toThrow('JWT middleware requires either secret or publicKey');
|
|
382
|
+
});
|
|
383
|
+
it('should not throw when disabled without secret', () => {
|
|
384
|
+
expect(() => createJWTMiddleware({ enabled: false })).not.toThrow();
|
|
385
|
+
});
|
|
386
|
+
it('should accept publicKey instead of secret', () => {
|
|
387
|
+
const publicKey = '-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----';
|
|
388
|
+
expect(() => createJWTMiddleware({ enabled: true, publicKey })).not.toThrow();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
describe('Token extraction from different header sources', () => {
|
|
392
|
+
it('should extract token from req.headers.get', async () => {
|
|
393
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
394
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
395
|
+
const context = {
|
|
396
|
+
req: {
|
|
397
|
+
headers: {
|
|
398
|
+
get: vi.fn((name) => (name === 'authorization' ? `Bearer ${token}` : null)),
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
403
|
+
await middleware(context, next);
|
|
404
|
+
expect(context.user).toEqual({ id: '123', roles: ['user'], sub: '123', iat: expect.any(Number) });
|
|
405
|
+
expect(next).toHaveBeenCalled();
|
|
406
|
+
});
|
|
407
|
+
it('should extract token from request.headers.get', async () => {
|
|
408
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
409
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
410
|
+
const context = {
|
|
411
|
+
request: {
|
|
412
|
+
headers: {
|
|
413
|
+
get: vi.fn((name) => (name === 'authorization' ? `Bearer ${token}` : null)),
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
418
|
+
await middleware(context, next);
|
|
419
|
+
expect(context.user).toEqual({ id: '123', roles: ['user'], sub: '123', iat: expect.any(Number) });
|
|
420
|
+
expect(next).toHaveBeenCalled();
|
|
421
|
+
});
|
|
422
|
+
it('should extract token from context.headers object', async () => {
|
|
423
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
424
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
425
|
+
const context = {
|
|
426
|
+
headers: {
|
|
427
|
+
authorization: `Bearer ${token}`,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
431
|
+
await middleware(context, next);
|
|
432
|
+
expect(context.user).toEqual({ id: '123', roles: ['user'], sub: '123', iat: expect.any(Number) });
|
|
433
|
+
expect(next).toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
it('should use custom header name', async () => {
|
|
436
|
+
const middleware = createJWTMiddleware({ secret: SECRET, headerName: 'x-auth-token' });
|
|
437
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
438
|
+
const context = {
|
|
439
|
+
headers: {
|
|
440
|
+
'x-auth-token': `Bearer ${token}`,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
444
|
+
await middleware(context, next);
|
|
445
|
+
expect(context.user).toBeDefined();
|
|
446
|
+
expect(next).toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
describe('Token prefix handling', () => {
|
|
450
|
+
it('should handle token without Bearer prefix', async () => {
|
|
451
|
+
const middleware = createJWTMiddleware({ secret: SECRET, tokenPrefix: '' });
|
|
452
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
453
|
+
const context = {
|
|
454
|
+
headers: {
|
|
455
|
+
authorization: token,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
459
|
+
await middleware(context, next);
|
|
460
|
+
expect(context.user).toBeDefined();
|
|
461
|
+
expect(next).toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
it('should handle custom token prefix', async () => {
|
|
464
|
+
const middleware = createJWTMiddleware({ secret: SECRET, tokenPrefix: 'Token ' });
|
|
465
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
466
|
+
const context = {
|
|
467
|
+
headers: {
|
|
468
|
+
authorization: `Token ${token}`,
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
472
|
+
await middleware(context, next);
|
|
473
|
+
expect(context.user).toBeDefined();
|
|
474
|
+
expect(next).toHaveBeenCalled();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
describe('Optional mode', () => {
|
|
478
|
+
it('should continue without error when optional and no token provided', async () => {
|
|
479
|
+
const middleware = createJWTMiddleware({ secret: SECRET, optional: true });
|
|
480
|
+
const context = {
|
|
481
|
+
headers: {},
|
|
482
|
+
};
|
|
483
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
484
|
+
const result = await middleware(context, next);
|
|
485
|
+
expect(result).toBe('success');
|
|
486
|
+
expect(context.user).toBeUndefined();
|
|
487
|
+
expect(next).toHaveBeenCalled();
|
|
488
|
+
});
|
|
489
|
+
it('should log warning in development mode when token is invalid', async () => {
|
|
490
|
+
process.env.NODE_ENV = 'development';
|
|
491
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
492
|
+
const middleware = createJWTMiddleware({ secret: SECRET, optional: true });
|
|
493
|
+
const context = {
|
|
494
|
+
headers: {
|
|
495
|
+
authorization: 'Bearer invalid-token',
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
499
|
+
await middleware(context, next);
|
|
500
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
501
|
+
expect(next).toHaveBeenCalled();
|
|
502
|
+
consoleSpy.mockRestore();
|
|
503
|
+
});
|
|
504
|
+
it('should not log warning in production mode when token is invalid', async () => {
|
|
505
|
+
process.env.NODE_ENV = 'production';
|
|
506
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
507
|
+
const middleware = createJWTMiddleware({ secret: SECRET, optional: true });
|
|
508
|
+
const context = {
|
|
509
|
+
headers: {
|
|
510
|
+
authorization: 'Bearer invalid-token',
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
514
|
+
await middleware(context, next);
|
|
515
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
516
|
+
expect(next).toHaveBeenCalled();
|
|
517
|
+
consoleSpy.mockRestore();
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
describe('Token verification options', () => {
|
|
521
|
+
it('should verify token with issuer', async () => {
|
|
522
|
+
const middleware = createJWTMiddleware({ secret: SECRET, issuer: 'test-issuer' });
|
|
523
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET, { issuer: 'test-issuer' });
|
|
524
|
+
const context = {
|
|
525
|
+
headers: {
|
|
526
|
+
authorization: `Bearer ${token}`,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
530
|
+
await middleware(context, next);
|
|
531
|
+
expect(context.user).toBeDefined();
|
|
532
|
+
expect(next).toHaveBeenCalled();
|
|
533
|
+
});
|
|
534
|
+
it('should reject token with wrong issuer', async () => {
|
|
535
|
+
const middleware = createJWTMiddleware({ secret: SECRET, issuer: 'expected-issuer' });
|
|
536
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET, { issuer: 'wrong-issuer' });
|
|
537
|
+
const context = {
|
|
538
|
+
headers: {
|
|
539
|
+
authorization: `Bearer ${token}`,
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
const next = vi.fn();
|
|
543
|
+
await expect(middleware(context, next)).rejects.toThrow();
|
|
544
|
+
});
|
|
545
|
+
it('should verify token with audience', async () => {
|
|
546
|
+
const middleware = createJWTMiddleware({ secret: SECRET, audience: 'test-audience' });
|
|
547
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET, { audience: 'test-audience' });
|
|
548
|
+
const context = {
|
|
549
|
+
headers: {
|
|
550
|
+
authorization: `Bearer ${token}`,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
554
|
+
await middleware(context, next);
|
|
555
|
+
expect(context.user).toBeDefined();
|
|
556
|
+
expect(next).toHaveBeenCalled();
|
|
557
|
+
});
|
|
558
|
+
it('should handle maxAge verification', async () => {
|
|
559
|
+
const middleware = createJWTMiddleware({ secret: SECRET, maxAge: '1h' });
|
|
560
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET);
|
|
561
|
+
const context = {
|
|
562
|
+
headers: {
|
|
563
|
+
authorization: `Bearer ${token}`,
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
567
|
+
await middleware(context, next);
|
|
568
|
+
expect(context.user).toBeDefined();
|
|
569
|
+
expect(next).toHaveBeenCalled();
|
|
570
|
+
});
|
|
571
|
+
it('should use custom algorithms', async () => {
|
|
572
|
+
const middleware = createJWTMiddleware({ secret: SECRET, algorithms: ['HS384'] });
|
|
573
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET, { algorithm: 'HS384' });
|
|
574
|
+
const context = {
|
|
575
|
+
headers: {
|
|
576
|
+
authorization: `Bearer ${token}`,
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
580
|
+
await middleware(context, next);
|
|
581
|
+
expect(context.user).toBeDefined();
|
|
582
|
+
expect(next).toHaveBeenCalled();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
describe('Custom user extractor', () => {
|
|
586
|
+
it('should use custom extractUser function', async () => {
|
|
587
|
+
const customExtractor = (payload) => ({
|
|
588
|
+
id: payload.user_id,
|
|
589
|
+
roles: payload.user_roles,
|
|
590
|
+
email: payload.email,
|
|
591
|
+
});
|
|
592
|
+
const middleware = createJWTMiddleware({ secret: SECRET, extractUser: customExtractor });
|
|
593
|
+
const token = jwt.sign({ user_id: '123', user_roles: ['admin'], email: 'test@example.com' }, SECRET);
|
|
594
|
+
const context = {
|
|
595
|
+
headers: {
|
|
596
|
+
authorization: `Bearer ${token}`,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
600
|
+
await middleware(context, next);
|
|
601
|
+
expect(context.user).toEqual({
|
|
602
|
+
id: '123',
|
|
603
|
+
roles: ['admin'],
|
|
604
|
+
email: 'test@example.com',
|
|
605
|
+
});
|
|
606
|
+
expect(next).toHaveBeenCalled();
|
|
607
|
+
});
|
|
608
|
+
it('should handle extractUser returning undefined', async () => {
|
|
609
|
+
const customExtractor = () => undefined;
|
|
610
|
+
const middleware = createJWTMiddleware({ secret: SECRET, extractUser: customExtractor });
|
|
611
|
+
const token = jwt.sign({ sub: '123' }, SECRET);
|
|
612
|
+
const context = {
|
|
613
|
+
headers: {
|
|
614
|
+
authorization: `Bearer ${token}`,
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
618
|
+
await middleware(context, next);
|
|
619
|
+
expect(context.user).toBeUndefined();
|
|
620
|
+
// jwtPayload is only set when user is defined
|
|
621
|
+
expect(context.jwtPayload).toBeUndefined();
|
|
622
|
+
expect(next).toHaveBeenCalled();
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
describe('Error handling', () => {
|
|
626
|
+
it('should throw error when required and no token provided', async () => {
|
|
627
|
+
const middleware = createJWTMiddleware({ secret: SECRET, optional: false });
|
|
628
|
+
const context = {
|
|
629
|
+
headers: {},
|
|
630
|
+
};
|
|
631
|
+
const next = vi.fn();
|
|
632
|
+
await expect(middleware(context, next)).rejects.toThrow('No authorization header found');
|
|
633
|
+
});
|
|
634
|
+
it('should throw specific error for expired token', async () => {
|
|
635
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
636
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, SECRET, { expiresIn: '-1s' });
|
|
637
|
+
const context = {
|
|
638
|
+
headers: {
|
|
639
|
+
authorization: `Bearer ${token}`,
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
const next = vi.fn();
|
|
643
|
+
await expect(middleware(context, next)).rejects.toThrow('Token has expired');
|
|
644
|
+
});
|
|
645
|
+
it('should throw specific error for invalid signature', async () => {
|
|
646
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
647
|
+
const token = jwt.sign({ sub: '123', roles: ['user'] }, 'wrong-secret');
|
|
648
|
+
const context = {
|
|
649
|
+
headers: {
|
|
650
|
+
authorization: `Bearer ${token}`,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
const next = vi.fn();
|
|
654
|
+
await expect(middleware(context, next)).rejects.toThrow('Invalid token');
|
|
655
|
+
});
|
|
656
|
+
it('should throw error for malformed token', async () => {
|
|
657
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
658
|
+
const context = {
|
|
659
|
+
headers: {
|
|
660
|
+
authorization: 'Bearer malformed.token',
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
const next = vi.fn();
|
|
664
|
+
await expect(middleware(context, next)).rejects.toThrow();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
describe('JWT payload storage', () => {
|
|
668
|
+
it('should store JWT payload in context', async () => {
|
|
669
|
+
const middleware = createJWTMiddleware({ secret: SECRET });
|
|
670
|
+
const payload = { sub: '123', roles: ['user'], custom: 'data' };
|
|
671
|
+
const token = jwt.sign(payload, SECRET);
|
|
672
|
+
const context = {
|
|
673
|
+
headers: {
|
|
674
|
+
authorization: `Bearer ${token}`,
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
678
|
+
await middleware(context, next);
|
|
679
|
+
expect(context.jwtPayload).toBeDefined();
|
|
680
|
+
expect(context.jwtPayload.sub).toBe('123');
|
|
681
|
+
expect(context.jwtPayload.custom).toBe('data');
|
|
682
|
+
expect(next).toHaveBeenCalled();
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
describe('Disabled mode', () => {
|
|
686
|
+
it('should skip JWT verification when disabled', async () => {
|
|
687
|
+
const middleware = createJWTMiddleware({ enabled: false, secret: SECRET });
|
|
688
|
+
const context = {
|
|
689
|
+
headers: {
|
|
690
|
+
authorization: 'Bearer invalid-token',
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
const next = vi.fn().mockResolvedValue('success');
|
|
694
|
+
const result = await middleware(context, next);
|
|
695
|
+
expect(result).toBe('success');
|
|
696
|
+
expect(context.user).toBeUndefined();
|
|
697
|
+
expect(next).toHaveBeenCalled();
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.test.d.ts","sourceRoot":"","sources":["../../tests/types.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from 'vitest';
|
|
2
|
+
import { createAbility, detectSubjectType, defaultAbilityBuilder, createDatabaseAbilityBuilder, createConfigBasedAbilityBuilder, } from '../src/middleware/ability';
|
|
3
|
+
import { createJWTMiddleware, createJWT } from '../src/middleware/jwt';
|
|
4
|
+
import { createCaslMiddleware } from '../src/middleware/graphql';
|
|
5
|
+
describe('Type Safety Tests', () => {
|
|
6
|
+
describe('Ability Types', () => {
|
|
7
|
+
it('should have correct AppAbility type structure', () => {
|
|
8
|
+
expectTypeOf().toExtend(); // AppAbility extends PureAbility base type
|
|
9
|
+
});
|
|
10
|
+
it('should infer correct return type for createAbility', async () => {
|
|
11
|
+
const ability = await createAbility({ id: '1', roles: ['user'] });
|
|
12
|
+
expectTypeOf(ability).toEqualTypeOf();
|
|
13
|
+
});
|
|
14
|
+
it('should accept AbilityBuilderFunction as parameter', () => {
|
|
15
|
+
const builder = user => defaultAbilityBuilder(user);
|
|
16
|
+
expectTypeOf(builder).parameter(0).toEqualTypeOf();
|
|
17
|
+
expectTypeOf(builder).returns.resolves.toEqualTypeOf();
|
|
18
|
+
});
|
|
19
|
+
it('should have correct detectSubjectType signature', () => {
|
|
20
|
+
expectTypeOf(detectSubjectType).parameter(0).toEqualTypeOf();
|
|
21
|
+
expectTypeOf(detectSubjectType).returns.toEqualTypeOf();
|
|
22
|
+
});
|
|
23
|
+
it('should type database ability builder correctly', () => {
|
|
24
|
+
const fetchRules = async (userId) => [];
|
|
25
|
+
const builder = createDatabaseAbilityBuilder(fetchRules);
|
|
26
|
+
expectTypeOf(builder).parameter(0).toEqualTypeOf();
|
|
27
|
+
expectTypeOf(builder).returns.resolves.toEqualTypeOf();
|
|
28
|
+
});
|
|
29
|
+
it('should type config-based ability builder correctly', () => {
|
|
30
|
+
const config = {
|
|
31
|
+
roles: {
|
|
32
|
+
admin: [{ action: 'manage', subject: 'all' }],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const builder = createConfigBasedAbilityBuilder(config);
|
|
36
|
+
expectTypeOf(builder).parameter(0).toEqualTypeOf();
|
|
37
|
+
// Builder returns AppAbility synchronously
|
|
38
|
+
expectTypeOf(builder).toBeFunction();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('JWT Types', () => {
|
|
42
|
+
it('should have correct JWTConfig type structure', () => {
|
|
43
|
+
const config = {
|
|
44
|
+
secret: 'test',
|
|
45
|
+
enabled: true,
|
|
46
|
+
optional: false,
|
|
47
|
+
};
|
|
48
|
+
expectTypeOf(config).toExtend(); // Check config has at least these properties
|
|
49
|
+
});
|
|
50
|
+
it('should have correct JWTPayload structure', () => {
|
|
51
|
+
expectTypeOf().toHaveProperty('sub');
|
|
52
|
+
expectTypeOf().toHaveProperty('iat');
|
|
53
|
+
});
|
|
54
|
+
it('should infer correct return type for createJWT', () => {
|
|
55
|
+
const user = { id: '1', roles: ['user'] };
|
|
56
|
+
const token = createJWT(user, { secret: 'test' });
|
|
57
|
+
expectTypeOf(token).toBeString();
|
|
58
|
+
});
|
|
59
|
+
it('should type middleware correctly', () => {
|
|
60
|
+
const middleware = createJWTMiddleware({ secret: 'test' });
|
|
61
|
+
expectTypeOf(middleware).parameter(0).toEqualTypeOf();
|
|
62
|
+
expectTypeOf(middleware).parameter(1).toBeFunction();
|
|
63
|
+
expectTypeOf(middleware).returns.toEqualTypeOf();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('GraphQL Middleware Types', () => {
|
|
67
|
+
it('should have correct MiddlewareOptions type structure', () => {
|
|
68
|
+
const options = {
|
|
69
|
+
subjectMap: {},
|
|
70
|
+
actionMap: {},
|
|
71
|
+
fieldPermissions: {},
|
|
72
|
+
debug: false,
|
|
73
|
+
};
|
|
74
|
+
expectTypeOf(options).toExtend(); // Check options has at least these properties
|
|
75
|
+
});
|
|
76
|
+
it('should type middleware function correctly', () => {
|
|
77
|
+
const middleware = createCaslMiddleware();
|
|
78
|
+
expectTypeOf(middleware).toBeFunction();
|
|
79
|
+
expectTypeOf(middleware).parameter(0).toBeFunction(); // resolve
|
|
80
|
+
expectTypeOf(middleware).parameter(1).toBeAny(); // root
|
|
81
|
+
expectTypeOf(middleware).parameter(2).toBeAny(); // args
|
|
82
|
+
expectTypeOf(middleware).parameter(3).toEqualTypeOf(); // context
|
|
83
|
+
expectTypeOf(middleware).returns.toEqualTypeOf();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('Context Types', () => {
|
|
87
|
+
it('should have correct Context type structure', () => {
|
|
88
|
+
expectTypeOf().toHaveProperty('ability');
|
|
89
|
+
expectTypeOf().toHaveProperty('user');
|
|
90
|
+
});
|
|
91
|
+
it('should have correct User type structure', () => {
|
|
92
|
+
expectTypeOf().toHaveProperty('id');
|
|
93
|
+
expectTypeOf().toHaveProperty('roles');
|
|
94
|
+
const user = { id: '1', roles: ['admin'] };
|
|
95
|
+
expectTypeOf(user.id).toBeString();
|
|
96
|
+
// roles is optional, so check the array type when present
|
|
97
|
+
if (user.roles) {
|
|
98
|
+
expectTypeOf(user.roles).toEqualTypeOf();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
it('should allow optional properties on User', () => {
|
|
102
|
+
const userWithOptionals = {
|
|
103
|
+
id: '1',
|
|
104
|
+
roles: ['user'],
|
|
105
|
+
email: 'test@example.com',
|
|
106
|
+
};
|
|
107
|
+
expectTypeOf(userWithOptionals).toExtend(); // Check that object is assignable to User type
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('Ability Rule Types', () => {
|
|
111
|
+
it('should correctly type ability actions', () => {
|
|
112
|
+
const ability = defaultAbilityBuilder({ id: '1', roles: ['user'] });
|
|
113
|
+
// Type checking that these calls are valid
|
|
114
|
+
expectTypeOf(ability.can).parameter(0).toBeString();
|
|
115
|
+
expectTypeOf(ability.can).parameter(1).toEqualTypeOf();
|
|
116
|
+
});
|
|
117
|
+
it('should support subject type checking', () => {
|
|
118
|
+
const ability = defaultAbilityBuilder({ id: '1', roles: ['user'] });
|
|
119
|
+
// Verify can() method signature accepts string action and optional subject
|
|
120
|
+
expectTypeOf(ability.can).parameter(0).toBeString();
|
|
121
|
+
expectTypeOf(ability.can).toBeCallableWith('read', 'Post');
|
|
122
|
+
expectTypeOf(ability.can).toBeCallableWith('read', undefined);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('Builder Function Types', () => {
|
|
126
|
+
it('should accept sync and async builder functions', () => {
|
|
127
|
+
const syncBuilder = () => defaultAbilityBuilder();
|
|
128
|
+
const asyncBuilder = async () => defaultAbilityBuilder();
|
|
129
|
+
// Both should be valid builder functions
|
|
130
|
+
expectTypeOf(syncBuilder).toBeFunction();
|
|
131
|
+
expectTypeOf(asyncBuilder).toBeFunction();
|
|
132
|
+
expectTypeOf(asyncBuilder).returns.resolves.toEqualTypeOf();
|
|
133
|
+
});
|
|
134
|
+
it('should enforce correct parameter types for builders', () => {
|
|
135
|
+
const builder = user => {
|
|
136
|
+
expectTypeOf(user).toEqualTypeOf();
|
|
137
|
+
return defaultAbilityBuilder(user);
|
|
138
|
+
};
|
|
139
|
+
expectTypeOf(builder).toBeCallableWith(undefined);
|
|
140
|
+
expectTypeOf(builder).toBeCallableWith({ id: '1', roles: [] });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { yogaCaslPlugin } from '../src/middleware/yoga';
|
|
2
|
+
import { yogaCaslPlugin, createYogaPlugin } from '../src/middleware/yoga';
|
|
3
3
|
describe('Yoga CASL Plugin', () => {
|
|
4
4
|
it('should be a valid Yoga plugin', () => {
|
|
5
5
|
expect(yogaCaslPlugin).toBeDefined();
|
|
@@ -44,4 +44,82 @@ describe('Yoga CASL Plugin', () => {
|
|
|
44
44
|
const ability = extendedData.ability;
|
|
45
45
|
expect(ability.can('read', 'Query')).toBe(true);
|
|
46
46
|
});
|
|
47
|
+
describe('createYogaPlugin', () => {
|
|
48
|
+
it('should create a plugin with default options', () => {
|
|
49
|
+
const plugin = createYogaPlugin();
|
|
50
|
+
expect(plugin).toBeDefined();
|
|
51
|
+
expect(plugin).toHaveProperty('onExecute');
|
|
52
|
+
expect(typeof plugin.onExecute).toBe('function');
|
|
53
|
+
});
|
|
54
|
+
it('should create a plugin with custom options', () => {
|
|
55
|
+
const customBuilder = async () => {
|
|
56
|
+
const { defaultAbilityBuilder } = await import('../src/middleware/ability');
|
|
57
|
+
return defaultAbilityBuilder();
|
|
58
|
+
};
|
|
59
|
+
const plugin = createYogaPlugin({
|
|
60
|
+
abilityBuilder: customBuilder,
|
|
61
|
+
});
|
|
62
|
+
expect(plugin).toBeDefined();
|
|
63
|
+
expect(plugin).toHaveProperty('onExecute');
|
|
64
|
+
});
|
|
65
|
+
it('should log message when onExecute is called', async () => {
|
|
66
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
67
|
+
const plugin = createYogaPlugin();
|
|
68
|
+
const mockArgs = {
|
|
69
|
+
args: {
|
|
70
|
+
schema: {},
|
|
71
|
+
document: {},
|
|
72
|
+
contextValue: {},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
await plugin.onExecute(mockArgs);
|
|
76
|
+
expect(consoleSpy).toHaveBeenCalledWith('Yoga plugin not yet implemented');
|
|
77
|
+
consoleSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('yogaCaslPlugin edge cases', () => {
|
|
81
|
+
it('should handle missing onContextBuilding gracefully', async () => {
|
|
82
|
+
// Test that the plugin has the expected property
|
|
83
|
+
expect(yogaCaslPlugin.onContextBuilding).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
it('should extend context even with empty initial context', async () => {
|
|
86
|
+
let extendedData = {};
|
|
87
|
+
const extendContext = vi.fn(extension => {
|
|
88
|
+
extendedData = extension;
|
|
89
|
+
});
|
|
90
|
+
const contextParams = {
|
|
91
|
+
context: {},
|
|
92
|
+
extendContext,
|
|
93
|
+
};
|
|
94
|
+
await yogaCaslPlugin.onContextBuilding(contextParams);
|
|
95
|
+
expect(extendContext).toHaveBeenCalled();
|
|
96
|
+
expect(extendedData).toHaveProperty('ability');
|
|
97
|
+
expect(extendedData).toHaveProperty('user');
|
|
98
|
+
});
|
|
99
|
+
it('should create ability with default permissions', async () => {
|
|
100
|
+
let extendedData = {};
|
|
101
|
+
const extendContext = vi.fn(extension => {
|
|
102
|
+
extendedData = extension;
|
|
103
|
+
});
|
|
104
|
+
const contextParams = {
|
|
105
|
+
context: {},
|
|
106
|
+
extendContext,
|
|
107
|
+
};
|
|
108
|
+
await yogaCaslPlugin.onContextBuilding(contextParams);
|
|
109
|
+
const ability = extendedData.ability;
|
|
110
|
+
// Default ability should have Query read access
|
|
111
|
+
expect(ability.can('read', 'Query')).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it('should handle context extension errors', async () => {
|
|
114
|
+
const extendContext = vi.fn(() => {
|
|
115
|
+
throw new Error('Extension failed');
|
|
116
|
+
});
|
|
117
|
+
const contextParams = {
|
|
118
|
+
context: {},
|
|
119
|
+
extendContext,
|
|
120
|
+
};
|
|
121
|
+
// Should not throw, but let the error propagate
|
|
122
|
+
await expect(yogaCaslPlugin.onContextBuilding(contextParams)).rejects.toThrow('Extension failed');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
47
125
|
});
|