@nauth-toolkit/core 0.1.0 → 0.1.3
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 +30 -0
- package/package.json +7 -2
- 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,1120 +0,0 @@
|
|
|
1
|
-
import { Repository } from 'typeorm';
|
|
2
|
-
import { PhoneVerificationService } from './phone-verification.service';
|
|
3
|
-
import { NAuthException } from '../exceptions/nauth.exception';
|
|
4
|
-
import { ClientInfoService } from './client-info.service';
|
|
5
|
-
import { SMSProvider } from '../interfaces/provider.interface';
|
|
6
|
-
import { StorageAdapter } from '../interfaces/storage-adapter.interface';
|
|
7
|
-
import { NAuthConfig } from '../interfaces/config.interface';
|
|
8
|
-
import { NAuthLogger } from '../utils/nauth-logger';
|
|
9
|
-
import { AuthAuditService } from './auth-audit.service';
|
|
10
|
-
import { BaseVerificationToken, BaseUser } from '../entities';
|
|
11
|
-
import { IUser, IVerificationToken } from '../interfaces/entities.interface';
|
|
12
|
-
import { AuthErrorCode } from '../enums/error-codes.enum';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Phone Verification Service Unit Tests
|
|
16
|
-
*
|
|
17
|
-
* Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
|
|
18
|
-
*
|
|
19
|
-
* Covers:
|
|
20
|
-
* - Send verification SMS with rate limiting
|
|
21
|
-
* - Code-based verification (by phone and by sub)
|
|
22
|
-
* - Resend verification SMS with cooldown
|
|
23
|
-
* - Rate limiting per user
|
|
24
|
-
* - Error handling for all dependencies
|
|
25
|
-
* - Storage adapter failures
|
|
26
|
-
* - SMS provider errors
|
|
27
|
-
*/
|
|
28
|
-
describe('PhoneVerificationService', () => {
|
|
29
|
-
let service: PhoneVerificationService;
|
|
30
|
-
let mockVerificationTokenRepository: jest.Mocked<Repository<BaseVerificationToken>>;
|
|
31
|
-
let mockUserRepository: jest.Mocked<Repository<BaseUser>>;
|
|
32
|
-
let mockSmsProvider: jest.Mocked<SMSProvider>;
|
|
33
|
-
let mockStorageAdapter: jest.Mocked<StorageAdapter>;
|
|
34
|
-
let mockClientInfoService: jest.Mocked<ClientInfoService>;
|
|
35
|
-
let mockLogger: jest.Mocked<NAuthLogger>;
|
|
36
|
-
let mockAuditService: jest.Mocked<AuthAuditService>;
|
|
37
|
-
let mockConfig: NAuthConfig;
|
|
38
|
-
|
|
39
|
-
const mockUser: IUser = {
|
|
40
|
-
id: 123,
|
|
41
|
-
sub: 'user-sub-123',
|
|
42
|
-
email: 'test@example.com',
|
|
43
|
-
username: 'testuser',
|
|
44
|
-
phone: '+1234567890',
|
|
45
|
-
firstName: null,
|
|
46
|
-
lastName: null,
|
|
47
|
-
passwordHash: null,
|
|
48
|
-
passwordChangedAt: null,
|
|
49
|
-
passwordHistory: null,
|
|
50
|
-
isEmailVerified: false,
|
|
51
|
-
isPhoneVerified: false,
|
|
52
|
-
isActive: true,
|
|
53
|
-
mustChangePassword: false,
|
|
54
|
-
isLocked: false,
|
|
55
|
-
lockReason: null,
|
|
56
|
-
lockedAt: null,
|
|
57
|
-
lockedUntil: null,
|
|
58
|
-
failedLoginAttempts: 0,
|
|
59
|
-
lastFailedLoginAt: null,
|
|
60
|
-
lastLoginAt: null,
|
|
61
|
-
lastLoginIp: null,
|
|
62
|
-
hasSocialAuth: false,
|
|
63
|
-
socialProviders: null,
|
|
64
|
-
mfaEnabled: false,
|
|
65
|
-
mfaMethods: null,
|
|
66
|
-
preferredMfaMethod: null,
|
|
67
|
-
backupCodes: null,
|
|
68
|
-
metadata: null,
|
|
69
|
-
createdAt: new Date(),
|
|
70
|
-
updatedAt: new Date(),
|
|
71
|
-
deletedAt: null,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const mockVerificationToken: IVerificationToken = {
|
|
75
|
-
id: 456,
|
|
76
|
-
userId: 123,
|
|
77
|
-
type: 'phone',
|
|
78
|
-
token: 'hashed-token-abc123',
|
|
79
|
-
code: '123456',
|
|
80
|
-
expiresAt: new Date(Date.now() + 300000), // 5 minutes from now
|
|
81
|
-
attempts: 0,
|
|
82
|
-
usedAt: null,
|
|
83
|
-
ipAddress: '127.0.0.1',
|
|
84
|
-
userAgent: 'test-agent',
|
|
85
|
-
createdAt: new Date(),
|
|
86
|
-
isExpired: jest.fn().mockReturnValue(false),
|
|
87
|
-
maxAttemptsExceeded: jest.fn().mockReturnValue(false),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
mockVerificationTokenRepository = {
|
|
92
|
-
create: jest.fn(),
|
|
93
|
-
save: jest.fn(),
|
|
94
|
-
findOne: jest.fn(),
|
|
95
|
-
find: jest.fn(),
|
|
96
|
-
update: jest.fn().mockResolvedValue({ affected: 0 } as any),
|
|
97
|
-
count: jest.fn(),
|
|
98
|
-
} as any;
|
|
99
|
-
|
|
100
|
-
mockUserRepository = {
|
|
101
|
-
findOne: jest.fn(),
|
|
102
|
-
save: jest.fn(),
|
|
103
|
-
update: jest.fn().mockResolvedValue({ affected: 1 } as any),
|
|
104
|
-
} as any;
|
|
105
|
-
|
|
106
|
-
mockSmsProvider = {
|
|
107
|
-
sendOTP: jest.fn().mockResolvedValue(undefined),
|
|
108
|
-
setLogger: jest.fn(),
|
|
109
|
-
} as any;
|
|
110
|
-
|
|
111
|
-
mockStorageAdapter = {
|
|
112
|
-
get: jest.fn(),
|
|
113
|
-
set: jest.fn(),
|
|
114
|
-
incr: jest.fn().mockResolvedValue(1),
|
|
115
|
-
expire: jest.fn().mockResolvedValue(undefined),
|
|
116
|
-
ttl: jest.fn().mockResolvedValue(3600),
|
|
117
|
-
del: jest.fn().mockResolvedValue(undefined),
|
|
118
|
-
exists: jest.fn(),
|
|
119
|
-
initialize: jest.fn(),
|
|
120
|
-
isHealthy: jest.fn().mockResolvedValue(true),
|
|
121
|
-
cleanup: jest.fn(),
|
|
122
|
-
disconnect: jest.fn(),
|
|
123
|
-
} as any;
|
|
124
|
-
|
|
125
|
-
mockClientInfoService = {
|
|
126
|
-
get: jest.fn().mockReturnValue({
|
|
127
|
-
ipAddress: '127.0.0.1',
|
|
128
|
-
userAgent: 'test-agent',
|
|
129
|
-
}),
|
|
130
|
-
} as any;
|
|
131
|
-
|
|
132
|
-
mockLogger = {
|
|
133
|
-
log: jest.fn(),
|
|
134
|
-
error: jest.fn(),
|
|
135
|
-
warn: jest.fn(),
|
|
136
|
-
debug: jest.fn(),
|
|
137
|
-
verbose: jest.fn(),
|
|
138
|
-
} as any;
|
|
139
|
-
|
|
140
|
-
mockAuditService = {
|
|
141
|
-
recordEvent: jest.fn().mockResolvedValue(null),
|
|
142
|
-
} as any;
|
|
143
|
-
|
|
144
|
-
mockConfig = {
|
|
145
|
-
jwt: {
|
|
146
|
-
accessToken: { secret: 'test-secret', expiresIn: '15m' },
|
|
147
|
-
refreshToken: { secret: 'test-refresh-secret', expiresIn: '7d' },
|
|
148
|
-
},
|
|
149
|
-
signup: {
|
|
150
|
-
phoneVerification: {
|
|
151
|
-
codeLength: 6,
|
|
152
|
-
expiresIn: 300,
|
|
153
|
-
maxAttempts: 3,
|
|
154
|
-
resendDelay: 60,
|
|
155
|
-
rateLimitMax: 3,
|
|
156
|
-
rateLimitWindow: 3600,
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Instantiate service directly
|
|
162
|
-
service = new PhoneVerificationService(
|
|
163
|
-
mockVerificationTokenRepository,
|
|
164
|
-
mockUserRepository,
|
|
165
|
-
mockSmsProvider,
|
|
166
|
-
mockStorageAdapter,
|
|
167
|
-
mockConfig,
|
|
168
|
-
mockClientInfoService,
|
|
169
|
-
mockLogger,
|
|
170
|
-
mockAuditService,
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
afterEach(() => {
|
|
175
|
-
jest.clearAllMocks();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ============================================================================
|
|
179
|
-
// Service Initialization
|
|
180
|
-
// ============================================================================
|
|
181
|
-
|
|
182
|
-
it('should be defined', () => {
|
|
183
|
-
expect(service).toBeDefined();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ============================================================================
|
|
187
|
-
// sendVerificationSMS
|
|
188
|
-
// ============================================================================
|
|
189
|
-
|
|
190
|
-
describe('sendVerificationSMS', () => {
|
|
191
|
-
it('should send verification SMS successfully', async () => {
|
|
192
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
193
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
194
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
195
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null); // No last token
|
|
196
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
197
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
198
|
-
|
|
199
|
-
const result = await service.sendVerificationSMS('user-sub-123');
|
|
200
|
-
|
|
201
|
-
expect(mockStorageAdapter.incr).toHaveBeenCalled();
|
|
202
|
-
expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { sub: 'user-sub-123' } as any });
|
|
203
|
-
expect(mockVerificationTokenRepository.save).toHaveBeenCalled();
|
|
204
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalledWith('+1234567890', (expect as any).any(String));
|
|
205
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalled();
|
|
206
|
-
expect(result).toBe(456);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should throw NAuthException if user not found', async () => {
|
|
210
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
211
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
212
|
-
mockUserRepository.findOne.mockResolvedValue(null);
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
await service.sendVerificationSMS('invalid-sub');
|
|
216
|
-
fail('Should have thrown NAuthException');
|
|
217
|
-
} catch (error: any) {
|
|
218
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
219
|
-
expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('should throw NAuthException if phone not provided', async () => {
|
|
224
|
-
const userWithoutPhone = { ...mockUser, phone: null };
|
|
225
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
226
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
227
|
-
mockUserRepository.findOne.mockResolvedValue(userWithoutPhone as any);
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
231
|
-
fail('Should have thrown NAuthException');
|
|
232
|
-
} catch (error: any) {
|
|
233
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
234
|
-
expect(error.code).toBe(AuthErrorCode.PHONE_REQUIRED);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should throw NAuthException if phone already verified (when skipAlreadyVerifiedCheck is false)', async () => {
|
|
239
|
-
const verifiedUser = { ...mockUser, isPhoneVerified: true };
|
|
240
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
241
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
242
|
-
mockUserRepository.findOne.mockResolvedValue(verifiedUser as any);
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
await service.sendVerificationSMS('user-sub-123', false); // Don't skip check
|
|
246
|
-
fail('Should have thrown NAuthException');
|
|
247
|
-
} catch (error: any) {
|
|
248
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
249
|
-
expect(error.code).toBe(AuthErrorCode.ALREADY_VERIFIED);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should allow sending SMS when phone already verified (skipAlreadyVerifiedCheck is true)', async () => {
|
|
254
|
-
const verifiedUser = { ...mockUser, isPhoneVerified: true };
|
|
255
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
256
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
257
|
-
mockUserRepository.findOne.mockResolvedValue(verifiedUser as any);
|
|
258
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
259
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
260
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
261
|
-
|
|
262
|
-
await service.sendVerificationSMS('user-sub-123', true); // Skip check
|
|
263
|
-
|
|
264
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('should enforce rate limit (too many SMS)', async () => {
|
|
268
|
-
mockStorageAdapter.incr.mockResolvedValue(4); // Exceeds limit of 3
|
|
269
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
273
|
-
fail('Should have thrown NAuthException');
|
|
274
|
-
} catch (error: any) {
|
|
275
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
276
|
-
expect(error.code).toBe(AuthErrorCode.RATE_LIMIT_SMS);
|
|
277
|
-
expect(mockUserRepository.findOne).not.toHaveBeenCalled();
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('should enforce resend delay', async () => {
|
|
282
|
-
const recentToken = {
|
|
283
|
-
...mockVerificationToken,
|
|
284
|
-
createdAt: new Date(Date.now() - 30 * 1000), // 30 seconds ago (less than 60s delay)
|
|
285
|
-
};
|
|
286
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
287
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
288
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
289
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(recentToken as any);
|
|
290
|
-
|
|
291
|
-
try {
|
|
292
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
293
|
-
fail('Should have thrown NAuthException');
|
|
294
|
-
} catch (error: any) {
|
|
295
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
296
|
-
expect(error.code).toBe(AuthErrorCode.RATE_LIMIT_RESEND);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('should allow resend after delay period', async () => {
|
|
301
|
-
const oldToken = {
|
|
302
|
-
...mockVerificationToken,
|
|
303
|
-
createdAt: new Date(Date.now() - 70 * 1000), // 70 seconds ago (more than 60s delay)
|
|
304
|
-
};
|
|
305
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
306
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
307
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
308
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(oldToken as any);
|
|
309
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
310
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
311
|
-
|
|
312
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
313
|
-
|
|
314
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('should invalidate existing unused tokens', async () => {
|
|
318
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
319
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
320
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
321
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
322
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
323
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
324
|
-
|
|
325
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
326
|
-
|
|
327
|
-
expect(mockVerificationTokenRepository.update).toHaveBeenCalledWith(
|
|
328
|
-
(expect as any).objectContaining({
|
|
329
|
-
userId: 123,
|
|
330
|
-
type: 'phone',
|
|
331
|
-
}),
|
|
332
|
-
(expect as any).objectContaining({
|
|
333
|
-
usedAt: (expect as any).any(Date),
|
|
334
|
-
}),
|
|
335
|
-
);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it('should handle rate limit window reset when TTL > window', async () => {
|
|
339
|
-
mockStorageAdapter.ttl.mockResolvedValue(7200); // TTL longer than window (3600)
|
|
340
|
-
mockStorageAdapter.del.mockResolvedValue(undefined);
|
|
341
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
342
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
343
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
344
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
345
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
346
|
-
|
|
347
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
348
|
-
|
|
349
|
-
expect(mockStorageAdapter.del).toHaveBeenCalledWith('phone-verification:user-sub-123');
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('should handle SMS provider errors', async () => {
|
|
353
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
354
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
355
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
356
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
357
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
358
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
359
|
-
mockSmsProvider.sendOTP.mockRejectedValue(new Error('SMS service error'));
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
363
|
-
fail('Should have thrown error');
|
|
364
|
-
} catch (error: any) {
|
|
365
|
-
expect(error.message).toContain('SMS service error');
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('should handle audit service errors gracefully', async () => {
|
|
370
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
371
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
372
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
373
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
374
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
375
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
376
|
-
mockAuditService.recordEvent.mockRejectedValue(new Error('Audit error'));
|
|
377
|
-
|
|
378
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
379
|
-
|
|
380
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
381
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled(); // Should still send SMS
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('should use custom rate limit config', async () => {
|
|
385
|
-
mockConfig.signup!.phoneVerification!.rateLimitMax = 5;
|
|
386
|
-
mockConfig.signup!.phoneVerification!.rateLimitWindow = 1800;
|
|
387
|
-
service = new PhoneVerificationService(
|
|
388
|
-
mockVerificationTokenRepository,
|
|
389
|
-
mockUserRepository,
|
|
390
|
-
mockSmsProvider,
|
|
391
|
-
mockStorageAdapter,
|
|
392
|
-
mockConfig,
|
|
393
|
-
mockClientInfoService,
|
|
394
|
-
mockLogger,
|
|
395
|
-
mockAuditService,
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
mockStorageAdapter.incr.mockResolvedValue(6); // Exceeds new limit of 5
|
|
399
|
-
mockStorageAdapter.ttl.mockResolvedValue(1800);
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
403
|
-
fail('Should have thrown NAuthException');
|
|
404
|
-
} catch (error: any) {
|
|
405
|
-
expect(error.code).toBe(AuthErrorCode.RATE_LIMIT_SMS);
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('should use custom resend delay config', async () => {
|
|
410
|
-
mockConfig.signup!.phoneVerification!.resendDelay = 120; // 2 minutes
|
|
411
|
-
service = new PhoneVerificationService(
|
|
412
|
-
mockVerificationTokenRepository,
|
|
413
|
-
mockUserRepository,
|
|
414
|
-
mockSmsProvider,
|
|
415
|
-
mockStorageAdapter,
|
|
416
|
-
mockConfig,
|
|
417
|
-
mockClientInfoService,
|
|
418
|
-
mockLogger,
|
|
419
|
-
mockAuditService,
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const recentToken = {
|
|
423
|
-
...mockVerificationToken,
|
|
424
|
-
createdAt: new Date(Date.now() - 90 * 1000), // 90 seconds ago (less than 120s)
|
|
425
|
-
};
|
|
426
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
427
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
428
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
429
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(recentToken as any);
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
433
|
-
fail('Should have thrown NAuthException');
|
|
434
|
-
} catch (error: any) {
|
|
435
|
-
expect(error.code).toBe(AuthErrorCode.RATE_LIMIT_RESEND);
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
// ============================================================================
|
|
441
|
-
// verifyPhoneWithCode
|
|
442
|
-
// ============================================================================
|
|
443
|
-
|
|
444
|
-
describe('verifyPhoneWithCode', () => {
|
|
445
|
-
it('should verify phone with valid code', async () => {
|
|
446
|
-
mockVerificationTokenRepository.find.mockResolvedValue([mockVerificationToken] as any);
|
|
447
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
448
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
449
|
-
|
|
450
|
-
const result = await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
451
|
-
|
|
452
|
-
expect(result.message).toBe('Phone verified successfully. Please log in to continue.');
|
|
453
|
-
expect(mockUserRepository.update).toHaveBeenCalledWith(123, {
|
|
454
|
-
isPhoneVerified: true,
|
|
455
|
-
isActive: true,
|
|
456
|
-
});
|
|
457
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalled();
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it('should throw NAuthException for invalid code', async () => {
|
|
461
|
-
mockVerificationTokenRepository.find.mockResolvedValue([]); // No matching tokens
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
await service.verifyPhoneWithCode('+1234567890', 'wrong-code');
|
|
465
|
-
fail('Should have thrown NAuthException');
|
|
466
|
-
} catch (error: any) {
|
|
467
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
468
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_INVALID);
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('should throw NAuthException when no matching user phone', async () => {
|
|
473
|
-
const tokenForDifferentUser = {
|
|
474
|
-
...mockVerificationToken,
|
|
475
|
-
userId: 999,
|
|
476
|
-
};
|
|
477
|
-
const differentUser = {
|
|
478
|
-
...mockUser,
|
|
479
|
-
id: 999,
|
|
480
|
-
phone: '+9999999999', // Different phone
|
|
481
|
-
};
|
|
482
|
-
mockVerificationTokenRepository.find.mockResolvedValue([tokenForDifferentUser] as any);
|
|
483
|
-
mockUserRepository.findOne.mockResolvedValue(differentUser as any);
|
|
484
|
-
|
|
485
|
-
try {
|
|
486
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
487
|
-
fail('Should have thrown NAuthException');
|
|
488
|
-
} catch (error: any) {
|
|
489
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
490
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_INVALID);
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('should throw NAuthException for expired code', async () => {
|
|
495
|
-
const expiredToken = {
|
|
496
|
-
...mockVerificationToken,
|
|
497
|
-
expiresAt: new Date(Date.now() - 1000),
|
|
498
|
-
isExpired: jest.fn().mockReturnValue(true),
|
|
499
|
-
};
|
|
500
|
-
mockVerificationTokenRepository.find.mockResolvedValue([expiredToken] as any);
|
|
501
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
502
|
-
|
|
503
|
-
try {
|
|
504
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
505
|
-
fail('Should have thrown NAuthException');
|
|
506
|
-
} catch (error: any) {
|
|
507
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
508
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_EXPIRED);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it('should throw NAuthException after max attempts', async () => {
|
|
513
|
-
const exhaustedToken = {
|
|
514
|
-
...mockVerificationToken,
|
|
515
|
-
attempts: 3,
|
|
516
|
-
isExpired: jest.fn().mockReturnValue(false),
|
|
517
|
-
maxAttemptsExceeded: jest.fn().mockReturnValue(true),
|
|
518
|
-
};
|
|
519
|
-
mockVerificationTokenRepository.find.mockResolvedValue([exhaustedToken] as any);
|
|
520
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
524
|
-
fail('Should have thrown NAuthException');
|
|
525
|
-
} catch (error: any) {
|
|
526
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
527
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS);
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it('should increment attempts on invalid code', async () => {
|
|
532
|
-
const tokenWithWrongCode = {
|
|
533
|
-
...mockVerificationToken,
|
|
534
|
-
code: '999999', // Different code
|
|
535
|
-
attempts: 0,
|
|
536
|
-
};
|
|
537
|
-
mockVerificationTokenRepository.find.mockResolvedValue([tokenWithWrongCode] as any);
|
|
538
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
539
|
-
mockVerificationTokenRepository.save.mockResolvedValue(tokenWithWrongCode as any);
|
|
540
|
-
|
|
541
|
-
try {
|
|
542
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
543
|
-
fail('Should have thrown NAuthException');
|
|
544
|
-
} catch (error: any) {
|
|
545
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_INVALID);
|
|
546
|
-
expect(mockVerificationTokenRepository.save).toHaveBeenCalledWith(
|
|
547
|
-
(expect as any).objectContaining({
|
|
548
|
-
attempts: 1, // Incremented
|
|
549
|
-
}),
|
|
550
|
-
);
|
|
551
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
552
|
-
(expect as any).objectContaining({
|
|
553
|
-
eventType: (expect as any).any(String),
|
|
554
|
-
eventStatus: 'FAILURE',
|
|
555
|
-
}),
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it('should handle multiple tokens with same code and select correct user', async () => {
|
|
561
|
-
const token1 = {
|
|
562
|
-
...mockVerificationToken,
|
|
563
|
-
userId: 123,
|
|
564
|
-
code: '123456',
|
|
565
|
-
};
|
|
566
|
-
const token2 = {
|
|
567
|
-
...mockVerificationToken,
|
|
568
|
-
userId: 456,
|
|
569
|
-
code: '123456',
|
|
570
|
-
};
|
|
571
|
-
const user2 = {
|
|
572
|
-
...mockUser,
|
|
573
|
-
id: 456,
|
|
574
|
-
phone: '+9876543210',
|
|
575
|
-
};
|
|
576
|
-
mockVerificationTokenRepository.find.mockResolvedValue([token1, token2] as any);
|
|
577
|
-
mockUserRepository.findOne
|
|
578
|
-
.mockResolvedValueOnce(mockUser as any) // First call for user 123
|
|
579
|
-
.mockResolvedValueOnce(user2 as any); // Second call for user 456
|
|
580
|
-
|
|
581
|
-
const result = await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
582
|
-
|
|
583
|
-
// Should match token1 (user 123) because phone matches
|
|
584
|
-
expect(result.message).toBeDefined();
|
|
585
|
-
expect(mockUserRepository.update).toHaveBeenCalledWith(123, (expect as any).any(Object));
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it('should handle audit service errors gracefully', async () => {
|
|
589
|
-
mockVerificationTokenRepository.find.mockResolvedValue([mockVerificationToken] as any);
|
|
590
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
591
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
592
|
-
mockAuditService.recordEvent.mockRejectedValue(new Error('Audit error'));
|
|
593
|
-
|
|
594
|
-
const result = await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
595
|
-
|
|
596
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
597
|
-
expect(result.message).toBeDefined(); // Should still verify
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
it('should check expiration using isExpired method if available', async () => {
|
|
601
|
-
const tokenWithMethod = {
|
|
602
|
-
...mockVerificationToken,
|
|
603
|
-
isExpired: jest.fn().mockReturnValue(true),
|
|
604
|
-
};
|
|
605
|
-
mockVerificationTokenRepository.find.mockResolvedValue([tokenWithMethod] as any);
|
|
606
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
607
|
-
|
|
608
|
-
try {
|
|
609
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
610
|
-
fail('Should have thrown NAuthException');
|
|
611
|
-
} catch (error: any) {
|
|
612
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_EXPIRED);
|
|
613
|
-
expect(tokenWithMethod.isExpired).toHaveBeenCalled();
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it('should check expiration using expiresAt date if method not available', async () => {
|
|
618
|
-
const tokenWithoutMethod = {
|
|
619
|
-
...mockVerificationToken,
|
|
620
|
-
expiresAt: new Date(Date.now() - 1000), // Expired
|
|
621
|
-
isExpired: undefined,
|
|
622
|
-
};
|
|
623
|
-
mockVerificationTokenRepository.find.mockResolvedValue([tokenWithoutMethod] as any);
|
|
624
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
628
|
-
fail('Should have thrown NAuthException');
|
|
629
|
-
} catch (error: any) {
|
|
630
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_EXPIRED);
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it('should check max attempts using method if available', async () => {
|
|
635
|
-
const tokenWithMethod = {
|
|
636
|
-
...mockVerificationToken,
|
|
637
|
-
maxAttemptsExceeded: jest.fn().mockReturnValue(true),
|
|
638
|
-
};
|
|
639
|
-
mockVerificationTokenRepository.find.mockResolvedValue([tokenWithMethod] as any);
|
|
640
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
641
|
-
|
|
642
|
-
try {
|
|
643
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
644
|
-
fail('Should have thrown NAuthException');
|
|
645
|
-
} catch (error: any) {
|
|
646
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS);
|
|
647
|
-
expect(tokenWithMethod.maxAttemptsExceeded).toHaveBeenCalledWith(3);
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it('should check max attempts using attempts field if method not available', async () => {
|
|
652
|
-
const tokenWithoutMethod = {
|
|
653
|
-
...mockVerificationToken,
|
|
654
|
-
attempts: 3,
|
|
655
|
-
maxAttemptsExceeded: undefined,
|
|
656
|
-
};
|
|
657
|
-
mockVerificationTokenRepository.find.mockResolvedValue([tokenWithoutMethod] as any);
|
|
658
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
659
|
-
|
|
660
|
-
try {
|
|
661
|
-
await service.verifyPhoneWithCode('+1234567890', '123456');
|
|
662
|
-
fail('Should have thrown NAuthException');
|
|
663
|
-
} catch (error: any) {
|
|
664
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS);
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
// ============================================================================
|
|
670
|
-
// verifyPhoneWithCodeBySub
|
|
671
|
-
// ============================================================================
|
|
672
|
-
|
|
673
|
-
describe('verifyPhoneWithCodeBySub', () => {
|
|
674
|
-
it('should verify phone with valid code by sub', async () => {
|
|
675
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
676
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(mockVerificationToken as any);
|
|
677
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
678
|
-
|
|
679
|
-
const result = await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
680
|
-
|
|
681
|
-
expect(result.message).toBe('Phone verified successfully. Please log in to continue.');
|
|
682
|
-
expect(mockUserRepository.update).toHaveBeenCalledWith({ sub: 'user-sub-123' } as any, {
|
|
683
|
-
isPhoneVerified: true,
|
|
684
|
-
isActive: true,
|
|
685
|
-
});
|
|
686
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalled();
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
it('should throw NAuthException if user not found', async () => {
|
|
690
|
-
mockUserRepository.findOne.mockResolvedValue(null);
|
|
691
|
-
|
|
692
|
-
try {
|
|
693
|
-
await service.verifyPhoneWithCodeBySub('invalid-sub', '123456');
|
|
694
|
-
fail('Should have thrown NAuthException');
|
|
695
|
-
} catch (error: any) {
|
|
696
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
697
|
-
expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it('should throw NAuthException if phone not provided', async () => {
|
|
702
|
-
const userWithoutPhone = { ...mockUser, phone: null };
|
|
703
|
-
mockUserRepository.findOne.mockResolvedValue(userWithoutPhone as any);
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
707
|
-
fail('Should have thrown NAuthException');
|
|
708
|
-
} catch (error: any) {
|
|
709
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
710
|
-
expect(error.code).toBe(AuthErrorCode.PHONE_REQUIRED);
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
it('should throw NAuthException for invalid code', async () => {
|
|
715
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
716
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', 'wrong-code');
|
|
720
|
-
fail('Should have thrown NAuthException');
|
|
721
|
-
} catch (error: any) {
|
|
722
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
723
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_INVALID);
|
|
724
|
-
}
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
it('should throw NAuthException for expired code', async () => {
|
|
728
|
-
const expiredToken = {
|
|
729
|
-
...mockVerificationToken,
|
|
730
|
-
expiresAt: new Date(Date.now() - 1000),
|
|
731
|
-
isExpired: jest.fn().mockReturnValue(true),
|
|
732
|
-
};
|
|
733
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
734
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(expiredToken as any);
|
|
735
|
-
|
|
736
|
-
try {
|
|
737
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
738
|
-
fail('Should have thrown NAuthException');
|
|
739
|
-
} catch (error: any) {
|
|
740
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
741
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_EXPIRED);
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
it('should throw NAuthException after max attempts', async () => {
|
|
746
|
-
const exhaustedToken = {
|
|
747
|
-
...mockVerificationToken,
|
|
748
|
-
attempts: 3,
|
|
749
|
-
isExpired: jest.fn().mockReturnValue(false),
|
|
750
|
-
maxAttemptsExceeded: jest.fn().mockReturnValue(true),
|
|
751
|
-
};
|
|
752
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
753
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(exhaustedToken as any);
|
|
754
|
-
|
|
755
|
-
try {
|
|
756
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
757
|
-
fail('Should have thrown NAuthException');
|
|
758
|
-
} catch (error: any) {
|
|
759
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
760
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS);
|
|
761
|
-
}
|
|
762
|
-
});
|
|
763
|
-
|
|
764
|
-
it('should increment attempts on invalid code', async () => {
|
|
765
|
-
const tokenWithWrongCode = {
|
|
766
|
-
...mockVerificationToken,
|
|
767
|
-
code: '999999', // Different code
|
|
768
|
-
attempts: 0,
|
|
769
|
-
};
|
|
770
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
771
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(tokenWithWrongCode as any);
|
|
772
|
-
mockVerificationTokenRepository.save.mockResolvedValue(tokenWithWrongCode as any);
|
|
773
|
-
|
|
774
|
-
try {
|
|
775
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
776
|
-
fail('Should have thrown NAuthException');
|
|
777
|
-
} catch (error: any) {
|
|
778
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_INVALID);
|
|
779
|
-
expect(mockVerificationTokenRepository.save).toHaveBeenCalledWith(
|
|
780
|
-
(expect as any).objectContaining({
|
|
781
|
-
attempts: 1, // Incremented
|
|
782
|
-
}),
|
|
783
|
-
);
|
|
784
|
-
expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
|
|
785
|
-
(expect as any).objectContaining({
|
|
786
|
-
eventType: (expect as any).any(String),
|
|
787
|
-
eventStatus: 'FAILURE',
|
|
788
|
-
}),
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
it('should handle audit service errors gracefully', async () => {
|
|
794
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
795
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(mockVerificationToken as any);
|
|
796
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
797
|
-
mockAuditService.recordEvent.mockRejectedValue(new Error('Audit error'));
|
|
798
|
-
|
|
799
|
-
const result = await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
800
|
-
|
|
801
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
802
|
-
expect(result.message).toBeDefined(); // Should still verify
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it('should check expiration using isExpired method if available', async () => {
|
|
806
|
-
const tokenWithMethod = {
|
|
807
|
-
...mockVerificationToken,
|
|
808
|
-
isExpired: jest.fn().mockReturnValue(true),
|
|
809
|
-
};
|
|
810
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
811
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(tokenWithMethod as any);
|
|
812
|
-
|
|
813
|
-
try {
|
|
814
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
815
|
-
fail('Should have thrown NAuthException');
|
|
816
|
-
} catch (error: any) {
|
|
817
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_EXPIRED);
|
|
818
|
-
expect(tokenWithMethod.isExpired).toHaveBeenCalled();
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
it('should check expiration using expiresAt date if method not available', async () => {
|
|
823
|
-
const tokenWithoutMethod = {
|
|
824
|
-
...mockVerificationToken,
|
|
825
|
-
expiresAt: new Date(Date.now() - 1000), // Expired
|
|
826
|
-
isExpired: undefined,
|
|
827
|
-
};
|
|
828
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
829
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(tokenWithoutMethod as any);
|
|
830
|
-
|
|
831
|
-
try {
|
|
832
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
833
|
-
fail('Should have thrown NAuthException');
|
|
834
|
-
} catch (error: any) {
|
|
835
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_CODE_EXPIRED);
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
it('should check max attempts using method if available', async () => {
|
|
840
|
-
const tokenWithMethod = {
|
|
841
|
-
...mockVerificationToken,
|
|
842
|
-
maxAttemptsExceeded: jest.fn().mockReturnValue(true),
|
|
843
|
-
};
|
|
844
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
845
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(tokenWithMethod as any);
|
|
846
|
-
|
|
847
|
-
try {
|
|
848
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
849
|
-
fail('Should have thrown NAuthException');
|
|
850
|
-
} catch (error: any) {
|
|
851
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS);
|
|
852
|
-
expect(tokenWithMethod.maxAttemptsExceeded).toHaveBeenCalledWith(3);
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it('should check max attempts using attempts field if method not available', async () => {
|
|
857
|
-
const tokenWithoutMethod = {
|
|
858
|
-
...mockVerificationToken,
|
|
859
|
-
attempts: 3,
|
|
860
|
-
maxAttemptsExceeded: undefined,
|
|
861
|
-
};
|
|
862
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
863
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(tokenWithoutMethod as any);
|
|
864
|
-
|
|
865
|
-
try {
|
|
866
|
-
await service.verifyPhoneWithCodeBySub('user-sub-123', '123456');
|
|
867
|
-
fail('Should have thrown NAuthException');
|
|
868
|
-
} catch (error: any) {
|
|
869
|
-
expect(error.code).toBe(AuthErrorCode.VERIFICATION_TOO_MANY_ATTEMPTS);
|
|
870
|
-
}
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
// ============================================================================
|
|
875
|
-
// resendVerificationSMS
|
|
876
|
-
// ============================================================================
|
|
877
|
-
|
|
878
|
-
describe('resendVerificationSMS', () => {
|
|
879
|
-
it('should resend verification SMS successfully', async () => {
|
|
880
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
881
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null); // No last token
|
|
882
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
883
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
884
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
885
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
886
|
-
|
|
887
|
-
const result = await service.resendVerificationSMS('user-sub-123');
|
|
888
|
-
|
|
889
|
-
expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { sub: 'user-sub-123' } as any });
|
|
890
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
891
|
-
expect(result).toBe(456);
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
it('should enforce resend delay', async () => {
|
|
895
|
-
const recentToken = {
|
|
896
|
-
...mockVerificationToken,
|
|
897
|
-
createdAt: new Date(Date.now() - 30 * 1000), // 30 seconds ago
|
|
898
|
-
};
|
|
899
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
900
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(recentToken as any);
|
|
901
|
-
|
|
902
|
-
try {
|
|
903
|
-
await service.resendVerificationSMS('user-sub-123');
|
|
904
|
-
fail('Should have thrown NAuthException');
|
|
905
|
-
} catch (error: any) {
|
|
906
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
907
|
-
expect(error.code).toBe(AuthErrorCode.RATE_LIMIT_RESEND);
|
|
908
|
-
}
|
|
909
|
-
});
|
|
910
|
-
|
|
911
|
-
it('should allow resend after delay period', async () => {
|
|
912
|
-
const oldToken = {
|
|
913
|
-
...mockVerificationToken,
|
|
914
|
-
createdAt: new Date(Date.now() - 70 * 1000), // 70 seconds ago
|
|
915
|
-
};
|
|
916
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
917
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(oldToken as any);
|
|
918
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
919
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
920
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
921
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
922
|
-
|
|
923
|
-
await service.resendVerificationSMS('user-sub-123');
|
|
924
|
-
|
|
925
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
it('should throw NAuthException if user not found', async () => {
|
|
929
|
-
mockUserRepository.findOne.mockResolvedValue(null);
|
|
930
|
-
|
|
931
|
-
try {
|
|
932
|
-
await service.resendVerificationSMS('invalid-sub');
|
|
933
|
-
fail('Should have thrown NAuthException');
|
|
934
|
-
} catch (error: any) {
|
|
935
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
936
|
-
expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
it('should delegate to sendVerificationSMS', async () => {
|
|
941
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
942
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
943
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
944
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
945
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
946
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
947
|
-
|
|
948
|
-
await service.resendVerificationSMS('user-sub-123');
|
|
949
|
-
|
|
950
|
-
// Should call sendVerificationSMS with same parameters
|
|
951
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
952
|
-
});
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
// ============================================================================
|
|
956
|
-
// resendVerificationSMSForPhone
|
|
957
|
-
// ============================================================================
|
|
958
|
-
|
|
959
|
-
describe('resendVerificationSMSForPhone', () => {
|
|
960
|
-
it('should resend verification SMS by phone number', async () => {
|
|
961
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
962
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
963
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
964
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
965
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
966
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
967
|
-
|
|
968
|
-
const result = await service.resendVerificationSMSForPhone('+1234567890');
|
|
969
|
-
|
|
970
|
-
expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { phone: '+1234567890' } as any });
|
|
971
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
972
|
-
expect(result).toBe(456);
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
it('should throw NAuthException if user not found by phone', async () => {
|
|
976
|
-
mockUserRepository.findOne.mockResolvedValue(null);
|
|
977
|
-
|
|
978
|
-
try {
|
|
979
|
-
await service.resendVerificationSMSForPhone('+9999999999');
|
|
980
|
-
fail('Should have thrown NAuthException');
|
|
981
|
-
} catch (error: any) {
|
|
982
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
983
|
-
expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
it('should throw NAuthException if phone already verified', async () => {
|
|
988
|
-
const verifiedUser = { ...mockUser, isPhoneVerified: true };
|
|
989
|
-
mockUserRepository.findOne.mockResolvedValue(verifiedUser as any);
|
|
990
|
-
|
|
991
|
-
try {
|
|
992
|
-
await service.resendVerificationSMSForPhone('+1234567890');
|
|
993
|
-
fail('Should have thrown NAuthException');
|
|
994
|
-
} catch (error: any) {
|
|
995
|
-
expect(error).toBeInstanceOf(NAuthException);
|
|
996
|
-
expect(error.code).toBe(AuthErrorCode.ALREADY_VERIFIED);
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it('should delegate to resendVerificationSMS with user sub', async () => {
|
|
1001
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
1002
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
1003
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
1004
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
1005
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
1006
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
1007
|
-
|
|
1008
|
-
await service.resendVerificationSMSForPhone('+1234567890');
|
|
1009
|
-
|
|
1010
|
-
// Should find user by phone, then use their sub to resend
|
|
1011
|
-
expect(mockUserRepository.findOne).toHaveBeenCalledWith({ where: { phone: '+1234567890' } as any });
|
|
1012
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
// ============================================================================
|
|
1017
|
-
// Service Without Optional Dependencies
|
|
1018
|
-
// ============================================================================
|
|
1019
|
-
|
|
1020
|
-
describe('Service without optional dependencies', () => {
|
|
1021
|
-
it('should work without audit service', async () => {
|
|
1022
|
-
const serviceWithoutAudit = new PhoneVerificationService(
|
|
1023
|
-
mockVerificationTokenRepository,
|
|
1024
|
-
mockUserRepository,
|
|
1025
|
-
mockSmsProvider,
|
|
1026
|
-
mockStorageAdapter,
|
|
1027
|
-
mockConfig,
|
|
1028
|
-
mockClientInfoService,
|
|
1029
|
-
mockLogger,
|
|
1030
|
-
undefined, // No audit service
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
1034
|
-
mockStorageAdapter.ttl.mockResolvedValue(3600);
|
|
1035
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
1036
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
1037
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
1038
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
1039
|
-
|
|
1040
|
-
await serviceWithoutAudit.sendVerificationSMS('user-sub-123');
|
|
1041
|
-
|
|
1042
|
-
// Should not throw error
|
|
1043
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
1044
|
-
});
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
// ============================================================================
|
|
1048
|
-
// Edge Cases
|
|
1049
|
-
// ============================================================================
|
|
1050
|
-
|
|
1051
|
-
describe('Edge Cases', () => {
|
|
1052
|
-
it('should handle TTL of -1 (key does not exist)', async () => {
|
|
1053
|
-
mockStorageAdapter.ttl.mockResolvedValue(-1); // Key does not exist
|
|
1054
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
1055
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
1056
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
1057
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
1058
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
1059
|
-
|
|
1060
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
1061
|
-
|
|
1062
|
-
// Should create new window
|
|
1063
|
-
expect(mockStorageAdapter.incr).toHaveBeenCalled();
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
it('should handle TTL of 0 (key expired)', async () => {
|
|
1067
|
-
mockStorageAdapter.ttl.mockResolvedValue(0); // Key expired
|
|
1068
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
1069
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
1070
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
1071
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
1072
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
1073
|
-
|
|
1074
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
1075
|
-
|
|
1076
|
-
// Should create new window (when TTL is 0, window is expired, so TTL parameter is passed)
|
|
1077
|
-
expect(mockStorageAdapter.incr).toHaveBeenCalled();
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
it('should handle negative TTL (key expired)', async () => {
|
|
1081
|
-
mockStorageAdapter.ttl.mockResolvedValue(-10); // Negative TTL (expired)
|
|
1082
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
1083
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
1084
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
1085
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
1086
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
1087
|
-
|
|
1088
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
1089
|
-
|
|
1090
|
-
// Should create new window
|
|
1091
|
-
expect(mockStorageAdapter.incr).toHaveBeenCalled();
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
it('should handle missing config phone verification settings', async () => {
|
|
1095
|
-
mockConfig.phone = undefined;
|
|
1096
|
-
service = new PhoneVerificationService(
|
|
1097
|
-
mockVerificationTokenRepository,
|
|
1098
|
-
mockUserRepository,
|
|
1099
|
-
mockSmsProvider,
|
|
1100
|
-
mockStorageAdapter,
|
|
1101
|
-
mockConfig,
|
|
1102
|
-
mockClientInfoService,
|
|
1103
|
-
mockLogger,
|
|
1104
|
-
mockAuditService,
|
|
1105
|
-
);
|
|
1106
|
-
|
|
1107
|
-
mockStorageAdapter.incr.mockResolvedValue(1);
|
|
1108
|
-
mockStorageAdapter.ttl.mockResolvedValue(-1);
|
|
1109
|
-
mockUserRepository.findOne.mockResolvedValue(mockUser as any);
|
|
1110
|
-
mockVerificationTokenRepository.findOne.mockResolvedValue(null);
|
|
1111
|
-
mockVerificationTokenRepository.create.mockReturnValue(mockVerificationToken as any);
|
|
1112
|
-
mockVerificationTokenRepository.save.mockResolvedValue(mockVerificationToken as any);
|
|
1113
|
-
|
|
1114
|
-
// Should use defaults (rateLimitMax: 3, rateLimitWindow: 3600, resendDelay: 60, expiresIn: 300, maxAttempts: 3)
|
|
1115
|
-
await service.sendVerificationSMS('user-sub-123');
|
|
1116
|
-
|
|
1117
|
-
expect(mockSmsProvider.sendOTP).toHaveBeenCalled();
|
|
1118
|
-
});
|
|
1119
|
-
});
|
|
1120
|
-
});
|