@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,693 +0,0 @@
|
|
|
1
|
-
import { Repository } from 'typeorm';
|
|
2
|
-
import { MFAService } from './mfa.service';
|
|
3
|
-
import { BaseMFADevice, BaseUser } from '../entities';
|
|
4
|
-
import { IUser, IMFADevice } from '../interfaces/entities.interface';
|
|
5
|
-
import { IMFAProviderService } from '../interfaces/mfa-provider.interface';
|
|
6
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
7
|
-
import { MFAMethod, MFADeviceMethod } from '../enums/mfa-method.enum';
|
|
8
|
-
import { ChallengeService } from './challenge.service';
|
|
9
|
-
import { AuthChallenge } from '../dto/auth-challenge.dto';
|
|
10
|
-
import { NAuthConfig } from '../interfaces/config.interface';
|
|
11
|
-
import { NAuthLogger } from '../utils/nauth-logger';
|
|
12
|
-
import { AuthAuditService } from './auth-audit.service';
|
|
13
|
-
import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
|
|
14
|
-
import { ClientInfoService } from './client-info.service';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* MFA Service Unit Tests
|
|
18
|
-
*
|
|
19
|
-
* Tests MFA provider registry, verification routing, device management,
|
|
20
|
-
* and user preference updates.
|
|
21
|
-
*
|
|
22
|
-
* Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
|
|
23
|
-
*/
|
|
24
|
-
describe('MFAService', () => {
|
|
25
|
-
let service: MFAService;
|
|
26
|
-
let mockMfaDeviceRepository: jest.Mocked<Repository<BaseMFADevice>>;
|
|
27
|
-
let mockUserRepository: jest.Mocked<Repository<BaseUser>>;
|
|
28
|
-
let mockChallengeService: jest.Mocked<ChallengeService>;
|
|
29
|
-
let mockLogger: jest.Mocked<NAuthLogger>;
|
|
30
|
-
let mockAuditService: jest.Mocked<AuthAuditService>;
|
|
31
|
-
let mockClientInfoService: jest.Mocked<ClientInfoService>;
|
|
32
|
-
let mockProvider1: jest.Mocked<IMFAProviderService>;
|
|
33
|
-
let mockProvider2: jest.Mocked<IMFAProviderService>;
|
|
34
|
-
|
|
35
|
-
const mockConfig: Partial<NAuthConfig> = {
|
|
36
|
-
mfa: {
|
|
37
|
-
enabled: true,
|
|
38
|
-
enforcement: 'OPTIONAL',
|
|
39
|
-
allowedMethods: [MFAMethod.TOTP, MFAMethod.SMS, MFAMethod.PASSKEY],
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const mockUser: Partial<IUser> = {
|
|
44
|
-
id: 1,
|
|
45
|
-
sub: 'user-uuid-123',
|
|
46
|
-
email: 'test@example.com',
|
|
47
|
-
mfaEnabled: true,
|
|
48
|
-
mfaMethods: ['totp'],
|
|
49
|
-
preferredMfaMethod: 'totp',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const mockDevice: Partial<IMFADevice> = {
|
|
53
|
-
id: 1,
|
|
54
|
-
userId: 1,
|
|
55
|
-
type: 'totp' as MFADeviceMethod,
|
|
56
|
-
name: 'My Device',
|
|
57
|
-
isActive: true,
|
|
58
|
-
isPrimary: true,
|
|
59
|
-
createdAt: new Date(),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
// Create mock providers
|
|
64
|
-
mockProvider1 = {
|
|
65
|
-
methodName: 'totp',
|
|
66
|
-
isMethodAllowed: jest.fn().mockReturnValue(true),
|
|
67
|
-
setup: jest.fn(),
|
|
68
|
-
verifySetup: jest.fn(),
|
|
69
|
-
verify: jest.fn(),
|
|
70
|
-
} as any;
|
|
71
|
-
|
|
72
|
-
mockProvider2 = {
|
|
73
|
-
methodName: 'sms',
|
|
74
|
-
isMethodAllowed: jest.fn().mockReturnValue(true),
|
|
75
|
-
setup: jest.fn(),
|
|
76
|
-
verifySetup: jest.fn(),
|
|
77
|
-
verify: jest.fn(),
|
|
78
|
-
} as any;
|
|
79
|
-
|
|
80
|
-
// Create mock repositories
|
|
81
|
-
mockMfaDeviceRepository = {
|
|
82
|
-
find: jest.fn(),
|
|
83
|
-
delete: jest.fn(),
|
|
84
|
-
update: jest.fn(),
|
|
85
|
-
create: jest.fn(),
|
|
86
|
-
save: jest.fn(),
|
|
87
|
-
} as any;
|
|
88
|
-
|
|
89
|
-
mockUserRepository = {
|
|
90
|
-
findOne: jest.fn(),
|
|
91
|
-
save: jest.fn(),
|
|
92
|
-
update: jest.fn(),
|
|
93
|
-
} as any;
|
|
94
|
-
|
|
95
|
-
// Create mock services
|
|
96
|
-
mockChallengeService = {
|
|
97
|
-
createChallengeSession: jest.fn(),
|
|
98
|
-
} as any;
|
|
99
|
-
|
|
100
|
-
mockLogger = {
|
|
101
|
-
log: jest.fn(),
|
|
102
|
-
error: jest.fn(),
|
|
103
|
-
warn: jest.fn(),
|
|
104
|
-
debug: jest.fn(),
|
|
105
|
-
} as any;
|
|
106
|
-
|
|
107
|
-
mockAuditService = {
|
|
108
|
-
recordEvent: jest.fn(),
|
|
109
|
-
} as any;
|
|
110
|
-
|
|
111
|
-
mockClientInfoService = {
|
|
112
|
-
get: jest.fn(),
|
|
113
|
-
} as any;
|
|
114
|
-
|
|
115
|
-
// Instantiate service directly
|
|
116
|
-
service = new MFAService(
|
|
117
|
-
mockMfaDeviceRepository,
|
|
118
|
-
mockUserRepository,
|
|
119
|
-
mockChallengeService,
|
|
120
|
-
mockConfig as NAuthConfig,
|
|
121
|
-
mockLogger,
|
|
122
|
-
mockAuditService,
|
|
123
|
-
mockClientInfoService,
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
afterEach(() => {
|
|
128
|
-
jest.clearAllMocks();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ============================================================================
|
|
132
|
-
// Service Initialization
|
|
133
|
-
// ============================================================================
|
|
134
|
-
|
|
135
|
-
it('should be defined', () => {
|
|
136
|
-
expect(service).toBeDefined();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// ============================================================================
|
|
140
|
-
// registerProvider() Method
|
|
141
|
-
// ============================================================================
|
|
142
|
-
|
|
143
|
-
describe('registerProvider', () => {
|
|
144
|
-
it('should register provider successfully', () => {
|
|
145
|
-
service.registerProvider(mockProvider1);
|
|
146
|
-
|
|
147
|
-
expect(service.hasProvider('totp')).toBe(true);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should throw error when provider already registered', () => {
|
|
151
|
-
service.registerProvider(mockProvider1);
|
|
152
|
-
|
|
153
|
-
expect(() => service.registerProvider(mockProvider1)).toThrow(NAuthException);
|
|
154
|
-
expect(() => service.registerProvider(mockProvider1)).toThrow('already registered');
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should allow multiple different providers', () => {
|
|
158
|
-
service.registerProvider(mockProvider1);
|
|
159
|
-
service.registerProvider(mockProvider2);
|
|
160
|
-
|
|
161
|
-
expect(service.hasProvider('totp')).toBe(true);
|
|
162
|
-
expect(service.hasProvider('sms')).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// ============================================================================
|
|
167
|
-
// getProvider() Method
|
|
168
|
-
// ============================================================================
|
|
169
|
-
|
|
170
|
-
describe('getProvider', () => {
|
|
171
|
-
it('should return registered provider', () => {
|
|
172
|
-
service.registerProvider(mockProvider1);
|
|
173
|
-
|
|
174
|
-
const provider = service.getProvider('totp');
|
|
175
|
-
|
|
176
|
-
expect(provider).toBe(mockProvider1);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should throw error when provider not registered', () => {
|
|
180
|
-
expect(() => service.getProvider('totp')).toThrow(NAuthException);
|
|
181
|
-
expect(() => service.getProvider('totp')).toThrow('not registered');
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ============================================================================
|
|
186
|
-
// hasProvider() Method
|
|
187
|
-
// ============================================================================
|
|
188
|
-
|
|
189
|
-
describe('hasProvider', () => {
|
|
190
|
-
it('should return true for registered provider', () => {
|
|
191
|
-
service.registerProvider(mockProvider1);
|
|
192
|
-
|
|
193
|
-
expect(service.hasProvider('totp')).toBe(true);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should return false for unregistered provider', () => {
|
|
197
|
-
expect(service.hasProvider('totp')).toBe(false);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// ============================================================================
|
|
202
|
-
// listProviders() Method
|
|
203
|
-
// ============================================================================
|
|
204
|
-
|
|
205
|
-
describe('listProviders', () => {
|
|
206
|
-
it('should return empty array when no providers registered', () => {
|
|
207
|
-
expect(service.listProviders()).toEqual([]);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should return all registered provider names', () => {
|
|
211
|
-
service.registerProvider(mockProvider1);
|
|
212
|
-
service.registerProvider(mockProvider2);
|
|
213
|
-
|
|
214
|
-
const providers = service.listProviders();
|
|
215
|
-
|
|
216
|
-
expect(providers).toContain('totp');
|
|
217
|
-
expect(providers).toContain('sms');
|
|
218
|
-
expect(providers.length).toBe(2);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ============================================================================
|
|
223
|
-
// getAvailableMethods() Method
|
|
224
|
-
// ============================================================================
|
|
225
|
-
|
|
226
|
-
describe('getAvailableMethods', () => {
|
|
227
|
-
it('should return only allowed methods', async () => {
|
|
228
|
-
service.registerProvider(mockProvider1);
|
|
229
|
-
service.registerProvider(mockProvider2);
|
|
230
|
-
|
|
231
|
-
const methods = await service.getAvailableMethods(mockUser as IUser);
|
|
232
|
-
|
|
233
|
-
expect(methods).toContain('totp');
|
|
234
|
-
expect(methods).toContain('sms');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should filter out methods not allowed by provider', async () => {
|
|
238
|
-
const restrictedProvider = {
|
|
239
|
-
...mockProvider1,
|
|
240
|
-
isMethodAllowed: jest.fn().mockReturnValue(false),
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
service.registerProvider(restrictedProvider);
|
|
244
|
-
|
|
245
|
-
const methods = await service.getAvailableMethods(mockUser as IUser);
|
|
246
|
-
|
|
247
|
-
expect(methods).not.toContain('totp');
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('should return empty array when no providers registered', async () => {
|
|
251
|
-
const methods = await service.getAvailableMethods(mockUser as IUser);
|
|
252
|
-
|
|
253
|
-
expect(methods).toEqual([]);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ============================================================================
|
|
258
|
-
// verifyCode() Method
|
|
259
|
-
// ============================================================================
|
|
260
|
-
|
|
261
|
-
describe('verifyCode', () => {
|
|
262
|
-
beforeEach(() => {
|
|
263
|
-
service.registerProvider(mockProvider1);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should route verification to correct provider', async () => {
|
|
267
|
-
mockProvider1.verify.mockResolvedValue(true);
|
|
268
|
-
|
|
269
|
-
const result = await service.verifyCode(mockUser as IUser, 'totp', '123456');
|
|
270
|
-
|
|
271
|
-
expect(result).toBe(true);
|
|
272
|
-
expect(mockProvider1.verify).toHaveBeenCalledWith(mockUser as IUser, '123456', undefined);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it('should throw error when provider not registered', async () => {
|
|
276
|
-
try {
|
|
277
|
-
await service.verifyCode(mockUser as IUser, 'sms', '123456');
|
|
278
|
-
fail('Should have thrown NAuthException');
|
|
279
|
-
} catch (error: any) {
|
|
280
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should handle backup code verification', async () => {
|
|
285
|
-
// Create a new service instance to avoid provider already registered error
|
|
286
|
-
const serviceForBackupTest = new MFAService(
|
|
287
|
-
mockMfaDeviceRepository,
|
|
288
|
-
mockUserRepository,
|
|
289
|
-
mockChallengeService,
|
|
290
|
-
mockConfig as NAuthConfig,
|
|
291
|
-
mockLogger,
|
|
292
|
-
mockAuditService,
|
|
293
|
-
mockClientInfoService,
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
const providerWithBackup = {
|
|
297
|
-
...mockProvider1,
|
|
298
|
-
verifyBackupCode: jest.fn().mockResolvedValue(true),
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
serviceForBackupTest.registerProvider(providerWithBackup);
|
|
302
|
-
|
|
303
|
-
const result = await serviceForBackupTest.verifyCode(mockUser as IUser, MFAMethod.BACKUP, 'ABC12345');
|
|
304
|
-
|
|
305
|
-
expect(result).toBe(true);
|
|
306
|
-
expect(providerWithBackup.verifyBackupCode).toHaveBeenCalledWith(mockUser as IUser, 'ABC12345');
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('should throw error when backup code verification not available', async () => {
|
|
310
|
-
try {
|
|
311
|
-
await service.verifyCode(mockUser as IUser, MFAMethod.BACKUP, 'ABC12345');
|
|
312
|
-
fail('Should have thrown NAuthException');
|
|
313
|
-
} catch (error: any) {
|
|
314
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
315
|
-
expect(error.message).toContain('Backup code verification not available');
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('should pass deviceId to provider', async () => {
|
|
320
|
-
mockProvider1.verify.mockResolvedValue(true);
|
|
321
|
-
|
|
322
|
-
await service.verifyCode(mockUser as IUser, 'totp', '123456', 1);
|
|
323
|
-
|
|
324
|
-
expect(mockProvider1.verify).toHaveBeenCalledWith(mockUser as IUser, '123456', 1);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// ============================================================================
|
|
329
|
-
// setup() Method
|
|
330
|
-
// ============================================================================
|
|
331
|
-
|
|
332
|
-
describe('setup', () => {
|
|
333
|
-
beforeEach(() => {
|
|
334
|
-
service.registerProvider(mockProvider1);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('should route setup to correct provider', async () => {
|
|
338
|
-
const setupData = { secret: 'test-secret', qrCode: 'data:image/png;base64,...' };
|
|
339
|
-
mockProvider1.setup.mockResolvedValue(setupData);
|
|
340
|
-
|
|
341
|
-
const result = await service.setup(mockUser as IUser, 'totp');
|
|
342
|
-
|
|
343
|
-
expect(result).toEqual(setupData);
|
|
344
|
-
expect(mockProvider1.setup).toHaveBeenCalledWith(mockUser as IUser, undefined);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('should throw error when provider not registered', async () => {
|
|
348
|
-
try {
|
|
349
|
-
await service.setup(mockUser as IUser, 'sms');
|
|
350
|
-
fail('Should have thrown NAuthException');
|
|
351
|
-
} catch (error: any) {
|
|
352
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('should pass setupData to provider', async () => {
|
|
357
|
-
const setupData = { secret: 'test-secret' };
|
|
358
|
-
mockProvider1.setup.mockResolvedValue(setupData);
|
|
359
|
-
|
|
360
|
-
await service.setup(mockUser as IUser, 'totp', { phoneNumber: '+1234567890' });
|
|
361
|
-
|
|
362
|
-
expect(mockProvider1.setup).toHaveBeenCalledWith(mockUser as IUser, { phoneNumber: '+1234567890' });
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// ============================================================================
|
|
367
|
-
// getUserDevices() Method
|
|
368
|
-
// ============================================================================
|
|
369
|
-
|
|
370
|
-
describe('getUserDevices', () => {
|
|
371
|
-
it('should return user devices ordered by primary and creation date', async () => {
|
|
372
|
-
const devices = [
|
|
373
|
-
{ ...mockDevice, id: 1, isPrimary: true },
|
|
374
|
-
{ ...mockDevice, id: 2, isPrimary: false },
|
|
375
|
-
];
|
|
376
|
-
|
|
377
|
-
mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
|
|
378
|
-
|
|
379
|
-
const result = await service.getUserDevices(1);
|
|
380
|
-
|
|
381
|
-
expect(result).toEqual(devices as any);
|
|
382
|
-
expect(mockMfaDeviceRepository.find).toHaveBeenCalledWith({
|
|
383
|
-
where: { userId: 1 },
|
|
384
|
-
order: { isPrimary: 'DESC', createdAt: 'DESC' },
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('should return empty array when no devices found', async () => {
|
|
389
|
-
mockMfaDeviceRepository.find.mockResolvedValue([]);
|
|
390
|
-
|
|
391
|
-
const result = await service.getUserDevices(1);
|
|
392
|
-
|
|
393
|
-
expect(result).toEqual([]);
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// ============================================================================
|
|
398
|
-
// removeDevices() Method
|
|
399
|
-
// ============================================================================
|
|
400
|
-
|
|
401
|
-
describe('removeDevices', () => {
|
|
402
|
-
beforeEach(() => {
|
|
403
|
-
service.registerProvider(mockProvider1);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('should remove devices of specified method type', async () => {
|
|
407
|
-
const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
|
|
408
|
-
const devices = [
|
|
409
|
-
{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
|
|
410
|
-
{ ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
|
|
411
|
-
];
|
|
412
|
-
|
|
413
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
414
|
-
mockMfaDeviceRepository.find
|
|
415
|
-
.mockResolvedValueOnce(devices as any) // getUserDevices call
|
|
416
|
-
.mockResolvedValueOnce([devices[1]] as any); // After deletion
|
|
417
|
-
mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
418
|
-
mockUserRepository.save.mockResolvedValue(userEntity as any);
|
|
419
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
420
|
-
|
|
421
|
-
const result = await service.removeDevices('user-uuid-123', 'totp');
|
|
422
|
-
|
|
423
|
-
expect(result.deletedCount).toBe(1);
|
|
424
|
-
expect(result.mfaDisabled).toBe(false);
|
|
425
|
-
expect(mockMfaDeviceRepository.delete).toHaveBeenCalledWith(1);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('should throw error when method type is invalid', async () => {
|
|
429
|
-
try {
|
|
430
|
-
await service.removeDevices('user-uuid-123', 'invalid');
|
|
431
|
-
fail('Should have thrown NAuthException');
|
|
432
|
-
} catch (error: any) {
|
|
433
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
434
|
-
expect(error.message).toContain('Invalid MFA method');
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('should throw error when user not found', async () => {
|
|
439
|
-
mockUserRepository.findOne.mockResolvedValue(null);
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
await service.removeDevices('non-existent-user', 'totp');
|
|
443
|
-
fail('Should have thrown NAuthException');
|
|
444
|
-
} catch (error: any) {
|
|
445
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
446
|
-
expect(error.message).toContain('User entity not found');
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('should throw error when no devices of method type found', async () => {
|
|
451
|
-
const userEntity = { ...mockUser, id: 1 };
|
|
452
|
-
const devices = [{ ...mockDevice, id: 1, type: 'sms' as MFADeviceMethod, isActive: true }];
|
|
453
|
-
|
|
454
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
455
|
-
mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
|
|
456
|
-
|
|
457
|
-
try {
|
|
458
|
-
await service.removeDevices('user-uuid-123', 'totp');
|
|
459
|
-
fail('Should have thrown NAuthException');
|
|
460
|
-
} catch (error: any) {
|
|
461
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
462
|
-
expect(error.message).toContain('No active totp MFA devices found');
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('should disable MFA when last device removed', async () => {
|
|
467
|
-
const userEntity = { ...mockUser, id: 1, mfaEnabled: true, mfaMethods: ['totp'] };
|
|
468
|
-
const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
|
|
469
|
-
|
|
470
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
471
|
-
mockMfaDeviceRepository.find
|
|
472
|
-
.mockResolvedValueOnce(devices as any) // getUserDevices call
|
|
473
|
-
.mockResolvedValueOnce([]); // After deletion - no devices remain
|
|
474
|
-
mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
475
|
-
mockUserRepository.save.mockResolvedValue(userEntity as any);
|
|
476
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
477
|
-
|
|
478
|
-
const result = await service.removeDevices('user-uuid-123', 'totp');
|
|
479
|
-
|
|
480
|
-
expect(result.mfaDisabled).toBe(true);
|
|
481
|
-
expect(userEntity.mfaEnabled).toBe(false);
|
|
482
|
-
expect(userEntity.mfaMethods).toEqual([]);
|
|
483
|
-
expect(userEntity.preferredMfaMethod).toBeNull();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('should create MFA_SETUP_REQUIRED challenge when MFA disabled and enforcement is REQUIRED', async () => {
|
|
487
|
-
const userEntity = {
|
|
488
|
-
...mockUser,
|
|
489
|
-
id: 1,
|
|
490
|
-
mfaEnabled: true,
|
|
491
|
-
mfaMethods: ['totp'],
|
|
492
|
-
};
|
|
493
|
-
const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
|
|
494
|
-
const configWithEnforcement: Partial<NAuthConfig> = {
|
|
495
|
-
mfa: {
|
|
496
|
-
enabled: true,
|
|
497
|
-
enforcement: 'REQUIRED',
|
|
498
|
-
allowedMethods: [MFAMethod.TOTP as any, MFAMethod.SMS as any],
|
|
499
|
-
},
|
|
500
|
-
};
|
|
501
|
-
|
|
502
|
-
const serviceWithEnforcement = new MFAService(
|
|
503
|
-
mockMfaDeviceRepository,
|
|
504
|
-
mockUserRepository,
|
|
505
|
-
mockChallengeService,
|
|
506
|
-
configWithEnforcement as NAuthConfig,
|
|
507
|
-
mockLogger,
|
|
508
|
-
mockAuditService,
|
|
509
|
-
mockClientInfoService,
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
513
|
-
mockMfaDeviceRepository.find.mockResolvedValueOnce(devices as any).mockResolvedValueOnce([]);
|
|
514
|
-
mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
515
|
-
mockUserRepository.save.mockResolvedValue(userEntity as any);
|
|
516
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
517
|
-
mockChallengeService.createChallengeSession.mockResolvedValue({} as any);
|
|
518
|
-
|
|
519
|
-
await serviceWithEnforcement.removeDevices('user-uuid-123', 'totp');
|
|
520
|
-
|
|
521
|
-
expect(mockChallengeService.createChallengeSession).toHaveBeenCalledWith(
|
|
522
|
-
userEntity as IUser,
|
|
523
|
-
AuthChallenge.MFA_SETUP_REQUIRED,
|
|
524
|
-
(expect as any).objectContaining({
|
|
525
|
-
allowedMethods: ['totp', 'sms'],
|
|
526
|
-
requiresSetup: true,
|
|
527
|
-
}),
|
|
528
|
-
);
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it('should update preferred method when removed method was preferred', async () => {
|
|
532
|
-
const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
|
|
533
|
-
const devices = [
|
|
534
|
-
{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
|
|
535
|
-
{ ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
|
|
536
|
-
];
|
|
537
|
-
|
|
538
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
539
|
-
mockMfaDeviceRepository.find.mockResolvedValueOnce(devices as any).mockResolvedValueOnce([devices[1]] as any);
|
|
540
|
-
mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
541
|
-
mockUserRepository.save.mockResolvedValue(userEntity as any);
|
|
542
|
-
mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
543
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
544
|
-
|
|
545
|
-
await service.removeDevices('user-uuid-123', 'totp');
|
|
546
|
-
|
|
547
|
-
expect(userEntity.preferredMfaMethod).toBe('sms');
|
|
548
|
-
expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 2 } as any, { isPrimary: true } as any);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('should record audit event when device removed', async () => {
|
|
552
|
-
const userEntity = { ...mockUser, id: 1 };
|
|
553
|
-
const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
|
|
554
|
-
|
|
555
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
556
|
-
mockMfaDeviceRepository.find.mockResolvedValueOnce(devices as any).mockResolvedValueOnce([]);
|
|
557
|
-
mockMfaDeviceRepository.delete.mockResolvedValue({ affected: 1 } as any);
|
|
558
|
-
mockUserRepository.save.mockResolvedValue(userEntity as any);
|
|
559
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
560
|
-
|
|
561
|
-
await service.removeDevices('user-uuid-123', 'totp');
|
|
562
|
-
|
|
563
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
564
|
-
(expect as any).objectContaining({
|
|
565
|
-
userId: 1,
|
|
566
|
-
eventType: AuthAuditEventType.MFA_DISABLED,
|
|
567
|
-
eventStatus: 'INFO',
|
|
568
|
-
reason: 'all_devices_removed',
|
|
569
|
-
}),
|
|
570
|
-
);
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// ============================================================================
|
|
575
|
-
// setPreferredMethod() Method
|
|
576
|
-
// ============================================================================
|
|
577
|
-
|
|
578
|
-
describe('setPreferredMethod', () => {
|
|
579
|
-
beforeEach(() => {
|
|
580
|
-
service.registerProvider(mockProvider1);
|
|
581
|
-
service.registerProvider(mockProvider2);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it('should set preferred method successfully', async () => {
|
|
585
|
-
const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
|
|
586
|
-
const devices = [
|
|
587
|
-
{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
|
|
588
|
-
{ ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
|
|
589
|
-
];
|
|
590
|
-
|
|
591
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
592
|
-
mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
|
|
593
|
-
mockUserRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
594
|
-
mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
595
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
596
|
-
|
|
597
|
-
await service.setPreferredMethod('user-uuid-123', 'sms');
|
|
598
|
-
|
|
599
|
-
expect(mockUserRepository.update).toHaveBeenCalledWith({ id: 1 } as any, { preferredMfaMethod: 'sms' } as any);
|
|
600
|
-
expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 2 } as any, { isPrimary: true } as any);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('should throw error when method type is invalid', async () => {
|
|
604
|
-
try {
|
|
605
|
-
await service.setPreferredMethod('user-uuid-123', 'invalid');
|
|
606
|
-
fail('Should have thrown NAuthException');
|
|
607
|
-
} catch (error: any) {
|
|
608
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
609
|
-
expect(error.message).toContain('Invalid MFA method');
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('should throw error when user not found', async () => {
|
|
614
|
-
mockUserRepository.findOne.mockResolvedValue(null);
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
await service.setPreferredMethod('non-existent-user', 'totp');
|
|
618
|
-
fail('Should have thrown NAuthException');
|
|
619
|
-
} catch (error: any) {
|
|
620
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
621
|
-
expect(error.message).toContain('User not found');
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it('should throw error when method not configured for user', async () => {
|
|
626
|
-
const userEntity = { ...mockUser, id: 1 };
|
|
627
|
-
const devices = [{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true }];
|
|
628
|
-
|
|
629
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
630
|
-
mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
|
|
631
|
-
|
|
632
|
-
try {
|
|
633
|
-
await service.setPreferredMethod('user-uuid-123', 'sms');
|
|
634
|
-
fail('Should have thrown NAuthException');
|
|
635
|
-
} catch (error: any) {
|
|
636
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
637
|
-
expect(error.message).toContain('is not configured for this user');
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
it('should update device primary flags correctly', async () => {
|
|
642
|
-
const userEntity = { ...mockUser, id: 1 };
|
|
643
|
-
const devices = [
|
|
644
|
-
{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
|
|
645
|
-
{ ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
|
|
646
|
-
{ ...mockDevice, id: 3, type: 'sms' as MFADeviceMethod, isActive: true },
|
|
647
|
-
];
|
|
648
|
-
|
|
649
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
650
|
-
mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
|
|
651
|
-
mockUserRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
652
|
-
mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
653
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
654
|
-
|
|
655
|
-
await service.setPreferredMethod('user-uuid-123', 'sms');
|
|
656
|
-
|
|
657
|
-
// Should set device 2 (first SMS device) as primary
|
|
658
|
-
expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 2 } as any, { isPrimary: true } as any);
|
|
659
|
-
// Should unset primary on other devices
|
|
660
|
-
expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 1 } as any, { isPrimary: false } as any);
|
|
661
|
-
expect(mockMfaDeviceRepository.update).toHaveBeenCalledWith({ id: 3 } as any, { isPrimary: false } as any);
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it('should record audit event when preferred method updated', async () => {
|
|
665
|
-
const userEntity = { ...mockUser, id: 1, preferredMfaMethod: 'totp' };
|
|
666
|
-
const devices = [
|
|
667
|
-
{ ...mockDevice, id: 1, type: 'totp' as MFADeviceMethod, isActive: true },
|
|
668
|
-
{ ...mockDevice, id: 2, type: 'sms' as MFADeviceMethod, isActive: true },
|
|
669
|
-
];
|
|
670
|
-
|
|
671
|
-
mockUserRepository.findOne.mockResolvedValue(userEntity as any);
|
|
672
|
-
mockMfaDeviceRepository.find.mockResolvedValue(devices as any);
|
|
673
|
-
mockUserRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
674
|
-
mockMfaDeviceRepository.update.mockResolvedValue({ affected: 1 } as any);
|
|
675
|
-
mockAuditService.recordEvent.mockResolvedValue({} as any);
|
|
676
|
-
|
|
677
|
-
await service.setPreferredMethod('user-uuid-123', 'sms');
|
|
678
|
-
|
|
679
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
680
|
-
(expect as any).objectContaining({
|
|
681
|
-
userId: 1,
|
|
682
|
-
eventType: AuthAuditEventType.MFA_PREFERRED_METHOD_UPDATED,
|
|
683
|
-
eventStatus: 'INFO',
|
|
684
|
-
metadata: (expect as any).objectContaining({
|
|
685
|
-
previousMethod: 'totp',
|
|
686
|
-
newMethod: 'sms',
|
|
687
|
-
deviceId: 2,
|
|
688
|
-
}),
|
|
689
|
-
}),
|
|
690
|
-
);
|
|
691
|
-
});
|
|
692
|
-
});
|
|
693
|
-
});
|