@trailmix-cms/cms 0.4.3 → 0.7.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.
- package/dist/auth.guard.d.ts +5 -13
- package/dist/auth.guard.d.ts.map +1 -1
- package/dist/auth.guard.js +23 -91
- package/dist/auth.guard.js.map +1 -1
- package/dist/collections/account.collection.d.ts +5 -3
- package/dist/collections/account.collection.d.ts.map +1 -1
- package/dist/collections/account.collection.js +15 -8
- package/dist/collections/account.collection.js.map +1 -1
- package/dist/collections/api-key.collection.d.ts +54 -0
- package/dist/collections/api-key.collection.d.ts.map +1 -0
- package/dist/collections/api-key.collection.js +142 -0
- package/dist/collections/api-key.collection.js.map +1 -0
- package/dist/collections/index.d.ts +4 -2
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/collections/index.js +9 -5
- package/dist/collections/index.js.map +1 -1
- package/dist/collections/organization.collection.d.ts +20 -0
- package/dist/collections/organization.collection.d.ts.map +1 -0
- package/dist/collections/{file.collection.js → organization.collection.js} +17 -17
- package/dist/collections/organization.collection.js.map +1 -0
- package/dist/collections/role.collection.d.ts +32 -0
- package/dist/collections/role.collection.d.ts.map +1 -0
- package/dist/collections/role.collection.js +90 -0
- package/dist/collections/role.collection.js.map +1 -0
- package/dist/collections/security-audit.collection.d.ts +30 -0
- package/dist/collections/security-audit.collection.d.ts.map +1 -0
- package/dist/collections/security-audit.collection.js +79 -0
- package/dist/collections/security-audit.collection.js.map +1 -0
- package/dist/constants/cms-collection-names.d.ts +4 -2
- package/dist/constants/cms-collection-names.d.ts.map +1 -1
- package/dist/constants/cms-collection-names.js +4 -2
- package/dist/constants/cms-collection-names.js.map +1 -1
- package/dist/constants/provider-symbols.d.ts +10 -12
- package/dist/constants/provider-symbols.d.ts.map +1 -1
- package/dist/constants/provider-symbols.js +10 -12
- package/dist/constants/provider-symbols.js.map +1 -1
- package/dist/controllers/account.controller.d.ts +11 -15
- package/dist/controllers/account.controller.d.ts.map +1 -1
- package/dist/controllers/account.controller.js +69 -13
- package/dist/controllers/account.controller.js.map +1 -1
- package/dist/controllers/api-keys.controller.d.ts +13 -0
- package/dist/controllers/api-keys.controller.d.ts.map +1 -0
- package/dist/controllers/api-keys.controller.js +125 -0
- package/dist/controllers/api-keys.controller.js.map +1 -0
- package/dist/controllers/audit.controller.d.ts.map +1 -1
- package/dist/controllers/audit.controller.js +3 -3
- package/dist/controllers/audit.controller.js.map +1 -1
- package/dist/controllers/audits.controller.d.ts +10 -0
- package/dist/controllers/audits.controller.d.ts.map +1 -0
- package/dist/controllers/audits.controller.js +107 -0
- package/dist/controllers/audits.controller.js.map +1 -0
- package/dist/controllers/global-roles.controller.d.ts +16 -0
- package/dist/controllers/global-roles.controller.d.ts.map +1 -0
- package/dist/controllers/global-roles.controller.js +137 -0
- package/dist/controllers/global-roles.controller.js.map +1 -0
- package/dist/controllers/index.d.ts +6 -1
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js +6 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/organization-roles.controller.d.ts +16 -0
- package/dist/controllers/organization-roles.controller.d.ts.map +1 -0
- package/dist/controllers/organization-roles.controller.js +145 -0
- package/dist/controllers/organization-roles.controller.js.map +1 -0
- package/dist/controllers/organizations.controller.d.ts +65 -0
- package/dist/controllers/organizations.controller.d.ts.map +1 -0
- package/dist/controllers/organizations.controller.js +140 -0
- package/dist/controllers/organizations.controller.js.map +1 -0
- package/dist/controllers/security-audits.controller.d.ts +11 -0
- package/dist/controllers/security-audits.controller.d.ts.map +1 -0
- package/dist/controllers/security-audits.controller.js +130 -0
- package/dist/controllers/security-audits.controller.js.map +1 -0
- package/dist/decorators/account.decorator.d.ts +1 -3
- package/dist/decorators/account.decorator.d.ts.map +1 -1
- package/dist/decorators/account.decorator.js +3 -10
- package/dist/decorators/account.decorator.js.map +1 -1
- package/dist/decorators/audit-context.decorator.d.ts +6 -0
- package/dist/decorators/audit-context.decorator.d.ts.map +1 -1
- package/dist/decorators/audit-context.decorator.js +12 -3
- package/dist/decorators/audit-context.decorator.js.map +1 -1
- package/dist/decorators/auth.decorator.d.ts +5 -3
- package/dist/decorators/auth.decorator.d.ts.map +1 -1
- package/dist/decorators/auth.decorator.js +38 -3
- package/dist/decorators/auth.decorator.js.map +1 -1
- package/dist/decorators/index.d.ts +4 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +20 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/dto/account.dto.d.ts +33 -0
- package/dist/dto/account.dto.d.ts.map +1 -0
- package/dist/dto/account.dto.js +14 -0
- package/dist/dto/account.dto.js.map +1 -0
- package/dist/dto/api-key.dto.d.ts +89 -0
- package/dist/dto/api-key.dto.d.ts.map +1 -0
- package/dist/dto/api-key.dto.js +27 -0
- package/dist/dto/api-key.dto.js.map +1 -0
- package/dist/dto/audit.dto.d.ts +11 -5
- package/dist/dto/audit.dto.d.ts.map +1 -1
- package/dist/dto/audit.dto.js +1 -1
- package/dist/dto/audit.dto.js.map +1 -1
- package/dist/dto/global-role.dto.d.ts +99 -0
- package/dist/dto/global-role.dto.d.ts.map +1 -0
- package/dist/dto/global-role.dto.js +26 -0
- package/dist/dto/global-role.dto.js.map +1 -0
- package/dist/dto/organization-role.dto.d.ts +107 -0
- package/dist/dto/organization-role.dto.d.ts.map +1 -0
- package/dist/dto/organization-role.dto.js +26 -0
- package/dist/dto/organization-role.dto.js.map +1 -0
- package/dist/dto/organization.dto.d.ts +57 -0
- package/dist/dto/organization.dto.d.ts.map +1 -0
- package/dist/dto/organization.dto.js +32 -0
- package/dist/dto/organization.dto.js.map +1 -0
- package/dist/dto/security-audit.dto.d.ts +95 -0
- package/dist/dto/security-audit.dto.d.ts.map +1 -0
- package/dist/dto/security-audit.dto.js +26 -0
- package/dist/dto/security-audit.dto.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/managers/global-role.manager.d.ts +42 -0
- package/dist/managers/global-role.manager.d.ts.map +1 -0
- package/dist/managers/global-role.manager.js +117 -0
- package/dist/managers/global-role.manager.js.map +1 -0
- package/dist/managers/index.d.ts +4 -0
- package/dist/managers/index.d.ts.map +1 -0
- package/dist/managers/index.js +20 -0
- package/dist/managers/index.js.map +1 -0
- package/dist/managers/organization-role.manager.d.ts +47 -0
- package/dist/managers/organization-role.manager.d.ts.map +1 -0
- package/dist/managers/organization-role.manager.js +218 -0
- package/dist/managers/organization-role.manager.js.map +1 -0
- package/dist/managers/organization.manager.d.ts +39 -0
- package/dist/managers/organization.manager.d.ts.map +1 -0
- package/dist/managers/organization.manager.js +196 -0
- package/dist/managers/organization.manager.js.map +1 -0
- package/dist/module.d.ts +92 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +137 -0
- package/dist/module.js.map +1 -0
- package/dist/pipes/api-key.pipe.d.ts +8 -0
- package/dist/pipes/api-key.pipe.d.ts.map +1 -0
- package/dist/pipes/api-key.pipe.js +28 -0
- package/dist/pipes/api-key.pipe.js.map +1 -0
- package/dist/pipes/organization.pipe.d.ts +8 -0
- package/dist/pipes/organization.pipe.d.ts.map +1 -0
- package/dist/pipes/organization.pipe.js +28 -0
- package/dist/pipes/organization.pipe.js.map +1 -0
- package/dist/pipes/role.pipe.d.ts +8 -0
- package/dist/pipes/{file.pipe.d.ts.map → role.pipe.d.ts.map} +1 -1
- package/dist/pipes/{file.pipe.js → role.pipe.js} +8 -8
- package/dist/pipes/{file.pipe.js.map → role.pipe.js.map} +1 -1
- package/dist/services/account.service.d.ts +0 -2
- package/dist/services/account.service.d.ts.map +1 -1
- package/dist/services/account.service.js +1 -37
- package/dist/services/account.service.js.map +1 -1
- package/dist/services/api-key.service.d.ts +42 -0
- package/dist/services/api-key.service.d.ts.map +1 -0
- package/dist/services/api-key.service.js +306 -0
- package/dist/services/api-key.service.js.map +1 -0
- package/dist/services/auth.service.d.ts +40 -0
- package/dist/services/auth.service.d.ts.map +1 -0
- package/dist/services/auth.service.js +227 -0
- package/dist/services/auth.service.js.map +1 -0
- package/dist/services/authorization.service.d.ts +44 -9
- package/dist/services/authorization.service.d.ts.map +1 -1
- package/dist/services/authorization.service.js +107 -41
- package/dist/services/authorization.service.js.map +1 -1
- package/dist/services/feature.service.d.ts +23 -0
- package/dist/services/feature.service.d.ts.map +1 -0
- package/dist/services/feature.service.js +49 -0
- package/dist/services/feature.service.js.map +1 -0
- package/dist/services/global-role.service.d.ts +17 -0
- package/dist/services/global-role.service.d.ts.map +1 -0
- package/dist/services/global-role.service.js +99 -0
- package/dist/services/global-role.service.js.map +1 -0
- package/dist/services/index.d.ts +9 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +25 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/organization-role.service.d.ts +33 -0
- package/dist/services/organization-role.service.d.ts.map +1 -0
- package/dist/services/organization-role.service.js +102 -0
- package/dist/services/organization-role.service.js.map +1 -0
- package/dist/services/organization.service.d.ts +29 -0
- package/dist/services/organization.service.d.ts.map +1 -0
- package/dist/services/organization.service.js +95 -0
- package/dist/services/organization.service.js.map +1 -0
- package/dist/types/feature-config.d.ts +9 -0
- package/dist/types/feature-config.d.ts.map +1 -0
- package/dist/types/feature-config.js +3 -0
- package/dist/types/feature-config.js.map +1 -0
- package/dist/types/hooks/auth-guard-hook.d.ts.map +1 -0
- package/dist/types/hooks/auth-guard-hook.js.map +1 -0
- package/dist/types/hooks/index.d.ts +3 -0
- package/dist/types/hooks/index.d.ts.map +1 -0
- package/dist/types/hooks/index.js +19 -0
- package/dist/types/hooks/index.js.map +1 -0
- package/dist/types/hooks/organization-delete-hook.d.ts +20 -0
- package/dist/types/hooks/organization-delete-hook.d.ts.map +1 -0
- package/dist/types/hooks/organization-delete-hook.js +3 -0
- package/dist/types/hooks/organization-delete-hook.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +21 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/request-principal.d.ts +9 -0
- package/dist/types/request-principal.d.ts.map +1 -0
- package/dist/types/request-principal.js +3 -0
- package/dist/types/request-principal.js.map +1 -0
- package/dist/utils/provider-helpers.d.ts +6 -1
- package/dist/utils/provider-helpers.d.ts.map +1 -1
- package/dist/utils/provider-helpers.js +11 -1
- package/dist/utils/provider-helpers.js.map +1 -1
- package/package.json +59 -17
- package/test/unit/collections/api-key.collection.spec.ts +416 -0
- package/test/unit/managers/global-role.manager.spec.ts +269 -0
- package/test/unit/managers/organization-role.manager.spec.ts +632 -0
- package/test/unit/managers/organization.manager.spec.ts +395 -0
- package/test/unit/module.spec.ts +596 -0
- package/test/unit/services/account.service.spec.ts +90 -0
- package/test/unit/services/api-key.service.spec.ts +1244 -0
- package/test/unit/services/auth.service.spec.ts +790 -0
- package/test/unit/services/authorization.service.spec.ts +636 -0
- package/test/unit/services/feature.service.spec.ts +56 -0
- package/test/unit/services/global-role.service.spec.ts +289 -0
- package/test/unit/services/organization-role.service.spec.ts +300 -0
- package/test/unit/services/organization.service.spec.ts +385 -0
- package/test/utils/auth-guard.ts +114 -0
- package/test/utils/base.ts +16 -0
- package/test/utils/entities/account.ts +13 -0
- package/test/utils/entities/api-key.ts +15 -0
- package/test/utils/entities/audit.ts +18 -0
- package/test/utils/entities/index.ts +6 -0
- package/test/utils/entities/mapping.ts +20 -0
- package/test/utils/entities/organization.ts +13 -0
- package/test/utils/entities/role.ts +21 -0
- package/test/utils/entities/security-audit.ts +16 -0
- package/test/utils/index.ts +4 -0
- package/test/utils/models/audit-context.ts +10 -0
- package/test/utils/models/authorization.ts +7 -0
- package/test/utils/models/global-role.ts +22 -0
- package/test/utils/models/index.ts +5 -0
- package/test/utils/models/organization-role.ts +23 -0
- package/test/utils/models/publishable.ts +7 -0
- package/tsconfig.build.json +36 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/dist/auth-guard-hook.d.ts.map +0 -1
- package/dist/auth-guard-hook.js.map +0 -1
- package/dist/cms.module.d.ts +0 -8
- package/dist/cms.module.d.ts.map +0 -1
- package/dist/cms.module.js +0 -44
- package/dist/cms.module.js.map +0 -1
- package/dist/cms.providers.d.ts +0 -120
- package/dist/cms.providers.d.ts.map +0 -1
- package/dist/cms.providers.js +0 -126
- package/dist/cms.providers.js.map +0 -1
- package/dist/collections/file.collection.d.ts +0 -21
- package/dist/collections/file.collection.d.ts.map +0 -1
- package/dist/collections/file.collection.js.map +0 -1
- package/dist/collections/text.collection.d.ts +0 -20
- package/dist/collections/text.collection.d.ts.map +0 -1
- package/dist/collections/text.collection.js +0 -56
- package/dist/collections/text.collection.js.map +0 -1
- package/dist/pipes/file.pipe.d.ts +0 -8
- /package/dist/{auth-guard-hook.d.ts → types/hooks/auth-guard-hook.d.ts} +0 -0
- /package/dist/{auth-guard-hook.js → types/hooks/auth-guard-hook.js} +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
2
|
+
import { Logger, InternalServerErrorException, ExecutionContext } from '@nestjs/common';
|
|
3
|
+
import { FastifyRequest } from 'fastify';
|
|
4
|
+
import { getAuth } from '@clerk/fastify';
|
|
5
|
+
|
|
6
|
+
import * as trailmixModels from '@trailmix-cms/models';
|
|
7
|
+
|
|
8
|
+
import { AuthService, AuthResult, GlobalRoleService } from '@/services';
|
|
9
|
+
import { AccountService } from '@/services/account.service';
|
|
10
|
+
import { ApiKeyCollection, SecurityAuditCollection } from '@/collections';
|
|
11
|
+
import { PROVIDER_SYMBOLS } from '@/constants';
|
|
12
|
+
import { type AuthGuardHook, type RequestPrincipal } from '@/types';
|
|
13
|
+
|
|
14
|
+
import * as TestUtils from '../../utils';
|
|
15
|
+
|
|
16
|
+
// Mock @clerk/fastify
|
|
17
|
+
jest.mock('@clerk/fastify', () => ({
|
|
18
|
+
getAuth: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('AuthService', () => {
|
|
22
|
+
let service: AuthService;
|
|
23
|
+
let accountService: jest.Mocked<AccountService>;
|
|
24
|
+
let globalRoleService: jest.Mocked<GlobalRoleService>;
|
|
25
|
+
let securityAuditCollection: jest.Mocked<SecurityAuditCollection>;
|
|
26
|
+
let apiKeyCollection: jest.Mocked<ApiKeyCollection>;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
// Mock Logger methods to prevent console output during tests
|
|
30
|
+
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
|
31
|
+
jest.spyOn(Logger.prototype, 'error').mockImplementation();
|
|
32
|
+
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
|
|
33
|
+
jest.spyOn(Logger.prototype, 'debug').mockImplementation();
|
|
34
|
+
jest.spyOn(Logger.prototype, 'verbose').mockImplementation();
|
|
35
|
+
|
|
36
|
+
const mockAccountService = {
|
|
37
|
+
getAccount: jest.fn(),
|
|
38
|
+
upsertAccount: jest.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mockGlobalRoleService = {
|
|
42
|
+
find: jest.fn(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const mockSecurityAuditCollection = {
|
|
46
|
+
insertOne: jest.fn().mockResolvedValue(undefined),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const mockApiKeyCollection = {
|
|
50
|
+
findOne: jest.fn(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
54
|
+
providers: [
|
|
55
|
+
AuthService,
|
|
56
|
+
{
|
|
57
|
+
provide: AccountService,
|
|
58
|
+
useValue: mockAccountService,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
provide: GlobalRoleService,
|
|
62
|
+
useValue: mockGlobalRoleService,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
provide: SecurityAuditCollection,
|
|
66
|
+
useValue: mockSecurityAuditCollection,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
provide: ApiKeyCollection,
|
|
70
|
+
useValue: mockApiKeyCollection,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}).compile();
|
|
74
|
+
|
|
75
|
+
service = module.get<AuthService>(AuthService);
|
|
76
|
+
accountService = module.get(AccountService);
|
|
77
|
+
globalRoleService = module.get(GlobalRoleService);
|
|
78
|
+
securityAuditCollection = module.get(SecurityAuditCollection);
|
|
79
|
+
apiKeyCollection = module.get(ApiKeyCollection);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
jest.clearAllMocks();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(() => {
|
|
87
|
+
jest.restoreAllMocks();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('validateAuth', () => {
|
|
91
|
+
const requestUrl = '/test/endpoint';
|
|
92
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
93
|
+
const accountPrincipal: RequestPrincipal = {
|
|
94
|
+
entity: accountEntity,
|
|
95
|
+
principal_type: trailmixModels.Principal.Account,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
describe('null principal', () => {
|
|
99
|
+
it('returns IsValid when allowAnonymous is true (ensuring anonymous access is allowed)', async () => {
|
|
100
|
+
const result = await service.validateAuth(
|
|
101
|
+
null,
|
|
102
|
+
true,
|
|
103
|
+
[],
|
|
104
|
+
[],
|
|
105
|
+
requestUrl
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
109
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns Unauthorized when allowAnonymous is false (ensuring anonymous access is blocked)', async () => {
|
|
113
|
+
const result = await service.validateAuth(
|
|
114
|
+
null,
|
|
115
|
+
false,
|
|
116
|
+
[],
|
|
117
|
+
[],
|
|
118
|
+
requestUrl
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result).toBe(AuthResult.Unauthorized);
|
|
122
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('principal type validation', () => {
|
|
127
|
+
it('returns IsValid when principal type matches required type (ensuring matching principal types pass)', async () => {
|
|
128
|
+
const result = await service.validateAuth(
|
|
129
|
+
accountPrincipal,
|
|
130
|
+
false,
|
|
131
|
+
[trailmixModels.Principal.Account],
|
|
132
|
+
[],
|
|
133
|
+
requestUrl
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
137
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns Forbidden when principal type does not match required type (ensuring non-matching principal types are rejected)', async () => {
|
|
141
|
+
const result = await service.validateAuth(
|
|
142
|
+
accountPrincipal,
|
|
143
|
+
false,
|
|
144
|
+
[trailmixModels.Principal.ApiKey],
|
|
145
|
+
[],
|
|
146
|
+
requestUrl
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result).toBe(AuthResult.Forbidden);
|
|
150
|
+
expect(securityAuditCollection.insertOne).toHaveBeenCalledWith({
|
|
151
|
+
event_type: trailmixModels.SecurityAuditEventType.UnauthorizedAccess,
|
|
152
|
+
principal_id: accountEntity._id,
|
|
153
|
+
principal_type: trailmixModels.Principal.Account,
|
|
154
|
+
message: `Unauthorized access to ${requestUrl}, required principal type not found: ${trailmixModels.Principal.ApiKey}`,
|
|
155
|
+
source: AuthService.name,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns IsValid when no principal types are required (ensuring any principal type passes when none required)', async () => {
|
|
160
|
+
const result = await service.validateAuth(
|
|
161
|
+
accountPrincipal,
|
|
162
|
+
false,
|
|
163
|
+
[],
|
|
164
|
+
[],
|
|
165
|
+
requestUrl
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
169
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('role validation', () => {
|
|
174
|
+
it('returns IsValid when no roles are required (ensuring authenticated principals pass when no roles required)', async () => {
|
|
175
|
+
const result = await service.validateAuth(
|
|
176
|
+
accountPrincipal,
|
|
177
|
+
false,
|
|
178
|
+
[],
|
|
179
|
+
[],
|
|
180
|
+
requestUrl
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
184
|
+
expect(globalRoleService.find).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns Forbidden when no global role assignments exist (ensuring null role assignments are rejected and audited)', async () => {
|
|
188
|
+
globalRoleService.find.mockResolvedValue(null as any);
|
|
189
|
+
|
|
190
|
+
const result = await service.validateAuth(
|
|
191
|
+
accountPrincipal,
|
|
192
|
+
false,
|
|
193
|
+
[],
|
|
194
|
+
[trailmixModels.RoleValue.User],
|
|
195
|
+
requestUrl
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(result).toBe(AuthResult.Forbidden);
|
|
199
|
+
expect(globalRoleService.find).toHaveBeenCalledWith({
|
|
200
|
+
principal_id: accountEntity._id,
|
|
201
|
+
principal_type: trailmixModels.Principal.Account,
|
|
202
|
+
});
|
|
203
|
+
expect(securityAuditCollection.insertOne).toHaveBeenCalledWith({
|
|
204
|
+
event_type: trailmixModels.SecurityAuditEventType.UnauthorizedAccess,
|
|
205
|
+
principal_id: accountEntity._id,
|
|
206
|
+
principal_type: trailmixModels.Principal.Account,
|
|
207
|
+
message: `Unauthorized access to ${requestUrl}, no global role assignments found`,
|
|
208
|
+
source: AuthService.name,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns Forbidden when global role assignments is empty array (ensuring empty role assignments are rejected)', async () => {
|
|
213
|
+
globalRoleService.find.mockResolvedValue([]);
|
|
214
|
+
|
|
215
|
+
const result = await service.validateAuth(
|
|
216
|
+
accountPrincipal,
|
|
217
|
+
false,
|
|
218
|
+
[],
|
|
219
|
+
[trailmixModels.RoleValue.User],
|
|
220
|
+
requestUrl
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(result).toBe(AuthResult.Forbidden);
|
|
224
|
+
expect(securityAuditCollection.insertOne).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns IsValid when principal has matching required role (ensuring matching roles pass)', async () => {
|
|
228
|
+
const userRole = TestUtils.Models.createGlobalRoleModel({
|
|
229
|
+
principal_id: accountEntity._id,
|
|
230
|
+
principal_type: trailmixModels.Principal.Account,
|
|
231
|
+
role: trailmixModels.RoleValue.User,
|
|
232
|
+
});
|
|
233
|
+
globalRoleService.find.mockResolvedValue([userRole]);
|
|
234
|
+
|
|
235
|
+
const result = await service.validateAuth(
|
|
236
|
+
accountPrincipal,
|
|
237
|
+
false,
|
|
238
|
+
[],
|
|
239
|
+
[trailmixModels.RoleValue.User],
|
|
240
|
+
requestUrl
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
244
|
+
expect(globalRoleService.find).toHaveBeenCalledWith({
|
|
245
|
+
principal_id: accountEntity._id,
|
|
246
|
+
principal_type: trailmixModels.Principal.Account,
|
|
247
|
+
});
|
|
248
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('returns IsValid when principal has Admin role even if not in required roles (ensuring Admin role grants access)', async () => {
|
|
252
|
+
const adminRole = TestUtils.Models.createGlobalRoleModel({
|
|
253
|
+
principal_id: accountEntity._id,
|
|
254
|
+
principal_type: trailmixModels.Principal.Account,
|
|
255
|
+
role: trailmixModels.RoleValue.Admin,
|
|
256
|
+
});
|
|
257
|
+
globalRoleService.find.mockResolvedValue([adminRole]);
|
|
258
|
+
|
|
259
|
+
const result = await service.validateAuth(
|
|
260
|
+
accountPrincipal,
|
|
261
|
+
false,
|
|
262
|
+
[],
|
|
263
|
+
[trailmixModels.RoleValue.User],
|
|
264
|
+
requestUrl
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
268
|
+
expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('returns IsValid when principal has one of multiple required roles (ensuring any matching role passes)', async () => {
|
|
272
|
+
const userRole = TestUtils.Models.createGlobalRoleModel({
|
|
273
|
+
principal_id: accountEntity._id,
|
|
274
|
+
principal_type: trailmixModels.Principal.Account,
|
|
275
|
+
role: trailmixModels.RoleValue.User,
|
|
276
|
+
});
|
|
277
|
+
globalRoleService.find.mockResolvedValue([userRole]);
|
|
278
|
+
|
|
279
|
+
const result = await service.validateAuth(
|
|
280
|
+
accountPrincipal,
|
|
281
|
+
false,
|
|
282
|
+
[],
|
|
283
|
+
[trailmixModels.RoleValue.Admin, trailmixModels.RoleValue.User],
|
|
284
|
+
requestUrl
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('returns Forbidden when principal does not have required role (ensuring non-matching roles are rejected)', async () => {
|
|
291
|
+
const readerRole = TestUtils.Models.createGlobalRoleModel({
|
|
292
|
+
principal_id: accountEntity._id,
|
|
293
|
+
principal_type: trailmixModels.Principal.Account,
|
|
294
|
+
role: trailmixModels.RoleValue.Reader,
|
|
295
|
+
});
|
|
296
|
+
globalRoleService.find.mockResolvedValue([readerRole]);
|
|
297
|
+
|
|
298
|
+
const result = await service.validateAuth(
|
|
299
|
+
accountPrincipal,
|
|
300
|
+
false,
|
|
301
|
+
[],
|
|
302
|
+
[trailmixModels.RoleValue.User],
|
|
303
|
+
requestUrl
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
expect(result).toBe(AuthResult.Forbidden);
|
|
307
|
+
expect(securityAuditCollection.insertOne).toHaveBeenCalledWith({
|
|
308
|
+
event_type: trailmixModels.SecurityAuditEventType.UnauthorizedAccess,
|
|
309
|
+
principal_id: accountEntity._id,
|
|
310
|
+
principal_type: trailmixModels.Principal.Account,
|
|
311
|
+
message: `Unauthorized access to ${requestUrl}, required role not found: ${trailmixModels.RoleValue.User}`,
|
|
312
|
+
source: AuthService.name,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('returns IsValid when principal has multiple roles including required one (ensuring role matching works with multiple roles)', async () => {
|
|
317
|
+
const roles = [
|
|
318
|
+
TestUtils.Models.createGlobalRoleModel({
|
|
319
|
+
principal_id: accountEntity._id,
|
|
320
|
+
principal_type: trailmixModels.Principal.Account,
|
|
321
|
+
role: trailmixModels.RoleValue.Reader,
|
|
322
|
+
}),
|
|
323
|
+
TestUtils.Models.createGlobalRoleModel({
|
|
324
|
+
principal_id: accountEntity._id,
|
|
325
|
+
principal_type: trailmixModels.Principal.Account,
|
|
326
|
+
role: trailmixModels.RoleValue.User,
|
|
327
|
+
}),
|
|
328
|
+
];
|
|
329
|
+
globalRoleService.find.mockResolvedValue(roles);
|
|
330
|
+
|
|
331
|
+
const result = await service.validateAuth(
|
|
332
|
+
accountPrincipal,
|
|
333
|
+
false,
|
|
334
|
+
[],
|
|
335
|
+
[trailmixModels.RoleValue.User],
|
|
336
|
+
requestUrl
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('allowAnonymous with principal', () => {
|
|
344
|
+
it('returns IsValid when allowAnonymous is true even with principal (ensuring anonymous flag allows authenticated principals)', async () => {
|
|
345
|
+
const result = await service.validateAuth(
|
|
346
|
+
accountPrincipal,
|
|
347
|
+
true,
|
|
348
|
+
[],
|
|
349
|
+
[trailmixModels.RoleValue.User],
|
|
350
|
+
requestUrl
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
expect(result).toBe(AuthResult.IsValid);
|
|
354
|
+
expect(globalRoleService.find).not.toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('getPrincipal', () => {
|
|
360
|
+
const createMockContext = (request: Partial<FastifyRequest>): ExecutionContext => {
|
|
361
|
+
return {
|
|
362
|
+
switchToHttp: () => ({
|
|
363
|
+
getRequest: () => request as FastifyRequest,
|
|
364
|
+
}),
|
|
365
|
+
} as ExecutionContext;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
describe('API key priority', () => {
|
|
369
|
+
it('returns API key principal when API key exists (ensuring API key takes priority over account)', async () => {
|
|
370
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey();
|
|
371
|
+
const request = {
|
|
372
|
+
headers: {
|
|
373
|
+
[trailmixModels.API_KEY_HEADER]: apiKeyEntity.api_key,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
const context = createMockContext(request);
|
|
377
|
+
|
|
378
|
+
apiKeyCollection.findOne.mockResolvedValue(apiKeyEntity);
|
|
379
|
+
|
|
380
|
+
const result = await service.getPrincipal(context);
|
|
381
|
+
|
|
382
|
+
expect(result).toEqual({
|
|
383
|
+
entity: apiKeyEntity,
|
|
384
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
385
|
+
});
|
|
386
|
+
expect(apiKeyCollection.findOne).toHaveBeenCalledWith({ api_key: apiKeyEntity.api_key });
|
|
387
|
+
expect(accountService.getAccount).not.toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('account principal', () => {
|
|
392
|
+
it('returns account principal when account exists (ensuring existing accounts are returned)', async () => {
|
|
393
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
394
|
+
const userId = 'user-123';
|
|
395
|
+
const request = {
|
|
396
|
+
headers: {},
|
|
397
|
+
};
|
|
398
|
+
const context = createMockContext(request);
|
|
399
|
+
|
|
400
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
401
|
+
accountService.getAccount.mockResolvedValue(accountEntity);
|
|
402
|
+
apiKeyCollection.findOne.mockResolvedValue(null);
|
|
403
|
+
|
|
404
|
+
const result = await service.getPrincipal(context);
|
|
405
|
+
|
|
406
|
+
expect(result).toEqual({
|
|
407
|
+
entity: accountEntity,
|
|
408
|
+
principal_type: trailmixModels.Principal.Account,
|
|
409
|
+
});
|
|
410
|
+
expect(accountService.getAccount).toHaveBeenCalledWith(userId);
|
|
411
|
+
expect(accountService.upsertAccount).not.toHaveBeenCalled();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('returns account principal after creating new account (ensuring new accounts are created and returned)', async () => {
|
|
415
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
416
|
+
const userId = 'user-123';
|
|
417
|
+
const request = {
|
|
418
|
+
headers: {},
|
|
419
|
+
};
|
|
420
|
+
const context = createMockContext(request);
|
|
421
|
+
|
|
422
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
423
|
+
accountService.getAccount.mockResolvedValue(null);
|
|
424
|
+
accountService.upsertAccount.mockResolvedValue(accountEntity);
|
|
425
|
+
apiKeyCollection.findOne.mockResolvedValue(null);
|
|
426
|
+
|
|
427
|
+
const result = await service.getPrincipal(context);
|
|
428
|
+
|
|
429
|
+
expect(result).toEqual({
|
|
430
|
+
entity: accountEntity,
|
|
431
|
+
principal_type: trailmixModels.Principal.Account,
|
|
432
|
+
});
|
|
433
|
+
expect(accountService.getAccount).toHaveBeenCalledWith(userId);
|
|
434
|
+
expect(accountService.upsertAccount).toHaveBeenCalledWith(userId);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('does not call auth guard hook when hook is not provided (ensuring optional hook does not break flow)', async () => {
|
|
438
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
439
|
+
const userId = 'user-123';
|
|
440
|
+
const request = {
|
|
441
|
+
headers: {},
|
|
442
|
+
};
|
|
443
|
+
const context = createMockContext(request);
|
|
444
|
+
|
|
445
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
446
|
+
accountService.getAccount.mockResolvedValue(null);
|
|
447
|
+
accountService.upsertAccount.mockResolvedValue(accountEntity);
|
|
448
|
+
apiKeyCollection.findOne.mockResolvedValue(null);
|
|
449
|
+
|
|
450
|
+
const result = await service.getPrincipal(context);
|
|
451
|
+
|
|
452
|
+
expect(result).not.toBeNull();
|
|
453
|
+
expect(result?.principal_type).toBe(trailmixModels.Principal.Account);
|
|
454
|
+
// Hook is not provided in default setup, so it should not be called
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('returns null when no userId and no API key (ensuring missing credentials return null)', async () => {
|
|
458
|
+
const request = {
|
|
459
|
+
headers: {},
|
|
460
|
+
};
|
|
461
|
+
const context = createMockContext(request);
|
|
462
|
+
|
|
463
|
+
(getAuth as jest.Mock).mockReturnValue({ userId: null });
|
|
464
|
+
apiKeyCollection.findOne.mockResolvedValue(null);
|
|
465
|
+
|
|
466
|
+
const result = await service.getPrincipal(context);
|
|
467
|
+
|
|
468
|
+
expect(result).toBeNull();
|
|
469
|
+
expect(accountService.getAccount).not.toHaveBeenCalled();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('getApiKey (via getPrincipal)', () => {
|
|
475
|
+
const createMockContext = (request: Partial<FastifyRequest>): ExecutionContext => {
|
|
476
|
+
return {
|
|
477
|
+
switchToHttp: () => ({
|
|
478
|
+
getRequest: () => request as FastifyRequest,
|
|
479
|
+
}),
|
|
480
|
+
} as ExecutionContext;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
it('returns null when apiKeyCollection is not provided (ensuring missing collection returns null)', async () => {
|
|
484
|
+
const moduleWithoutApiKey: TestingModule = await Test.createTestingModule({
|
|
485
|
+
providers: [
|
|
486
|
+
AuthService,
|
|
487
|
+
{
|
|
488
|
+
provide: AccountService,
|
|
489
|
+
useValue: { getAccount: jest.fn(), upsertAccount: jest.fn() },
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
provide: GlobalRoleService,
|
|
493
|
+
useValue: { find: jest.fn() },
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
provide: SecurityAuditCollection,
|
|
497
|
+
useValue: { insertOne: jest.fn() },
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
}).compile();
|
|
501
|
+
|
|
502
|
+
const serviceWithoutApiKey = moduleWithoutApiKey.get<AuthService>(AuthService);
|
|
503
|
+
const request = { headers: {} };
|
|
504
|
+
const context = createMockContext(request);
|
|
505
|
+
|
|
506
|
+
(getAuth as jest.Mock).mockReturnValue({ userId: null });
|
|
507
|
+
|
|
508
|
+
const result = await serviceWithoutApiKey.getPrincipal(context);
|
|
509
|
+
|
|
510
|
+
expect(result).toBeNull();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('returns null when API key header is missing (ensuring missing header returns null)', async () => {
|
|
514
|
+
const request = {
|
|
515
|
+
headers: {},
|
|
516
|
+
};
|
|
517
|
+
const context = createMockContext(request);
|
|
518
|
+
|
|
519
|
+
(getAuth as jest.Mock).mockReturnValue({ userId: null });
|
|
520
|
+
|
|
521
|
+
const result = await service.getPrincipal(context);
|
|
522
|
+
|
|
523
|
+
expect(result).toBeNull();
|
|
524
|
+
expect(apiKeyCollection.findOne).not.toHaveBeenCalled();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('returns null when API key not found (ensuring non-existent API keys return null)', async () => {
|
|
528
|
+
const apiKey = 'test-api-key';
|
|
529
|
+
const request = {
|
|
530
|
+
headers: {
|
|
531
|
+
[trailmixModels.API_KEY_HEADER]: apiKey,
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
const context = createMockContext(request);
|
|
535
|
+
|
|
536
|
+
apiKeyCollection.findOne.mockResolvedValue(null);
|
|
537
|
+
(getAuth as jest.Mock).mockReturnValue({ userId: null });
|
|
538
|
+
|
|
539
|
+
const result = await service.getPrincipal(context);
|
|
540
|
+
|
|
541
|
+
expect(result).toBeNull();
|
|
542
|
+
expect(apiKeyCollection.findOne).toHaveBeenCalledWith({ api_key: apiKey });
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('returns null when API key is disabled (ensuring disabled API keys return null)', async () => {
|
|
546
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({ disabled: true });
|
|
547
|
+
const request = {
|
|
548
|
+
headers: {
|
|
549
|
+
[trailmixModels.API_KEY_HEADER]: apiKeyEntity.api_key,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
const context = createMockContext(request);
|
|
553
|
+
|
|
554
|
+
apiKeyCollection.findOne.mockResolvedValue(apiKeyEntity);
|
|
555
|
+
(getAuth as jest.Mock).mockReturnValue({ userId: null });
|
|
556
|
+
|
|
557
|
+
const result = await service.getPrincipal(context);
|
|
558
|
+
|
|
559
|
+
expect(result).toBeNull();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('returns API key entity when valid (ensuring valid API keys are returned)', async () => {
|
|
563
|
+
const apiKeyEntity = TestUtils.Entities.createApiKey({ disabled: false });
|
|
564
|
+
const request = {
|
|
565
|
+
headers: {
|
|
566
|
+
[trailmixModels.API_KEY_HEADER]: apiKeyEntity.api_key,
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
const context = createMockContext(request);
|
|
570
|
+
|
|
571
|
+
apiKeyCollection.findOne.mockResolvedValue(apiKeyEntity);
|
|
572
|
+
|
|
573
|
+
const result = await service.getPrincipal(context);
|
|
574
|
+
|
|
575
|
+
expect(result).toEqual({
|
|
576
|
+
entity: apiKeyEntity,
|
|
577
|
+
principal_type: trailmixModels.Principal.ApiKey,
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('auth guard hook', () => {
|
|
583
|
+
const createMockContext = (request: Partial<FastifyRequest>): ExecutionContext => {
|
|
584
|
+
return {
|
|
585
|
+
switchToHttp: () => ({
|
|
586
|
+
getRequest: () => request as FastifyRequest,
|
|
587
|
+
}),
|
|
588
|
+
} as ExecutionContext;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
it('calls auth guard hook when creating new account (ensuring hook is called for new accounts)', async () => {
|
|
592
|
+
const mockAuthGuardHook = {
|
|
593
|
+
onHook: jest.fn().mockResolvedValue(true),
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const moduleWithHook: TestingModule = await Test.createTestingModule({
|
|
597
|
+
providers: [
|
|
598
|
+
AuthService,
|
|
599
|
+
{
|
|
600
|
+
provide: AccountService,
|
|
601
|
+
useValue: {
|
|
602
|
+
getAccount: jest.fn().mockResolvedValue(null),
|
|
603
|
+
upsertAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
provide: GlobalRoleService,
|
|
608
|
+
useValue: { find: jest.fn() },
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
provide: SecurityAuditCollection,
|
|
612
|
+
useValue: { insertOne: jest.fn() },
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
provide: ApiKeyCollection,
|
|
616
|
+
useValue: { findOne: jest.fn().mockResolvedValue(null) },
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
provide: PROVIDER_SYMBOLS.AUTH_GUARD_HOOK,
|
|
620
|
+
useValue: mockAuthGuardHook,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
}).compile();
|
|
624
|
+
|
|
625
|
+
const serviceWithHook = moduleWithHook.get<AuthService>(AuthService);
|
|
626
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
627
|
+
const userId = 'user-123';
|
|
628
|
+
const request = { headers: {} };
|
|
629
|
+
const context = createMockContext(request);
|
|
630
|
+
|
|
631
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
632
|
+
const accountServiceWithHook = moduleWithHook.get<AccountService>(AccountService);
|
|
633
|
+
(accountServiceWithHook.getAccount as jest.Mock).mockResolvedValue(null);
|
|
634
|
+
(accountServiceWithHook.upsertAccount as jest.Mock).mockResolvedValue(accountEntity);
|
|
635
|
+
|
|
636
|
+
await serviceWithHook.getPrincipal(context);
|
|
637
|
+
|
|
638
|
+
expect(mockAuthGuardHook.onHook).toHaveBeenCalledWith(accountEntity);
|
|
639
|
+
expect(mockAuthGuardHook.onHook).toHaveBeenCalledTimes(1);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('does not call auth guard hook when account already exists (ensuring hook is only called for new accounts)', async () => {
|
|
643
|
+
const mockAuthGuardHook = {
|
|
644
|
+
onHook: jest.fn().mockResolvedValue(true),
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const moduleWithHook: TestingModule = await Test.createTestingModule({
|
|
648
|
+
providers: [
|
|
649
|
+
AuthService,
|
|
650
|
+
{
|
|
651
|
+
provide: AccountService,
|
|
652
|
+
useValue: {
|
|
653
|
+
getAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
654
|
+
upsertAccount: jest.fn(),
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
provide: GlobalRoleService,
|
|
659
|
+
useValue: { find: jest.fn() },
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
provide: SecurityAuditCollection,
|
|
663
|
+
useValue: { insertOne: jest.fn() },
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
provide: ApiKeyCollection,
|
|
667
|
+
useValue: { findOne: jest.fn().mockResolvedValue(null) },
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
provide: PROVIDER_SYMBOLS.AUTH_GUARD_HOOK,
|
|
671
|
+
useValue: mockAuthGuardHook,
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
}).compile();
|
|
675
|
+
|
|
676
|
+
const serviceWithHook = moduleWithHook.get<AuthService>(AuthService);
|
|
677
|
+
const userId = 'user-123';
|
|
678
|
+
const request = { headers: {} };
|
|
679
|
+
const context = createMockContext(request);
|
|
680
|
+
|
|
681
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
682
|
+
|
|
683
|
+
await serviceWithHook.getPrincipal(context);
|
|
684
|
+
|
|
685
|
+
expect(mockAuthGuardHook.onHook).not.toHaveBeenCalled();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it('throws InternalServerErrorException when auth guard hook returns false (ensuring hook rejection throws error)', async () => {
|
|
689
|
+
const mockAuthGuardHook = {
|
|
690
|
+
onHook: jest.fn().mockResolvedValue(false),
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const moduleWithHook: TestingModule = await Test.createTestingModule({
|
|
694
|
+
providers: [
|
|
695
|
+
AuthService,
|
|
696
|
+
{
|
|
697
|
+
provide: AccountService,
|
|
698
|
+
useValue: {
|
|
699
|
+
getAccount: jest.fn().mockResolvedValue(null),
|
|
700
|
+
upsertAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
provide: GlobalRoleService,
|
|
705
|
+
useValue: { find: jest.fn() },
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
provide: SecurityAuditCollection,
|
|
709
|
+
useValue: { insertOne: jest.fn() },
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
provide: ApiKeyCollection,
|
|
713
|
+
useValue: { findOne: jest.fn().mockResolvedValue(null) },
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
provide: PROVIDER_SYMBOLS.AUTH_GUARD_HOOK,
|
|
717
|
+
useValue: mockAuthGuardHook,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
}).compile();
|
|
721
|
+
|
|
722
|
+
const serviceWithHook = moduleWithHook.get<AuthService>(AuthService);
|
|
723
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
724
|
+
const userId = 'user-123';
|
|
725
|
+
const request = { headers: {} };
|
|
726
|
+
const context = createMockContext(request);
|
|
727
|
+
|
|
728
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
729
|
+
const accountServiceWithHook = moduleWithHook.get<AccountService>(AccountService);
|
|
730
|
+
(accountServiceWithHook.getAccount as jest.Mock).mockResolvedValue(null);
|
|
731
|
+
(accountServiceWithHook.upsertAccount as jest.Mock).mockResolvedValue(accountEntity);
|
|
732
|
+
|
|
733
|
+
await expect(serviceWithHook.getPrincipal(context)).rejects.toThrow('Failed to validate account using auth guard hook');
|
|
734
|
+
expect(mockAuthGuardHook.onHook).toHaveBeenCalledWith(accountEntity);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('allows account creation to proceed when auth guard hook returns true (ensuring hook approval allows flow)', async () => {
|
|
738
|
+
const mockAuthGuardHook = {
|
|
739
|
+
onHook: jest.fn().mockResolvedValue(true),
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const moduleWithHook: TestingModule = await Test.createTestingModule({
|
|
743
|
+
providers: [
|
|
744
|
+
AuthService,
|
|
745
|
+
{
|
|
746
|
+
provide: AccountService,
|
|
747
|
+
useValue: {
|
|
748
|
+
getAccount: jest.fn().mockResolvedValue(null),
|
|
749
|
+
upsertAccount: jest.fn().mockResolvedValue(TestUtils.Entities.createAccount()),
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
provide: GlobalRoleService,
|
|
754
|
+
useValue: { find: jest.fn() },
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
provide: SecurityAuditCollection,
|
|
758
|
+
useValue: { insertOne: jest.fn() },
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
provide: ApiKeyCollection,
|
|
762
|
+
useValue: { findOne: jest.fn().mockResolvedValue(null) },
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
provide: PROVIDER_SYMBOLS.AUTH_GUARD_HOOK,
|
|
766
|
+
useValue: mockAuthGuardHook,
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
}).compile();
|
|
770
|
+
|
|
771
|
+
const serviceWithHook = moduleWithHook.get<AuthService>(AuthService);
|
|
772
|
+
const accountEntity = TestUtils.Entities.createAccount();
|
|
773
|
+
const userId = 'user-123';
|
|
774
|
+
const request = { headers: {} };
|
|
775
|
+
const context = createMockContext(request);
|
|
776
|
+
|
|
777
|
+
(getAuth as jest.Mock).mockReturnValue({ userId });
|
|
778
|
+
const accountServiceWithHook = moduleWithHook.get<AccountService>(AccountService);
|
|
779
|
+
(accountServiceWithHook.getAccount as jest.Mock).mockResolvedValue(null);
|
|
780
|
+
(accountServiceWithHook.upsertAccount as jest.Mock).mockResolvedValue(accountEntity);
|
|
781
|
+
|
|
782
|
+
const result = await serviceWithHook.getPrincipal(context);
|
|
783
|
+
|
|
784
|
+
expect(result).not.toBeNull();
|
|
785
|
+
expect(result?.principal_type).toBe(trailmixModels.Principal.Account);
|
|
786
|
+
expect(result?.entity).toEqual(accountEntity);
|
|
787
|
+
expect(mockAuthGuardHook.onHook).toHaveBeenCalledWith(accountEntity);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
});
|