@nauth-toolkit/core 0.1.0 → 0.1.5

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 (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +9 -0
  3. package/package.json +8 -3
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,675 +0,0 @@
1
- import { Repository, SelectQueryBuilder } from 'typeorm';
2
- import { InternalAuthAuditService as AuthAuditService } from './auth-audit.service';
3
- import { BaseAuthAudit, BaseUser } from '../entities';
4
- import { IAuthAudit, IUser } from '../interfaces/entities.interface';
5
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
6
- import { AuthAuditEventStatus } from '../entities/auth-audit.entity';
7
- import { NAuthLogger } from '../utils/nauth-logger';
8
- import { NAuthException } from '../exceptions/nauth.exception';
9
- import { ClientInfoService } from './client-info.service';
10
- import { ClientInfo } from '../interfaces/client-info.interface';
11
- import { RiskFactor } from '../enums/risk-factor.enum';
12
- import { GetUserAuthHistoryDTO } from '../dto/get-user-auth-history.dto';
13
- import { GetEventsByTypeDTO } from '../dto/get-events-by-type.dto';
14
- import { GetSuspiciousActivityDTO } from '../dto/get-suspicious-activity.dto';
15
- import { GetRiskAssessmentHistoryDTO } from '../dto/get-risk-assessment-history.dto';
16
-
17
- /**
18
- * Auth Audit Service Unit Tests
19
- *
20
- * Tests audit event recording, querying, and filtering functionality.
21
- * Covers all methods, edge cases, client info extraction, and error handling.
22
- *
23
- * Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
24
- */
25
- describe('AuthAuditService', () => {
26
- let service: AuthAuditService; // Use InternalAuthAuditService (aliased) for tests since it includes recordEvent()
27
- let mockAuditRepository: jest.Mocked<Repository<BaseAuthAudit>>;
28
- let mockUserRepository: jest.Mocked<Repository<BaseUser>>;
29
- let mockLogger: jest.Mocked<NAuthLogger>;
30
- let mockClientInfoService: jest.Mocked<ClientInfoService>;
31
- let mockQueryBuilder: jest.Mocked<SelectQueryBuilder<BaseAuthAudit>>;
32
-
33
- const mockUser: Partial<IUser> = {
34
- id: 1,
35
- sub: 'user-uuid-123',
36
- email: 'test@example.com',
37
- };
38
-
39
- const mockAuditRecord: Partial<IAuthAudit> = {
40
- id: 1,
41
- userId: 1,
42
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
43
- eventStatus: 'SUCCESS' as AuthAuditEventStatus,
44
- ipAddress: '1.2.3.4',
45
- userAgent: 'test-user-agent',
46
- createdAt: new Date(),
47
- };
48
-
49
- beforeEach(() => {
50
- // Create mock query builder
51
- mockQueryBuilder = {
52
- where: jest.fn().mockReturnThis(),
53
- andWhere: jest.fn().mockReturnThis(),
54
- orderBy: jest.fn().mockReturnThis(),
55
- skip: jest.fn().mockReturnThis(),
56
- take: jest.fn().mockReturnThis(),
57
- getManyAndCount: jest.fn(),
58
- getMany: jest.fn(),
59
- } as any;
60
-
61
- // TypeScript expects orderBy to accept 1 or 2 arguments, but mock can accept any
62
- (mockQueryBuilder.orderBy as any).mockImplementation(() => mockQueryBuilder);
63
-
64
- // Create mock repositories
65
- mockAuditRepository = {
66
- create: jest.fn(),
67
- save: jest.fn(),
68
- createQueryBuilder: jest.fn(() => mockQueryBuilder),
69
- findOne: jest.fn(),
70
- } as any;
71
-
72
- mockUserRepository = {
73
- findOne: jest.fn(),
74
- } as any;
75
-
76
- // Create mock services
77
- mockLogger = {
78
- log: jest.fn(),
79
- error: jest.fn(),
80
- warn: jest.fn(),
81
- debug: jest.fn(),
82
- } as any;
83
-
84
- mockClientInfoService = {
85
- get: jest.fn(),
86
- } as any;
87
-
88
- // Instantiate service directly (using InternalAuthAuditService aliased as AuthAuditService for tests)
89
- service = new AuthAuditService(mockAuditRepository, mockUserRepository, mockLogger, mockClientInfoService);
90
- });
91
-
92
- afterEach(() => {
93
- jest.clearAllMocks();
94
- });
95
-
96
- // ============================================================================
97
- // Service Initialization
98
- // ============================================================================
99
-
100
- it('should be defined', () => {
101
- expect(service).toBeDefined();
102
- });
103
-
104
- // ============================================================================
105
- // recordEvent() Method
106
- // ============================================================================
107
-
108
- describe('recordEvent', () => {
109
- it('should record event successfully with userId', async () => {
110
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
111
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
112
- mockClientInfoService.get.mockReturnValue({} as ClientInfo);
113
-
114
- const result = await service.recordEvent({
115
- userId: 1,
116
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
117
- eventStatus: 'SUCCESS',
118
- });
119
-
120
- expect(result).toBeDefined();
121
- expect(mockAuditRepository.create).toHaveBeenCalled();
122
- expect(mockAuditRepository.save).toHaveBeenCalled();
123
- });
124
-
125
- it('should resolve userSub to userId when userId not provided', async () => {
126
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
127
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
128
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
129
- mockClientInfoService.get.mockReturnValue({} as ClientInfo);
130
-
131
- const result = await service.recordEvent({
132
- userSub: 'user-uuid-123',
133
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
134
- eventStatus: 'SUCCESS',
135
- });
136
-
137
- expect(result).toBeDefined();
138
- expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { sub: 'user-uuid-123' } });
139
- expect(mockAuditRepository.create).toHaveBeenCalledWith((expect as any).objectContaining({ userId: 1 }));
140
- });
141
-
142
- it('should return null when userSub provided but user not found', async () => {
143
- mockUserRepository.findOne.mockResolvedValue(null);
144
-
145
- const result = await service.recordEvent({
146
- userSub: 'non-existent-user',
147
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
148
- eventStatus: 'SUCCESS',
149
- });
150
-
151
- expect(result).toBeNull();
152
- expect(mockLogger.warn).toHaveBeenCalledWith('Cannot record audit event - user not found: non-existent-user');
153
- });
154
-
155
- it('should return null when neither userId nor userSub provided', async () => {
156
- const result = await service.recordEvent({
157
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
158
- eventStatus: 'SUCCESS',
159
- } as any);
160
-
161
- expect(result).toBeNull();
162
- expect(mockLogger.warn).toHaveBeenCalledWith('Cannot record audit event - userId or userSub required');
163
- });
164
-
165
- it('should auto-extract client info from ClientInfoService when available', async () => {
166
- const clientInfo: ClientInfo = {
167
- ipAddress: '5.6.7.8',
168
- ipCountry: 'US',
169
- ipCity: 'New York',
170
- userAgent: 'Mozilla/5.0',
171
- platform: 'Windows',
172
- browser: 'Chrome',
173
- deviceToken: 'device-123',
174
- deviceName: 'My Device',
175
- deviceType: 'desktop',
176
- sessionId: 456,
177
- };
178
-
179
- mockClientInfoService.get.mockReturnValue(clientInfo);
180
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
181
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
182
-
183
- await service.recordEvent({
184
- userId: 1,
185
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
186
- eventStatus: 'SUCCESS',
187
- });
188
-
189
- expect(mockAuditRepository.create).toHaveBeenCalledWith(
190
- (expect as any).objectContaining({
191
- userId: 1,
192
- ipAddress: '5.6.7.8',
193
- ipCountry: 'US',
194
- ipCity: 'New York',
195
- userAgent: 'Mozilla/5.0',
196
- platform: 'Windows',
197
- browser: 'Chrome',
198
- deviceId: 'device-123',
199
- deviceName: 'My Device',
200
- deviceType: 'desktop',
201
- sessionId: 456,
202
- }),
203
- );
204
- });
205
-
206
- it('should allow explicit fields to override auto-extracted client info', async () => {
207
- const clientInfo: ClientInfo = {
208
- ipAddress: '5.6.7.8',
209
- userAgent: 'test-agent',
210
- ipCountry: 'US',
211
- };
212
-
213
- mockClientInfoService.get.mockReturnValue(clientInfo);
214
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
215
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
216
-
217
- await service.recordEvent({
218
- userId: 1,
219
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
220
- eventStatus: 'SUCCESS',
221
- ipAddress: '1.2.3.4', // Override
222
- ipCountry: 'CA', // Override
223
- });
224
-
225
- expect(mockAuditRepository.create).toHaveBeenCalledWith(
226
- (expect as any).objectContaining({
227
- ipAddress: '1.2.3.4', // Explicit value used
228
- ipCountry: 'CA', // Explicit value used
229
- }),
230
- );
231
- });
232
-
233
- it('should handle client info extraction errors gracefully', async () => {
234
- mockClientInfoService.get.mockImplementation(() => {
235
- throw new Error('Context not available');
236
- });
237
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
238
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
239
-
240
- const result = await service.recordEvent({
241
- userId: 1,
242
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
243
- eventStatus: 'SUCCESS',
244
- });
245
-
246
- expect(result).toBeDefined();
247
- expect(mockLogger.debug).toHaveBeenCalledWith(
248
- (expect as any).stringContaining('Failed to extract client info for audit'),
249
- );
250
- });
251
-
252
- it('should record all event fields correctly', async () => {
253
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
254
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
255
- mockClientInfoService.get.mockReturnValue({} as ClientInfo);
256
-
257
- await service.recordEvent({
258
- userId: 1,
259
- eventType: AuthAuditEventType.MFA_VERIFICATION_SUCCESS,
260
- eventStatus: 'SUCCESS',
261
- riskFactor: 75,
262
- riskFactors: [RiskFactor.NEW_DEVICE, RiskFactor.NEW_IP],
263
- adaptiveMfaTriggered: true,
264
- ipAddress: '1.2.3.4',
265
- ipCountry: 'US',
266
- ipCity: 'New York',
267
- userAgent: 'test-agent',
268
- platform: 'iOS',
269
- browser: 'Safari',
270
- deviceId: 'device-123',
271
- deviceName: 'iPhone',
272
- deviceType: 'mobile',
273
- sessionId: 789,
274
- challengeSessionId: 456,
275
- authMethod: 'password',
276
- performedBy: 'admin@example.com',
277
- reason: 'test reason',
278
- description: 'test description',
279
- metadata: { key: 'value' },
280
- });
281
-
282
- expect(mockAuditRepository.create).toHaveBeenCalledWith(
283
- (expect as any).objectContaining({
284
- userId: 1,
285
- eventType: AuthAuditEventType.MFA_VERIFICATION_SUCCESS,
286
- eventStatus: 'SUCCESS',
287
- riskFactor: 75,
288
- riskFactors: [RiskFactor.NEW_DEVICE, RiskFactor.NEW_IP],
289
- adaptiveMfaTriggered: true,
290
- ipAddress: '1.2.3.4',
291
- ipCountry: 'US',
292
- ipCity: 'New York',
293
- userAgent: 'test-agent',
294
- platform: 'iOS',
295
- browser: 'Safari',
296
- deviceId: 'device-123',
297
- deviceName: 'iPhone',
298
- deviceType: 'mobile',
299
- sessionId: 789,
300
- challengeSessionId: 456,
301
- authMethod: 'password',
302
- performedBy: 'admin@example.com',
303
- reason: 'test reason',
304
- description: 'test description',
305
- metadata: { key: 'value' },
306
- }),
307
- );
308
- });
309
-
310
- it('should handle repository save errors gracefully (non-blocking)', async () => {
311
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
312
- mockAuditRepository.save.mockRejectedValue(new Error('Database error'));
313
- mockClientInfoService.get.mockReturnValue({} as ClientInfo);
314
-
315
- const result = await service.recordEvent({
316
- userId: 1,
317
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
318
- eventStatus: 'SUCCESS',
319
- });
320
-
321
- expect(result).toBeNull();
322
- expect(mockLogger.error).toHaveBeenCalledWith(
323
- (expect as any).stringContaining('Failed to record audit event'),
324
- (expect as any).objectContaining({
325
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
326
- }),
327
- );
328
- });
329
-
330
- it('should work without ClientInfoService (optional dependency)', async () => {
331
- const serviceWithoutClientInfo = new AuthAuditService(
332
- mockAuditRepository,
333
- mockUserRepository,
334
- mockLogger,
335
- undefined, // No ClientInfoService
336
- );
337
-
338
- mockAuditRepository.create.mockReturnValue(mockAuditRecord as any);
339
- mockAuditRepository.save.mockResolvedValue(mockAuditRecord as any);
340
-
341
- const result = await serviceWithoutClientInfo.recordEvent({
342
- userId: 1,
343
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
344
- eventStatus: 'SUCCESS',
345
- });
346
-
347
- expect(result).toBeDefined();
348
- expect(mockAuditRepository.create).toHaveBeenCalled();
349
- });
350
- });
351
-
352
- // ============================================================================
353
- // getUserAuthHistory() Method
354
- // ============================================================================
355
-
356
- describe('getUserAuthHistory', () => {
357
- it('should get user auth history with default pagination', async () => {
358
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
359
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
360
-
361
- const request = new GetUserAuthHistoryDTO();
362
- request.userSub = 'user-uuid-123';
363
- const result = await service.getUserAuthHistory(request);
364
-
365
- expect(result.data).toEqual([mockAuditRecord as any]);
366
- expect(result.total).toBe(1);
367
- expect(result.page).toBe(1);
368
- expect(result.limit).toBe(50);
369
- expect(result.totalPages).toBe(1);
370
- expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { sub: 'user-uuid-123' } });
371
- expect(mockQueryBuilder.where).toHaveBeenCalledWith('audit.userId = :userId', { userId: 1 });
372
- });
373
-
374
- it('should throw error when user not found', async () => {
375
- mockUserRepository.findOne.mockResolvedValue(null);
376
-
377
- const request = new GetUserAuthHistoryDTO();
378
- request.userSub = 'non-existent-user';
379
- try {
380
- await service.getUserAuthHistory(request);
381
- fail('Should have thrown NAuthException');
382
- } catch (error: any) {
383
- expect(error).toBeInstanceOf(NAuthException);
384
- expect(error.message).toContain('User not found');
385
- }
386
- });
387
-
388
- it('should apply pagination options', async () => {
389
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
390
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 100]);
391
-
392
- const request = new GetUserAuthHistoryDTO();
393
- request.userSub = 'user-uuid-123';
394
- request.page = 2;
395
- request.limit = 25;
396
- const result = await service.getUserAuthHistory(request);
397
-
398
- expect(result.page).toBe(2);
399
- expect(result.limit).toBe(25);
400
- expect(result.totalPages).toBe(4);
401
- expect(mockQueryBuilder.skip).toHaveBeenCalledWith(25);
402
- expect(mockQueryBuilder.take).toHaveBeenCalledWith(25);
403
- });
404
-
405
- it('should filter by date range', async () => {
406
- const startDate = new Date('2025-01-01');
407
- const endDate = new Date('2025-01-31');
408
-
409
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
410
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
411
-
412
- const request = new GetUserAuthHistoryDTO();
413
- request.userSub = 'user-uuid-123';
414
- request.startDate = startDate;
415
- request.endDate = endDate;
416
- await service.getUserAuthHistory(request);
417
-
418
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.createdAt >= :startDate', { startDate });
419
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.createdAt <= :endDate', { endDate });
420
- });
421
-
422
- it('should filter by event types', async () => {
423
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
424
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
425
-
426
- const request = new GetUserAuthHistoryDTO();
427
- request.userSub = 'user-uuid-123';
428
- request.eventTypes = [AuthAuditEventType.LOGIN_SUCCESS, AuthAuditEventType.LOGIN_FAILED];
429
- await service.getUserAuthHistory(request);
430
-
431
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.eventType IN (:...eventTypes)', {
432
- eventTypes: [AuthAuditEventType.LOGIN_SUCCESS, AuthAuditEventType.LOGIN_FAILED],
433
- });
434
- });
435
-
436
- it('should filter by event status', async () => {
437
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
438
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
439
-
440
- const request = new GetUserAuthHistoryDTO();
441
- request.userSub = 'user-uuid-123';
442
- request.eventStatus = ['SUCCESS', 'FAILURE'] as AuthAuditEventStatus[];
443
- await service.getUserAuthHistory(request);
444
-
445
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.eventStatus IN (:...eventStatus)', {
446
- eventStatus: ['SUCCESS', 'FAILURE'],
447
- });
448
- });
449
-
450
- it('should order by createdAt DESC', async () => {
451
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
452
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
453
-
454
- const request = new GetUserAuthHistoryDTO();
455
- request.userSub = 'user-uuid-123';
456
- await service.getUserAuthHistory(request);
457
-
458
- // orderBy is called with 2 args but mock signature shows 1, so check it was called
459
- expect(mockQueryBuilder.orderBy).toHaveBeenCalled();
460
- });
461
-
462
- it('should calculate totalPages correctly', async () => {
463
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
464
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 100]);
465
-
466
- const request = new GetUserAuthHistoryDTO();
467
- request.userSub = 'user-uuid-123';
468
- request.limit = 30;
469
- const result = await service.getUserAuthHistory(request);
470
-
471
- expect(result.totalPages).toBe(4); // Math.ceil(100 / 30)
472
- });
473
- });
474
-
475
- // ============================================================================
476
- // getEventsByType() Method
477
- // ============================================================================
478
-
479
- describe('getEventsByType', () => {
480
- it('should get events by type with default pagination', async () => {
481
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
482
-
483
- const request = new GetEventsByTypeDTO();
484
- request.eventType = AuthAuditEventType.LOGIN_SUCCESS;
485
- const result = await service.getEventsByType(request);
486
-
487
- expect(result.data).toEqual([mockAuditRecord as any]);
488
- expect(result.total).toBe(1);
489
- expect(result.page).toBe(1);
490
- expect(result.limit).toBe(50);
491
- expect(result.totalPages).toBe(1);
492
- expect(mockQueryBuilder.where).toHaveBeenCalledWith('audit.eventType = :eventType', {
493
- eventType: AuthAuditEventType.LOGIN_SUCCESS,
494
- });
495
- });
496
-
497
- it('should apply pagination options', async () => {
498
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 50]);
499
-
500
- const request = new GetEventsByTypeDTO();
501
- request.eventType = AuthAuditEventType.LOGIN_SUCCESS;
502
- request.page = 3;
503
- request.limit = 10;
504
- const result = await service.getEventsByType(request);
505
-
506
- expect(result.page).toBe(3);
507
- expect(result.limit).toBe(10);
508
- expect(result.totalPages).toBe(5);
509
- expect(mockQueryBuilder.skip).toHaveBeenCalledWith(20);
510
- expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
511
- });
512
-
513
- it('should filter by date range', async () => {
514
- const startDate = new Date('2025-01-01');
515
- const endDate = new Date('2025-01-31');
516
-
517
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
518
-
519
- const request = new GetEventsByTypeDTO();
520
- request.eventType = AuthAuditEventType.LOGIN_SUCCESS;
521
- request.startDate = startDate;
522
- request.endDate = endDate;
523
- await service.getEventsByType(request);
524
-
525
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.createdAt >= :startDate', { startDate });
526
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.createdAt <= :endDate', { endDate });
527
- });
528
-
529
- it('should order by createdAt DESC', async () => {
530
- mockQueryBuilder.getManyAndCount.mockResolvedValue([[mockAuditRecord as any], 1]);
531
-
532
- const request = new GetEventsByTypeDTO();
533
- request.eventType = AuthAuditEventType.LOGIN_SUCCESS;
534
- await service.getEventsByType(request);
535
-
536
- // orderBy is called with 2 args but mock signature shows 1, so check it was called
537
- expect(mockQueryBuilder.orderBy).toHaveBeenCalled();
538
- });
539
- });
540
-
541
- // ============================================================================
542
- // getSuspiciousActivity() Method
543
- // ============================================================================
544
-
545
- describe('getSuspiciousActivity', () => {
546
- it('should get all suspicious activity with default limit', async () => {
547
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
548
-
549
- const request = new GetSuspiciousActivityDTO();
550
- const result = await service.getSuspiciousActivity(request);
551
-
552
- expect(result.data).toEqual([mockAuditRecord as any]);
553
- expect(mockQueryBuilder.where).toHaveBeenCalledWith(
554
- '(audit.eventStatus = :status OR audit.eventType = :eventType)',
555
- {
556
- status: 'SUSPICIOUS',
557
- eventType: AuthAuditEventType.SUSPICIOUS_ACTIVITY,
558
- },
559
- );
560
- expect(mockQueryBuilder.take).toHaveBeenCalledWith(100);
561
- });
562
-
563
- it('should filter by user when userSub provided', async () => {
564
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
565
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
566
-
567
- const request = new GetSuspiciousActivityDTO();
568
- request.userSub = 'user-uuid-123';
569
- const result = await service.getSuspiciousActivity(request);
570
-
571
- expect(result.data).toEqual([mockAuditRecord as any]);
572
- expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { sub: 'user-uuid-123' } });
573
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.userId = :userId', { userId: 1 });
574
- });
575
-
576
- it('should throw error when userSub provided but user not found', async () => {
577
- mockUserRepository.findOne.mockResolvedValue(null);
578
-
579
- try {
580
- const request = new GetSuspiciousActivityDTO();
581
- request.userSub = 'non-existent-user';
582
- await service.getSuspiciousActivity(request);
583
- fail('Should have thrown NAuthException');
584
- } catch (error: any) {
585
- expect(error).toBeInstanceOf(NAuthException);
586
- expect(error.message).toContain('User not found');
587
- }
588
- });
589
-
590
- it('should apply custom limit', async () => {
591
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
592
-
593
- const request = new GetSuspiciousActivityDTO();
594
- request.limit = 50;
595
- await service.getSuspiciousActivity(request);
596
-
597
- expect(mockQueryBuilder.take).toHaveBeenCalledWith(50);
598
- });
599
-
600
- it('should order by createdAt DESC', async () => {
601
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
602
-
603
- const request = new GetSuspiciousActivityDTO();
604
- await service.getSuspiciousActivity(request);
605
-
606
- // orderBy is called with 2 args but mock signature shows 1, so check it was called
607
- expect(mockQueryBuilder.orderBy).toHaveBeenCalled();
608
- });
609
- });
610
-
611
- // ============================================================================
612
- // getRiskAssessmentHistory() Method
613
- // ============================================================================
614
-
615
- describe('getRiskAssessmentHistory', () => {
616
- it('should get risk assessment history with default limit', async () => {
617
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
618
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
619
-
620
- const request = new GetRiskAssessmentHistoryDTO();
621
- request.userSub = 'user-uuid-123';
622
- const result = await service.getRiskAssessmentHistory(request);
623
-
624
- expect(result.data).toEqual([mockAuditRecord as any]);
625
- expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { sub: 'user-uuid-123' } });
626
- expect(mockQueryBuilder.where).toHaveBeenCalledWith('audit.userId = :userId', { userId: 1 });
627
- expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('audit.eventType IN (:...eventTypes)', {
628
- eventTypes: [
629
- AuthAuditEventType.ADAPTIVE_MFA_RISK_ASSESSED,
630
- AuthAuditEventType.ADAPTIVE_MFA_TRIGGERED,
631
- AuthAuditEventType.ADAPTIVE_MFA_BYPASSED,
632
- ],
633
- });
634
- expect(mockQueryBuilder.take).toHaveBeenCalledWith(100);
635
- });
636
-
637
- it('should throw error when user not found', async () => {
638
- mockUserRepository.findOne.mockResolvedValue(null);
639
-
640
- try {
641
- const request = new GetRiskAssessmentHistoryDTO();
642
- request.userSub = 'non-existent-user';
643
- await service.getRiskAssessmentHistory(request);
644
- fail('Should have thrown NAuthException');
645
- } catch (error: any) {
646
- expect(error).toBeInstanceOf(NAuthException);
647
- expect(error.message).toContain('User not found');
648
- }
649
- });
650
-
651
- it('should apply custom limit', async () => {
652
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
653
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
654
-
655
- const request = new GetRiskAssessmentHistoryDTO();
656
- request.userSub = 'user-uuid-123';
657
- request.limit = 50;
658
- await service.getRiskAssessmentHistory(request);
659
-
660
- expect(mockQueryBuilder.take).toHaveBeenCalledWith(50);
661
- });
662
-
663
- it('should order by createdAt DESC', async () => {
664
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
665
- mockQueryBuilder.getMany.mockResolvedValue([mockAuditRecord as any]);
666
-
667
- const request = new GetRiskAssessmentHistoryDTO();
668
- request.userSub = 'user-uuid-123';
669
- await service.getRiskAssessmentHistory(request);
670
-
671
- // orderBy is called with 2 args but mock signature shows 1, so check it was called
672
- expect(mockQueryBuilder.orderBy).toHaveBeenCalled();
673
- });
674
- });
675
- });