@stonecrop/casl-middleware 0.12.7 → 0.13.0

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 (40) hide show
  1. package/dist/index.js +3 -0
  2. package/dist/middleware/ability.js +139 -0
  3. package/dist/middleware/graphql.js +120 -0
  4. package/dist/middleware/introspection.js +169 -0
  5. package/dist/middleware/jwt.js +291 -0
  6. package/dist/middleware/postgraphile.js +80 -0
  7. package/dist/middleware/yoga.js +32 -0
  8. package/dist/src/tsdoc-metadata.json +11 -0
  9. package/dist/tests/ability.test.d.ts +2 -0
  10. package/dist/tests/ability.test.d.ts.map +1 -0
  11. package/dist/tests/ability.test.js +402 -0
  12. package/dist/tests/helpers/test-utils.d.ts +46 -0
  13. package/dist/tests/helpers/test-utils.d.ts.map +1 -0
  14. package/dist/tests/helpers/test-utils.js +92 -0
  15. package/dist/tests/introspection.test.d.ts +2 -0
  16. package/dist/tests/introspection.test.d.ts.map +1 -0
  17. package/dist/tests/introspection.test.js +368 -0
  18. package/dist/tests/jwt.test.d.ts +2 -0
  19. package/dist/tests/jwt.test.d.ts.map +1 -0
  20. package/dist/tests/jwt.test.js +700 -0
  21. package/dist/tests/middleware.test.d.ts +2 -0
  22. package/dist/tests/middleware.test.d.ts.map +1 -0
  23. package/dist/tests/middleware.test.js +184 -0
  24. package/dist/tests/postgraphile-plugin.test.d.ts +2 -0
  25. package/dist/tests/postgraphile-plugin.test.d.ts.map +1 -0
  26. package/dist/tests/postgraphile-plugin.test.js +56 -0
  27. package/dist/tests/setup.d.ts +2 -0
  28. package/dist/tests/setup.d.ts.map +1 -0
  29. package/dist/tests/setup.js +11 -0
  30. package/dist/tests/types.test.d.ts +2 -0
  31. package/dist/tests/types.test.d.ts.map +1 -0
  32. package/dist/tests/types.test.js +143 -0
  33. package/dist/tests/user-roles.test.d.ts +2 -0
  34. package/dist/tests/user-roles.test.d.ts.map +1 -0
  35. package/dist/tests/user-roles.test.js +157 -0
  36. package/dist/tests/yoga-plugin.test.d.ts +2 -0
  37. package/dist/tests/yoga-plugin.test.d.ts.map +1 -0
  38. package/dist/tests/yoga-plugin.test.js +125 -0
  39. package/dist/types/index.js +0 -0
  40. package/package.json +1 -1
@@ -0,0 +1,402 @@
1
+ import { AbilityBuilder, PureAbility, subject } from '@casl/ability';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { createAbility, detectSubjectType, defaultAbilityBuilder, createDatabaseAbilityBuilder, createConfigBasedAbilityBuilder, createAbilityFactory, } from '../src/middleware/ability';
4
+ const Ability = PureAbility;
5
+ describe('Ability Creation', () => {
6
+ describe('createAbility', () => {
7
+ // Create a test builder with proper matchers - using the same detectSubjectType as production
8
+ const testBuilder = (user) => {
9
+ const { can, cannot, build } = new AbilityBuilder(Ability);
10
+ if (!user) {
11
+ can('read', 'Query');
12
+ can('read', 'PublicType');
13
+ }
14
+ else {
15
+ can('read', 'Query');
16
+ can('read', 'Mutation');
17
+ if (user.roles?.includes('admin')) {
18
+ can('manage', 'all');
19
+ }
20
+ else {
21
+ can('read', 'User', { id: user.id });
22
+ can('update', 'User', { id: user.id });
23
+ cannot('update', 'User', 'role');
24
+ cannot('update', 'User', 'permissions');
25
+ }
26
+ if (user.roles?.includes('moderator')) {
27
+ can('update', 'Post');
28
+ can('delete', 'Comment');
29
+ }
30
+ if (user.roles?.includes('editor')) {
31
+ can('create', 'Post');
32
+ can('update', 'Post');
33
+ can('delete', 'Post');
34
+ }
35
+ }
36
+ return build({
37
+ detectSubjectType,
38
+ fieldMatcher: fields => field => {
39
+ if (!fields || !field)
40
+ return false;
41
+ return Array.isArray(fields) ? fields.includes(field) : fields === field;
42
+ },
43
+ conditionsMatcher: conditions => object => {
44
+ if (!conditions || !object)
45
+ return true;
46
+ return Object.keys(conditions).every(key => object[key] === conditions[key]);
47
+ },
48
+ });
49
+ };
50
+ it('should create a basic ability for unauthenticated users', async () => {
51
+ const ability = await createAbility(undefined, testBuilder);
52
+ expect(ability).toBeDefined();
53
+ expect(ability.can('read', 'Query')).toBe(true);
54
+ expect(ability.can('read', 'PublicType')).toBe(true);
55
+ expect(ability.can('read', 'Mutation')).toBe(false);
56
+ expect(ability.can('manage', 'all')).toBe(false);
57
+ });
58
+ it('should create abilities for authenticated users without roles', async () => {
59
+ const user = { id: '123', roles: [] };
60
+ const ability = await createAbility(user, testBuilder);
61
+ expect(ability.can('read', 'Query')).toBe(true);
62
+ expect(ability.can('read', 'Mutation')).toBe(true);
63
+ expect(ability.can('read', subject('User', { id: '123' }))).toBe(true);
64
+ expect(ability.can('update', subject('User', { id: '123' }))).toBe(true);
65
+ expect(ability.can('update', subject('User', { id: '456' }))).toBe(false);
66
+ // Check field-level restrictions
67
+ expect(ability.can('update', 'User', 'role')).toBe(false);
68
+ expect(ability.can('update', 'User', 'permissions')).toBe(false);
69
+ });
70
+ it('should create admin abilities for users with admin role', async () => {
71
+ const user = { id: '123', roles: ['admin'] };
72
+ const ability = await createAbility(user, testBuilder);
73
+ expect(ability.can('manage', 'all')).toBe(true);
74
+ expect(ability.can('read', 'Query')).toBe(true);
75
+ expect(ability.can('update', subject('User', { id: '456' }))).toBe(true);
76
+ expect(ability.can('delete', 'User')).toBe(true);
77
+ });
78
+ it('should handle mixed roles correctly', async () => {
79
+ const user = { id: '123', roles: ['user', 'moderator'] };
80
+ const ability = await createAbility(user, testBuilder);
81
+ expect(ability.can('read', 'Query')).toBe(true);
82
+ expect(ability.can('read', 'Mutation')).toBe(true);
83
+ expect(ability.can('manage', 'all')).toBe(false);
84
+ expect(ability.can('read', subject('User', { id: '123' }))).toBe(true);
85
+ expect(ability.can('update', subject('User', { id: '123' }))).toBe(true);
86
+ // Check field restrictions
87
+ expect(ability.can('update', 'User', 'role')).toBe(false);
88
+ expect(ability.can('update', 'User', 'permissions')).toBe(false);
89
+ // Check moderator permissions
90
+ expect(ability.can('update', 'Post')).toBe(true);
91
+ expect(ability.can('delete', 'Comment')).toBe(true);
92
+ });
93
+ });
94
+ describe('Custom Ability Builder', () => {
95
+ it('should allow custom ability definitions', () => {
96
+ const customCreateAbility = (user) => {
97
+ const { can, build } = new AbilityBuilder(Ability);
98
+ if (user?.permissions?.includes('read:posts')) {
99
+ can('read', 'Post');
100
+ }
101
+ if (user?.permissions?.includes('write:posts')) {
102
+ can(['create', 'update'], 'Post', { authorId: user.id });
103
+ }
104
+ if (user?.permissions?.includes('delete:posts')) {
105
+ can('delete', 'Post', { authorId: user.id });
106
+ }
107
+ return build({
108
+ detectSubjectType,
109
+ conditionsMatcher: conditions => object => {
110
+ if (!conditions || !object)
111
+ return true;
112
+ return Object.keys(conditions).every(key => object[key] === conditions[key]);
113
+ },
114
+ });
115
+ };
116
+ const user = { id: '123', permissions: ['read:posts', 'write:posts'] };
117
+ const ability = customCreateAbility(user);
118
+ expect(ability.can('read', 'Post')).toBe(true);
119
+ expect(ability.can('create', subject('Post', { authorId: '123' }))).toBe(true);
120
+ expect(ability.can('update', subject('Post', { authorId: '123' }))).toBe(true);
121
+ expect(ability.can('delete', subject('Post', { authorId: '123' }))).toBe(false);
122
+ expect(ability.can('update', subject('Post', { authorId: '456' }))).toBe(false);
123
+ });
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
+ });
402
+ });
@@ -0,0 +1,46 @@
1
+ import { PureAbility } from '@casl/ability';
2
+ import { GraphQLSchema } from 'graphql';
3
+ import type { AppAbility } from '../../src/middleware/ability';
4
+ import { Context, User } from '../../src/types';
5
+ /**
6
+ * Create a mock GraphQL context for testing
7
+ */
8
+ export declare const createMockContext: (user?: User, ability?: AppAbility) => Context;
9
+ /**
10
+ * Create a simple test schema
11
+ */
12
+ export declare const createTestSchema: () => GraphQLSchema;
13
+ /**
14
+ * Create test users with different roles
15
+ */
16
+ export declare const testUsers: {
17
+ public: undefined;
18
+ regular: {
19
+ id: string;
20
+ roles: string[];
21
+ };
22
+ moderator: {
23
+ id: string;
24
+ roles: string[];
25
+ };
26
+ editor: {
27
+ id: string;
28
+ roles: string[];
29
+ };
30
+ admin: {
31
+ id: string;
32
+ roles: string[];
33
+ };
34
+ };
35
+ /**
36
+ * Assertion helper for checking abilities
37
+ */
38
+ export declare const expectAbility: (ability: PureAbility) => {
39
+ toAllow: (action: string, subject: string, conditions?: any) => void;
40
+ toDeny: (action: string, subject: string, conditions?: any) => void;
41
+ };
42
+ /**
43
+ * Mock GraphQL ResolveInfo for testing
44
+ */
45
+ export declare const createMockResolveInfo: (overrides?: any) => any;
46
+ //# sourceMappingURL=test-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../tests/helpers/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE/C;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,IAAI,EAAE,UAAU,UAAU,KAAG,OAOrE,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAO,aA4CnC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;CAMrB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,WAAW;sBAC/B,MAAM,WAAW,MAAM,eAAe,GAAG;qBAG1C,MAAM,WAAW,MAAM,eAAe,GAAG;CAGzD,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAI,YAAW,GAAQ,KAAG,GAS1D,CAAA"}
@@ -0,0 +1,92 @@
1
+ import { PureAbility } from '@casl/ability';
2
+ import { makeExecutableSchema } from '@graphql-tools/schema';
3
+ /**
4
+ * Create a mock GraphQL context for testing
5
+ */
6
+ export const createMockContext = (user, ability) => {
7
+ const defaultAbility = new PureAbility([{ action: 'read', subject: 'Query' }]);
8
+ return {
9
+ user: user || undefined,
10
+ ability: ability || defaultAbility,
11
+ };
12
+ };
13
+ /**
14
+ * Create a simple test schema
15
+ */
16
+ export const createTestSchema = () => {
17
+ const typeDefs = `
18
+ type User {
19
+ id: ID!
20
+ name: String!
21
+ email: String
22
+ role: String
23
+ }
24
+
25
+ type Post {
26
+ id: ID!
27
+ title: String!
28
+ content: String!
29
+ authorId: String!
30
+ }
31
+
32
+ type Query {
33
+ me: User
34
+ user(id: ID!): User
35
+ posts: [Post!]!
36
+ }
37
+
38
+ type Mutation {
39
+ updateUser(id: ID!, name: String): User
40
+ createPost(title: String!, content: String!): Post
41
+ }
42
+ `;
43
+ const resolvers = {
44
+ Query: {
45
+ me: () => ({ id: '123', name: 'Test User', email: 'test@example.com', role: 'user' }),
46
+ user: (_, { id }) => ({ id, name: `User ${id}`, email: `user${id}@example.com`, role: 'user' }),
47
+ posts: () => [
48
+ { id: '1', title: 'Post 1', content: 'Content 1', authorId: '123' },
49
+ { id: '2', title: 'Post 2', content: 'Content 2', authorId: '456' },
50
+ ],
51
+ },
52
+ Mutation: {
53
+ updateUser: (_, { id, name }) => ({ id, name: name || 'Updated', email: 'updated@example.com' }),
54
+ createPost: (_, { title, content }) => ({ id: '3', title, content, authorId: '123' }),
55
+ },
56
+ };
57
+ return makeExecutableSchema({ typeDefs, resolvers });
58
+ };
59
+ /**
60
+ * Create test users with different roles
61
+ */
62
+ export const testUsers = {
63
+ public: undefined,
64
+ regular: { id: '123', roles: ['user'] },
65
+ moderator: { id: '456', roles: ['user', 'moderator'] },
66
+ editor: { id: '789', roles: ['user', 'editor'] },
67
+ admin: { id: '999', roles: ['admin'] },
68
+ };
69
+ /**
70
+ * Assertion helper for checking abilities
71
+ */
72
+ export const expectAbility = (ability) => ({
73
+ toAllow: (action, subject, conditions) => {
74
+ expect(ability.can(action, subject, conditions)).toBe(true);
75
+ },
76
+ toDeny: (action, subject, conditions) => {
77
+ expect(ability.can(action, subject, conditions)).toBe(false);
78
+ },
79
+ });
80
+ /**
81
+ * Mock GraphQL ResolveInfo for testing
82
+ */
83
+ export const createMockResolveInfo = (overrides = {}) => ({
84
+ fieldName: 'testField',
85
+ parentType: { name: 'Query' },
86
+ path: { typename: 'Query', key: 'testField' },
87
+ operation: {
88
+ operation: 'query',
89
+ loc: { source: { body: 'query { testField }' } },
90
+ },
91
+ ...overrides,
92
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=introspection.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"introspection.test.d.ts","sourceRoot":"","sources":["../../tests/introspection.test.ts"],"names":[],"mappings":""}