@trailmix-cms/cms 0.7.1 → 0.7.2
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.
- package/dist/auth.guard.d.ts.map +1 -1
- package/dist/auth.guard.js +7 -10
- package/dist/auth.guard.js.map +1 -1
- package/dist/controllers/account.controller.js +1 -1
- package/dist/controllers/account.controller.js.map +1 -1
- package/dist/controllers/api-keys.controller.js +1 -1
- package/dist/controllers/api-keys.controller.js.map +1 -1
- package/dist/controllers/audit.controller.js +1 -1
- package/dist/controllers/audit.controller.js.map +1 -1
- package/dist/controllers/audits.controller.js +1 -1
- package/dist/controllers/audits.controller.js.map +1 -1
- package/dist/controllers/global-roles.controller.js +1 -1
- package/dist/controllers/global-roles.controller.js.map +1 -1
- package/dist/controllers/security-audits.controller.js +1 -1
- package/dist/controllers/security-audits.controller.js.map +1 -1
- package/dist/decorators/auth.decorator.d.ts +6 -7
- package/dist/decorators/auth.decorator.d.ts.map +1 -1
- package/dist/decorators/auth.decorator.js +4 -6
- package/dist/decorators/auth.decorator.js.map +1 -1
- package/dist/services/auth.service.d.ts +16 -6
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +39 -7
- package/dist/services/auth.service.js.map +1 -1
- package/dist/types/request-principal.d.ts +2 -2
- package/dist/types/request-principal.d.ts.map +1 -1
- package/package.json +7 -7
- package/test/unit/auth.guard.spec.ts +355 -0
- package/test/unit/services/auth.service.spec.ts +290 -44
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -7,9 +7,9 @@ import * as trailmixModels from '@trailmix-cms/models';
|
|
|
7
7
|
|
|
8
8
|
import { AuthService, AuthResult, GlobalRoleService } from '@/services';
|
|
9
9
|
import { AccountService } from '@/services/account.service';
|
|
10
|
-
import { ApiKeyCollection, SecurityAuditCollection } from '@/collections';
|
|
10
|
+
import { AccountCollection, ApiKeyCollection, SecurityAuditCollection } from '@/collections';
|
|
11
11
|
import { PROVIDER_SYMBOLS } from '@/constants';
|
|
12
|
-
import { type
|
|
12
|
+
import { type RequestPrincipal } from '@/types';
|
|
13
13
|
|
|
14
14
|
import * as TestUtils from '../../utils';
|
|
15
15
|
|
|
@@ -21,6 +21,7 @@ jest.mock('@clerk/fastify', () => ({
|
|
|
21
21
|
describe('AuthService', () => {
|
|
22
22
|
let service: AuthService;
|
|
23
23
|
let accountService: jest.Mocked<AccountService>;
|
|
24
|
+
let accountCollection: jest.Mocked<AccountCollection>;
|
|
24
25
|
let globalRoleService: jest.Mocked<GlobalRoleService>;
|
|
25
26
|
let securityAuditCollection: jest.Mocked<SecurityAuditCollection>;
|
|
26
27
|
let apiKeyCollection: jest.Mocked<ApiKeyCollection>;
|
|
@@ -38,6 +39,10 @@ describe('AuthService', () => {
|
|
|
38
39
|
upsertAccount: jest.fn(),
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
const mockAccountCollection = {
|
|
43
|
+
findOne: jest.fn(),
|
|
44
|
+
};
|
|
45
|
+
|
|
41
46
|
const mockGlobalRoleService = {
|
|
42
47
|
find: jest.fn(),
|
|
43
48
|
};
|
|
@@ -57,6 +62,10 @@ describe('AuthService', () => {
|
|
|
57
62
|
provide: AccountService,
|
|
58
63
|
useValue: mockAccountService,
|
|
59
64
|
},
|
|
65
|
+
{
|
|
66
|
+
provide: AccountCollection,
|
|
67
|
+
useValue: mockAccountCollection,
|
|
68
|
+
},
|
|
60
69
|
{
|
|
61
70
|
provide: GlobalRoleService,
|
|
62
71
|
useValue: mockGlobalRoleService,
|
|
@@ -74,6 +83,7 @@ describe('AuthService', () => {
|
|
|
74
83
|
|
|
75
84
|
service = module.get<AuthService>(AuthService);
|
|
76
85
|
accountService = module.get(AccountService);
|
|
86
|
+
accountCollection = module.get(AccountCollection);
|
|
77
87
|
globalRoleService = module.get(GlobalRoleService);
|
|
78
88
|
securityAuditCollection = module.get(SecurityAuditCollection);
|
|
79
89
|
apiKeyCollection = module.get(ApiKeyCollection);
|
|
@@ -99,9 +109,12 @@ describe('AuthService', () => {
|
|
|
99
109
|
it('returns IsValid when allowAnonymous is true (ensuring anonymous access is allowed)', async () => {
|
|
100
110
|
const result = await service.validateAuth(
|
|
101
111
|
null,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
{
|
|
113
|
+
allowAnonymous: true,
|
|
114
|
+
requiredPrincipalTypes: [],
|
|
115
|
+
requiredGlobalRoles: [],
|
|
116
|
+
requiredApiKeyScopes: [],
|
|
117
|
+
},
|
|
105
118
|
requestUrl
|
|
106
119
|
);
|
|
107
120
|
|
|
@@ -112,9 +125,12 @@ describe('AuthService', () => {
|
|
|
112
125
|
it('returns Unauthorized when allowAnonymous is false (ensuring anonymous access is blocked)', async () => {
|
|
113
126
|
const result = await service.validateAuth(
|
|
114
127
|
null,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
{
|
|
129
|
+
allowAnonymous: false,
|
|
130
|
+
requiredPrincipalTypes: [],
|
|
131
|
+
requiredGlobalRoles: [],
|
|
132
|
+
requiredApiKeyScopes: [],
|
|
133
|
+
},
|
|
118
134
|
requestUrl
|
|
119
135
|
);
|
|
120
136
|
|
|
@@ -127,9 +143,12 @@ describe('AuthService', () => {
|
|
|
127
143
|
it('returns IsValid when principal type matches required type (ensuring matching principal types pass)', async () => {
|
|
128
144
|
const result = await service.validateAuth(
|
|
129
145
|
accountPrincipal,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
{
|
|
147
|
+
allowAnonymous: false,
|
|
148
|
+
requiredPrincipalTypes: [trailmixModels.Principal.Account],
|
|
149
|
+
requiredGlobalRoles: [],
|
|
150
|
+
requiredApiKeyScopes: [],
|
|
151
|
+
},
|
|
133
152
|
requestUrl
|
|
134
153
|
);
|
|
135
154
|
|
|
@@ -140,9 +159,12 @@ describe('AuthService', () => {
|
|
|
140
159
|
it('returns Forbidden when principal type does not match required type (ensuring non-matching principal types are rejected)', async () => {
|
|
141
160
|
const result = await service.validateAuth(
|
|
142
161
|
accountPrincipal,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
{
|
|
163
|
+
allowAnonymous: false,
|
|
164
|
+
requiredPrincipalTypes: [trailmixModels.Principal.ApiKey],
|
|
165
|
+
requiredGlobalRoles: [],
|
|
166
|
+
requiredApiKeyScopes: [],
|
|
167
|
+
},
|
|
146
168
|
requestUrl
|
|
147
169
|
);
|
|
148
170
|
|
|
@@ -159,9 +181,108 @@ describe('AuthService', () => {
|
|
|
159
181
|
it('returns IsValid when no principal types are required (ensuring any principal type passes when none required)', async () => {
|
|
160
182
|
const result = await service.validateAuth(
|
|
161
183
|
accountPrincipal,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
184
|
+
{
|
|
185
|
+
allowAnonymous: false,
|
|
186
|
+
requiredPrincipalTypes: [],
|
|
187
|
+
requiredGlobalRoles: [],
|
|
188
|
+
requiredApiKeyScopes: [],
|
|
189
|
+
},
|
|
190
|
+
requestUrl
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
194
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('API key scope validation', () => {
|
|
199
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({
|
|
200
|
+
scope_type: trailmixModels.ApiKeyScope.Account,
|
|
201
|
+
});
|
|
202
|
+
const apiKeyPrincipal: RequestPrincipal = {
|
|
203
|
+
entity: apiKeyEntity,
|
|
204
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
it('returns IsValid when API key scope matches required scope (ensuring matching API key scopes pass)', async () => {
|
|
208
|
+
const result = await service.validateAuth(
|
|
209
|
+
apiKeyPrincipal,
|
|
210
|
+
{
|
|
211
|
+
allowAnonymous: false,
|
|
212
|
+
requiredPrincipalTypes: [],
|
|
213
|
+
requiredGlobalRoles: [],
|
|
214
|
+
requiredApiKeyScopes: [trailmixModels.ApiKeyScope.Account],
|
|
215
|
+
},
|
|
216
|
+
requestUrl
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
220
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns Forbidden when API key scope does not match required scope (ensuring non-matching API key scopes are rejected)', async () => {
|
|
224
|
+
const result = await service.validateAuth(
|
|
225
|
+
apiKeyPrincipal,
|
|
226
|
+
{
|
|
227
|
+
allowAnonymous: false,
|
|
228
|
+
requiredPrincipalTypes: [],
|
|
229
|
+
requiredGlobalRoles: [],
|
|
230
|
+
requiredApiKeyScopes: [trailmixModels.ApiKeyScope.Organization],
|
|
231
|
+
},
|
|
232
|
+
requestUrl
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(result).toBe(AuthResult.Forbidden);
|
|
236
|
+
expect(securityAuditCollection.insertOne).toHaveBeenCalledWith({
|
|
237
|
+
event_type: trailmixModels.SecurityAuditEventType.UnauthorizedAccess,
|
|
238
|
+
principal_id: apiKeyEntity._id,
|
|
239
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
240
|
+
message: `Unauthorized access to ${requestUrl}, required API key scope is not allowed:${trailmixModels.ApiKeyScope.Organization}`,
|
|
241
|
+
source: AuthService.name,
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('returns IsValid when no API key scopes are required (ensuring any API key scope passes when none required)', async () => {
|
|
246
|
+
const result = await service.validateAuth(
|
|
247
|
+
apiKeyPrincipal,
|
|
248
|
+
{
|
|
249
|
+
allowAnonymous: false,
|
|
250
|
+
requiredPrincipalTypes: [],
|
|
251
|
+
requiredGlobalRoles: [],
|
|
252
|
+
requiredApiKeyScopes: [],
|
|
253
|
+
},
|
|
254
|
+
requestUrl
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
258
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns IsValid when API key has one of multiple required scopes (ensuring any matching scope passes)', async () => {
|
|
262
|
+
const result = await service.validateAuth(
|
|
263
|
+
apiKeyPrincipal,
|
|
264
|
+
{
|
|
265
|
+
allowAnonymous: false,
|
|
266
|
+
requiredPrincipalTypes: [],
|
|
267
|
+
requiredGlobalRoles: [],
|
|
268
|
+
requiredApiKeyScopes: [trailmixModels.ApiKeyScope.Organization, trailmixModels.ApiKeyScope.Account],
|
|
269
|
+
},
|
|
270
|
+
requestUrl
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
274
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('does not check API key scopes for account principals (ensuring scope check only applies to API keys)', async () => {
|
|
278
|
+
const result = await service.validateAuth(
|
|
279
|
+
accountPrincipal,
|
|
280
|
+
{
|
|
281
|
+
allowAnonymous: false,
|
|
282
|
+
requiredPrincipalTypes: [],
|
|
283
|
+
requiredGlobalRoles: [],
|
|
284
|
+
requiredApiKeyScopes: [trailmixModels.ApiKeyScope.Account],
|
|
285
|
+
},
|
|
165
286
|
requestUrl
|
|
166
287
|
);
|
|
167
288
|
|
|
@@ -174,9 +295,12 @@ describe('AuthService', () => {
|
|
|
174
295
|
it('returns IsValid when no roles are required (ensuring authenticated principals pass when no roles required)', async () => {
|
|
175
296
|
const result = await service.validateAuth(
|
|
176
297
|
accountPrincipal,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
298
|
+
{
|
|
299
|
+
allowAnonymous: false,
|
|
300
|
+
requiredPrincipalTypes: [],
|
|
301
|
+
requiredGlobalRoles: [],
|
|
302
|
+
requiredApiKeyScopes: [],
|
|
303
|
+
},
|
|
180
304
|
requestUrl
|
|
181
305
|
);
|
|
182
306
|
|
|
@@ -189,9 +313,12 @@ describe('AuthService', () => {
|
|
|
189
313
|
|
|
190
314
|
const result = await service.validateAuth(
|
|
191
315
|
accountPrincipal,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
316
|
+
{
|
|
317
|
+
allowAnonymous: false,
|
|
318
|
+
requiredPrincipalTypes: [],
|
|
319
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
320
|
+
requiredApiKeyScopes: [],
|
|
321
|
+
},
|
|
195
322
|
requestUrl
|
|
196
323
|
);
|
|
197
324
|
|
|
@@ -214,9 +341,12 @@ describe('AuthService', () => {
|
|
|
214
341
|
|
|
215
342
|
const result = await service.validateAuth(
|
|
216
343
|
accountPrincipal,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
344
|
+
{
|
|
345
|
+
allowAnonymous: false,
|
|
346
|
+
requiredPrincipalTypes: [],
|
|
347
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
348
|
+
requiredApiKeyScopes: [],
|
|
349
|
+
},
|
|
220
350
|
requestUrl
|
|
221
351
|
);
|
|
222
352
|
|
|
@@ -234,9 +364,12 @@ describe('AuthService', () => {
|
|
|
234
364
|
|
|
235
365
|
const result = await service.validateAuth(
|
|
236
366
|
accountPrincipal,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
367
|
+
{
|
|
368
|
+
allowAnonymous: false,
|
|
369
|
+
requiredPrincipalTypes: [],
|
|
370
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
371
|
+
requiredApiKeyScopes: [],
|
|
372
|
+
},
|
|
240
373
|
requestUrl
|
|
241
374
|
);
|
|
242
375
|
|
|
@@ -258,9 +391,12 @@ describe('AuthService', () => {
|
|
|
258
391
|
|
|
259
392
|
const result = await service.validateAuth(
|
|
260
393
|
accountPrincipal,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
394
|
+
{
|
|
395
|
+
allowAnonymous: false,
|
|
396
|
+
requiredPrincipalTypes: [],
|
|
397
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
398
|
+
requiredApiKeyScopes: [],
|
|
399
|
+
},
|
|
264
400
|
requestUrl
|
|
265
401
|
);
|
|
266
402
|
|
|
@@ -278,9 +414,12 @@ describe('AuthService', () => {
|
|
|
278
414
|
|
|
279
415
|
const result = await service.validateAuth(
|
|
280
416
|
accountPrincipal,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
417
|
+
{
|
|
418
|
+
allowAnonymous: false,
|
|
419
|
+
requiredPrincipalTypes: [],
|
|
420
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.Admin, trailmixModels.RoleValue.User],
|
|
421
|
+
requiredApiKeyScopes: [],
|
|
422
|
+
},
|
|
284
423
|
requestUrl
|
|
285
424
|
);
|
|
286
425
|
|
|
@@ -297,9 +436,12 @@ describe('AuthService', () => {
|
|
|
297
436
|
|
|
298
437
|
const result = await service.validateAuth(
|
|
299
438
|
accountPrincipal,
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
439
|
+
{
|
|
440
|
+
allowAnonymous: false,
|
|
441
|
+
requiredPrincipalTypes: [],
|
|
442
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
443
|
+
requiredApiKeyScopes: [],
|
|
444
|
+
},
|
|
303
445
|
requestUrl
|
|
304
446
|
);
|
|
305
447
|
|
|
@@ -330,9 +472,12 @@ describe('AuthService', () => {
|
|
|
330
472
|
|
|
331
473
|
const result = await service.validateAuth(
|
|
332
474
|
accountPrincipal,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
475
|
+
{
|
|
476
|
+
allowAnonymous: false,
|
|
477
|
+
requiredPrincipalTypes: [],
|
|
478
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
479
|
+
requiredApiKeyScopes: [],
|
|
480
|
+
},
|
|
336
481
|
requestUrl
|
|
337
482
|
);
|
|
338
483
|
|
|
@@ -344,9 +489,12 @@ describe('AuthService', () => {
|
|
|
344
489
|
it('returns IsValid when allowAnonymous is true even with principal (ensuring anonymous flag allows authenticated principals)', async () => {
|
|
345
490
|
const result = await service.validateAuth(
|
|
346
491
|
accountPrincipal,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
492
|
+
{
|
|
493
|
+
allowAnonymous: true,
|
|
494
|
+
requiredPrincipalTypes: [],
|
|
495
|
+
requiredGlobalRoles: [trailmixModels.RoleValue.User],
|
|
496
|
+
requiredApiKeyScopes: [],
|
|
497
|
+
},
|
|
350
498
|
requestUrl
|
|
351
499
|
);
|
|
352
500
|
|
|
@@ -488,6 +636,10 @@ describe('AuthService', () => {
|
|
|
488
636
|
provide: AccountService,
|
|
489
637
|
useValue: { getAccount: jest.fn(), upsertAccount: jest.fn() },
|
|
490
638
|
},
|
|
639
|
+
{
|
|
640
|
+
provide: AccountCollection,
|
|
641
|
+
useValue: { findOne: jest.fn() },
|
|
642
|
+
},
|
|
491
643
|
{
|
|
492
644
|
provide: GlobalRoleService,
|
|
493
645
|
useValue: { find: jest.fn() },
|
|
@@ -603,6 +755,10 @@ describe('AuthService', () => {
|
|
|
603
755
|
upsertAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
604
756
|
},
|
|
605
757
|
},
|
|
758
|
+
{
|
|
759
|
+
provide: AccountCollection,
|
|
760
|
+
useValue: { findOne: jest.fn() },
|
|
761
|
+
},
|
|
606
762
|
{
|
|
607
763
|
provide: GlobalRoleService,
|
|
608
764
|
useValue: { find: jest.fn() },
|
|
@@ -654,6 +810,10 @@ describe('AuthService', () => {
|
|
|
654
810
|
upsertAccount: jest.fn(),
|
|
655
811
|
},
|
|
656
812
|
},
|
|
813
|
+
{
|
|
814
|
+
provide: AccountCollection,
|
|
815
|
+
useValue: { findOne: jest.fn() },
|
|
816
|
+
},
|
|
657
817
|
{
|
|
658
818
|
provide: GlobalRoleService,
|
|
659
819
|
useValue: { find: jest.fn() },
|
|
@@ -700,6 +860,10 @@ describe('AuthService', () => {
|
|
|
700
860
|
upsertAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
701
861
|
},
|
|
702
862
|
},
|
|
863
|
+
{
|
|
864
|
+
provide: AccountCollection,
|
|
865
|
+
useValue: { findOne: jest.fn() },
|
|
866
|
+
},
|
|
703
867
|
{
|
|
704
868
|
provide: GlobalRoleService,
|
|
705
869
|
useValue: { find: jest.fn() },
|
|
@@ -749,6 +913,10 @@ describe('AuthService', () => {
|
|
|
749
913
|
upsertAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
750
914
|
},
|
|
751
915
|
},
|
|
916
|
+
{
|
|
917
|
+
provide: AccountCollection,
|
|
918
|
+
useValue: { findOne: jest.fn() },
|
|
919
|
+
},
|
|
752
920
|
{
|
|
753
921
|
provide: GlobalRoleService,
|
|
754
922
|
useValue: { find: jest.fn() },
|
|
@@ -787,4 +955,82 @@ describe('AuthService', () => {
|
|
|
787
955
|
expect(mockAuthGuardHook.onHook).toHaveBeenCalledWith(accountEntity);
|
|
788
956
|
});
|
|
789
957
|
});
|
|
958
|
+
|
|
959
|
+
describe('getAccountFromPrincipal', () => {
|
|
960
|
+
it('returns account entity when principal type is Account (ensuring account principals return account directly)', async () => {
|
|
961
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
962
|
+
const accountPrincipal: RequestPrincipal = {
|
|
963
|
+
entity: accountEntity,
|
|
964
|
+
principal_type: trailmixModels.Principal.Account,
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const result = await service.getAccountFromPrincipal(accountPrincipal);
|
|
968
|
+
|
|
969
|
+
expect(result).toEqual(accountEntity);
|
|
970
|
+
expect(accountCollection.findOne).not.toHaveBeenCalled();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('returns account when principal is account-scoped API key (ensuring account-scoped API keys resolve to account)', async () => {
|
|
974
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
975
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({
|
|
976
|
+
scope_type: trailmixModels.ApiKeyScope.Account,
|
|
977
|
+
scope_id: accountEntity._id,
|
|
978
|
+
});
|
|
979
|
+
const apiKeyPrincipal: RequestPrincipal = {
|
|
980
|
+
entity: apiKeyEntity,
|
|
981
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
accountCollection.findOne.mockResolvedValue(accountEntity);
|
|
985
|
+
|
|
986
|
+
const result = await service.getAccountFromPrincipal(apiKeyPrincipal);
|
|
987
|
+
|
|
988
|
+
expect(result).toEqual(accountEntity);
|
|
989
|
+
expect(accountCollection.findOne).toHaveBeenCalledWith({ _id: accountEntity._id });
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it('throws error when API key is not account-scoped (ensuring non-account-scoped API keys are rejected)', async () => {
|
|
993
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({
|
|
994
|
+
scope_type: trailmixModels.ApiKeyScope.Global,
|
|
995
|
+
});
|
|
996
|
+
const apiKeyPrincipal: RequestPrincipal = {
|
|
997
|
+
entity: apiKeyEntity,
|
|
998
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
await expect(service.getAccountFromPrincipal(apiKeyPrincipal)).rejects.toThrow('API key is not account-scoped');
|
|
1002
|
+
expect(accountCollection.findOne).not.toHaveBeenCalled();
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('throws error when account-scoped API key references non-existent account (ensuring missing accounts are rejected)', async () => {
|
|
1006
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
1007
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({
|
|
1008
|
+
scope_type: trailmixModels.ApiKeyScope.Account,
|
|
1009
|
+
scope_id: accountEntity._id,
|
|
1010
|
+
});
|
|
1011
|
+
const apiKeyPrincipal: RequestPrincipal = {
|
|
1012
|
+
entity: apiKeyEntity,
|
|
1013
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
accountCollection.findOne.mockResolvedValue(null);
|
|
1017
|
+
|
|
1018
|
+
await expect(service.getAccountFromPrincipal(apiKeyPrincipal)).rejects.toThrow('Account not found');
|
|
1019
|
+
expect(accountCollection.findOne).toHaveBeenCalledWith({ _id: accountEntity._id });
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it('throws error when API key has Organization scope (ensuring organization-scoped API keys are rejected)', async () => {
|
|
1023
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({
|
|
1024
|
+
scope_type: trailmixModels.ApiKeyScope.Organization,
|
|
1025
|
+
scope_id: TestUtils.Entities.createAccount()._id,
|
|
1026
|
+
});
|
|
1027
|
+
const apiKeyPrincipal: RequestPrincipal = {
|
|
1028
|
+
entity: apiKeyEntity,
|
|
1029
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
await expect(service.getAccountFromPrincipal(apiKeyPrincipal)).rejects.toThrow('API key is not account-scoped');
|
|
1033
|
+
expect(accountCollection.findOne).not.toHaveBeenCalled();
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
790
1036
|
});
|