@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.
- package/LICENSE +90 -0
- package/README.md +9 -0
- package/package.json +8 -3
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- 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
|
-
});
|