@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.
Files changed (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +30 -0
  3. package/package.json +7 -2
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,505 +0,0 @@
1
- import { Repository } from 'typeorm';
2
- import { TrustedDeviceService } from './trusted-device.service';
3
- import { BaseTrustedDevice } from '../entities/trusted-device.entity';
4
- import { NAuthConfig } from '../interfaces/config.interface';
5
- import { NAuthLogger } from '../utils/nauth-logger';
6
-
7
- /**
8
- * Trusted Device Service Unit Tests
9
- *
10
- * Tests device trust management for "remember device" feature.
11
- * Covers device creation, validation, revocation, and expiration handling.
12
- *
13
- * Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
14
- */
15
- describe('TrustedDeviceService', () => {
16
- let service: TrustedDeviceService;
17
- let mockRepository: jest.Mocked<Repository<BaseTrustedDevice>>;
18
- let mockLogger: jest.Mocked<NAuthLogger>;
19
-
20
- const mockConfig: Partial<NAuthConfig> = {
21
- mfa: {
22
- enabled: true,
23
- rememberDevices: 'user_opt_in',
24
- rememberDeviceDays: 30,
25
- },
26
- };
27
-
28
- const mockTrustedDevice: Partial<BaseTrustedDevice> = {
29
- id: 1,
30
- userId: 1,
31
- deviceTokenHash: 'hashed-token-123',
32
- deviceName: 'My Device',
33
- deviceType: 'desktop',
34
- ipAddress: '1.2.3.4',
35
- userAgent: 'test-user-agent',
36
- platform: 'Windows',
37
- browser: 'Chrome',
38
- trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now
39
- lastUsedAt: new Date(),
40
- createdAt: new Date(),
41
- };
42
-
43
- beforeEach(() => {
44
- // Create mock repository
45
- mockRepository = {
46
- findOne: jest.fn(),
47
- create: jest.fn(),
48
- save: jest.fn(),
49
- update: jest.fn(),
50
- delete: jest.fn(),
51
- find: jest.fn(),
52
- } as any;
53
-
54
- // Create mock logger
55
- mockLogger = {
56
- log: jest.fn(),
57
- error: jest.fn(),
58
- warn: jest.fn(),
59
- debug: jest.fn(),
60
- } as any;
61
-
62
- // Instantiate service directly
63
- service = new TrustedDeviceService(mockConfig as NAuthConfig, mockLogger, mockRepository);
64
- });
65
-
66
- afterEach(() => {
67
- jest.clearAllMocks();
68
- // Restore crypto.randomUUID if it was mocked
69
- jest.restoreAllMocks();
70
- });
71
-
72
- // ============================================================================
73
- // Service Initialization
74
- // ============================================================================
75
-
76
- it('should be defined', () => {
77
- expect(service).toBeDefined();
78
- });
79
-
80
- // ============================================================================
81
- // createTrustedDevice() Method
82
- // ============================================================================
83
-
84
- describe('createTrustedDevice', () => {
85
- it('should create trusted device successfully', async () => {
86
- mockRepository.findOne.mockResolvedValue(null);
87
- mockRepository.create.mockReturnValue(mockTrustedDevice as any);
88
- mockRepository.save.mockResolvedValue(mockTrustedDevice as any);
89
-
90
- const result = await service.createTrustedDevice(
91
- 1,
92
- 'My Device',
93
- 'desktop',
94
- '1.2.3.4',
95
- 'test-user-agent',
96
- 'Windows',
97
- 'Chrome',
98
- );
99
-
100
- // Verify result is a valid UUID format
101
- expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
102
- expect(mockRepository.create).toHaveBeenCalled();
103
- expect(mockRepository.save).toHaveBeenCalled();
104
- });
105
-
106
- it('should throw error when rememberDevices is not enabled', async () => {
107
- const configDisabled: Partial<NAuthConfig> = {
108
- mfa: {
109
- enabled: true,
110
- rememberDevices: 'never',
111
- },
112
- };
113
-
114
- const serviceDisabled = new TrustedDeviceService(configDisabled as NAuthConfig, mockLogger, mockRepository);
115
-
116
- try {
117
- await serviceDisabled.createTrustedDevice(1);
118
- fail('Should have thrown Error');
119
- } catch (error: any) {
120
- expect(error.message).toContain('rememberDevices is not enabled');
121
- }
122
- });
123
-
124
- it('should throw error when repository not available', async () => {
125
- const serviceWithoutRepo = new TrustedDeviceService(mockConfig as NAuthConfig, mockLogger, undefined);
126
-
127
- try {
128
- await serviceWithoutRepo.createTrustedDevice(1);
129
- fail('Should have thrown Error');
130
- } catch (error: any) {
131
- expect(error.message).toContain('TrustedDeviceRepository not available');
132
- }
133
- });
134
-
135
- it('should update existing device if already trusted', async () => {
136
- const existingDevice = { ...mockTrustedDevice };
137
- // Calculate hash for the device token that will be generated
138
- const createHash = (await import('crypto')).createHash;
139
- // We'll verify the hash was calculated correctly by checking the update was called
140
-
141
- mockRepository.findOne.mockResolvedValue(existingDevice as any);
142
- mockRepository.update.mockResolvedValue({ affected: 1 } as any);
143
-
144
- const result = await service.createTrustedDevice(1, 'Updated Device Name');
145
-
146
- // Verify result is a valid UUID format
147
- expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
148
- expect(mockRepository.update).toHaveBeenCalled();
149
- expect(mockRepository.create).not.toHaveBeenCalled();
150
- });
151
-
152
- it('should calculate expiry based on rememberDeviceDays', async () => {
153
- const configWithDays: Partial<NAuthConfig> = {
154
- mfa: {
155
- enabled: true,
156
- rememberDevices: 'user_opt_in',
157
- rememberDeviceDays: 60,
158
- },
159
- };
160
-
161
- const serviceWithDays = new TrustedDeviceService(configWithDays as NAuthConfig, mockLogger, mockRepository);
162
-
163
- mockRepository.findOne.mockResolvedValue(null);
164
- mockRepository.create.mockReturnValue(mockTrustedDevice as any);
165
- mockRepository.save.mockResolvedValue(mockTrustedDevice as any);
166
-
167
- await serviceWithDays.createTrustedDevice(1);
168
-
169
- // Verify trustedUntil is set correctly (60 days from now)
170
- const createCall = mockRepository.create.mock.calls[0][0] as any;
171
- const expectedDate = new Date();
172
- expectedDate.setDate(expectedDate.getDate() + 60);
173
- expect(new Date(createCall.trustedUntil).getTime()).toBeCloseTo(expectedDate.getTime(), -3); // Within 1 second
174
- });
175
-
176
- it('should store all device information', async () => {
177
- mockRepository.findOne.mockResolvedValue(null);
178
- mockRepository.create.mockReturnValue(mockTrustedDevice as any);
179
- mockRepository.save.mockResolvedValue(mockTrustedDevice as any);
180
-
181
- await service.createTrustedDevice(1, 'My Device', 'desktop', '1.2.3.4', 'test-user-agent', 'Windows', 'Chrome');
182
-
183
- expect(mockRepository.create).toHaveBeenCalledWith(
184
- (expect as any).objectContaining({
185
- userId: 1,
186
- deviceName: 'My Device',
187
- deviceType: 'desktop',
188
- ipAddress: '1.2.3.4',
189
- userAgent: 'test-user-agent',
190
- platform: 'Windows',
191
- browser: 'Chrome',
192
- }),
193
- );
194
- });
195
-
196
- it('should handle null/undefined device information', async () => {
197
- mockRepository.findOne.mockResolvedValue(null);
198
- mockRepository.create.mockReturnValue(mockTrustedDevice as any);
199
- mockRepository.save.mockResolvedValue(mockTrustedDevice as any);
200
-
201
- await service.createTrustedDevice(1, null, null, null, null, null, null);
202
-
203
- expect(mockRepository.create).toHaveBeenCalledWith(
204
- (expect as any).objectContaining({
205
- deviceName: null,
206
- deviceType: null,
207
- ipAddress: null,
208
- userAgent: null,
209
- platform: null,
210
- browser: null,
211
- }),
212
- );
213
- });
214
- });
215
-
216
- // ============================================================================
217
- // isDeviceTrusted() Method
218
- // ============================================================================
219
-
220
- describe('isDeviceTrusted', () => {
221
- it('should return false when deviceToken is null', async () => {
222
- const result = await service.isDeviceTrusted(null, 1);
223
-
224
- expect(result).toBe(false);
225
- });
226
-
227
- it('should return false when deviceToken is undefined', async () => {
228
- const result = await service.isDeviceTrusted(undefined, 1);
229
-
230
- expect(result).toBe(false);
231
- });
232
-
233
- it('should return false when repository not available', async () => {
234
- const serviceWithoutRepo = new TrustedDeviceService(mockConfig as NAuthConfig, mockLogger, undefined);
235
-
236
- const result = await serviceWithoutRepo.isDeviceTrusted('token', 1);
237
-
238
- expect(result).toBe(false);
239
- });
240
-
241
- it('should return false when rememberDevices is not enabled', async () => {
242
- const configDisabled: Partial<NAuthConfig> = {
243
- mfa: {
244
- enabled: true,
245
- rememberDevices: 'never',
246
- },
247
- };
248
-
249
- const serviceDisabled = new TrustedDeviceService(configDisabled as NAuthConfig, mockLogger, mockRepository);
250
-
251
- const result = await serviceDisabled.isDeviceTrusted('token', 1);
252
-
253
- expect(result).toBe(false);
254
- });
255
-
256
- it('should return true when device is trusted and not expired', async () => {
257
- const deviceToken = 'device-token-uuid-123';
258
- const trustedDevice = {
259
- ...mockTrustedDevice,
260
- trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // Future date
261
- };
262
-
263
- mockRepository.findOne.mockResolvedValue(trustedDevice as any);
264
-
265
- const result = await service.isDeviceTrusted(deviceToken, 1);
266
-
267
- expect(result).toBe(true);
268
- });
269
-
270
- it('should return false when device not found', async () => {
271
- const deviceToken = 'device-token-uuid-123';
272
-
273
- mockRepository.findOne.mockResolvedValue(null);
274
-
275
- const result = await service.isDeviceTrusted(deviceToken, 1);
276
-
277
- expect(result).toBe(false);
278
- });
279
-
280
- it('should return false and delete when device expired', async () => {
281
- const deviceToken = 'device-token-uuid-123';
282
- const expiredDevice = {
283
- ...mockTrustedDevice,
284
- trustedUntil: new Date(Date.now() - 1000), // Past date
285
- };
286
-
287
- mockRepository.findOne.mockResolvedValue(expiredDevice as any);
288
- mockRepository.delete.mockResolvedValue({ affected: 1 } as any);
289
-
290
- const result = await service.isDeviceTrusted(deviceToken, 1);
291
-
292
- expect(result).toBe(false);
293
- expect(mockRepository.delete).toHaveBeenCalled();
294
- expect(mockLogger.debug).toHaveBeenCalledWith((expect as any).stringContaining('Trusted device expired'));
295
- });
296
-
297
- it('should update lastUsedAt when device is trusted', async () => {
298
- const deviceToken = 'device-token-uuid-123';
299
- const trustedDevice = {
300
- ...mockTrustedDevice,
301
- trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
302
- lastUsedAt: new Date(Date.now() - 20 * 60 * 1000), // 20 minutes ago
303
- };
304
-
305
- mockRepository.findOne.mockResolvedValue(trustedDevice as any);
306
- mockRepository.update.mockResolvedValue({ affected: 1 } as any);
307
-
308
- await service.isDeviceTrusted(deviceToken, 1);
309
-
310
- expect(mockRepository.update).toHaveBeenCalled();
311
- });
312
-
313
- it('should throttle lastUsedAt updates to once per 15 minutes', async () => {
314
- const deviceToken = 'device-token-uuid-123';
315
- const trustedDevice = {
316
- ...mockTrustedDevice,
317
- trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
318
- lastUsedAt: new Date(), // Just now
319
- };
320
-
321
- mockRepository.findOne.mockResolvedValue(trustedDevice as any);
322
-
323
- await service.isDeviceTrusted(deviceToken, 1);
324
-
325
- // Should not update if lastUsedAt is recent
326
- expect(mockRepository.update).not.toHaveBeenCalled();
327
- });
328
- });
329
-
330
- // ============================================================================
331
- // validateDeviceToken() Method
332
- // ============================================================================
333
-
334
- describe('validateDeviceToken', () => {
335
- it('should return not suspicious when token is null', async () => {
336
- const result = await service.validateDeviceToken(null, 1);
337
-
338
- expect(result).toEqual({ isValid: false, isSuspicious: false });
339
- });
340
-
341
- it('should return not suspicious when token is undefined', async () => {
342
- const result = await service.validateDeviceToken(undefined, 1);
343
-
344
- expect(result).toEqual({ isValid: false, isSuspicious: false });
345
- });
346
-
347
- it('should return valid and not suspicious when device is trusted', async () => {
348
- const deviceToken = 'device-token-uuid-123';
349
- const trustedDevice = {
350
- ...mockTrustedDevice,
351
- trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
352
- };
353
-
354
- mockRepository.findOne.mockResolvedValue(trustedDevice as any);
355
-
356
- const result = await service.validateDeviceToken(deviceToken, 1);
357
-
358
- expect(result).toEqual({ isValid: true, isSuspicious: false });
359
- });
360
-
361
- it('should return suspicious when token provided but not trusted', async () => {
362
- const deviceToken = 'device-token-uuid-123';
363
-
364
- mockRepository.findOne.mockResolvedValue(null);
365
-
366
- const result = await service.validateDeviceToken(deviceToken, 1);
367
-
368
- expect(result).toEqual({ isValid: false, isSuspicious: true });
369
- });
370
-
371
- it('should return suspicious when token provided but expired', async () => {
372
- const deviceToken = 'device-token-uuid-123';
373
- const expiredDevice = {
374
- ...mockTrustedDevice,
375
- trustedUntil: new Date(Date.now() - 1000),
376
- };
377
-
378
- mockRepository.findOne.mockResolvedValue(expiredDevice as any);
379
- mockRepository.delete.mockResolvedValue({ affected: 1 } as any);
380
-
381
- const result = await service.validateDeviceToken(deviceToken, 1);
382
-
383
- expect(result).toEqual({ isValid: false, isSuspicious: true });
384
- });
385
- });
386
-
387
- // ============================================================================
388
- // revokeTrustedDevice() Method
389
- // ============================================================================
390
-
391
- describe('revokeTrustedDevice', () => {
392
- it('should revoke trusted device successfully', async () => {
393
- const deviceToken = 'device-token-uuid-123';
394
-
395
- mockRepository.delete.mockResolvedValue({ affected: 1 } as any);
396
-
397
- await service.revokeTrustedDevice(deviceToken, 1);
398
-
399
- expect(mockRepository.delete).toHaveBeenCalled();
400
- expect(mockLogger.debug).toHaveBeenCalledWith((expect as any).stringContaining('Revoked trusted device'));
401
- });
402
-
403
- it('should return early when repository not available', async () => {
404
- const serviceWithoutRepo = new TrustedDeviceService(mockConfig as NAuthConfig, mockLogger, undefined);
405
-
406
- await serviceWithoutRepo.revokeTrustedDevice('token', 1);
407
-
408
- // Should not throw, just return early
409
- expect(mockLogger.debug).not.toHaveBeenCalled();
410
- });
411
-
412
- it('should delete device by hash', async () => {
413
- const deviceToken = 'device-token-uuid-123';
414
- const createHash = (await import('crypto')).createHash;
415
- const hash = createHash('sha256').update(deviceToken).digest('hex');
416
-
417
- mockRepository.delete.mockResolvedValue({ affected: 1 } as any);
418
-
419
- await service.revokeTrustedDevice(deviceToken, 1);
420
-
421
- expect(mockRepository.delete).toHaveBeenCalledWith({
422
- userId: 1,
423
- deviceTokenHash: hash,
424
- });
425
- });
426
- });
427
-
428
- // ============================================================================
429
- // getUserTrustedDevices() Method
430
- // ============================================================================
431
-
432
- describe('getUserTrustedDevices', () => {
433
- it('should return user trusted devices', async () => {
434
- const devices = [
435
- { ...mockTrustedDevice, id: 1 },
436
- { ...mockTrustedDevice, id: 2 },
437
- ];
438
-
439
- mockRepository.find.mockResolvedValue(devices as any);
440
-
441
- const result = await service.getUserTrustedDevices(1);
442
-
443
- expect(result.length).toBe(2);
444
- expect('deviceTokenHash' in result[0]).toBe(false);
445
- expect('deviceTokenHash' in result[1]).toBe(false);
446
- });
447
-
448
- it('should return empty array when repository not available', async () => {
449
- const serviceWithoutRepo = new TrustedDeviceService(mockConfig as NAuthConfig, mockLogger, undefined);
450
-
451
- const result = await serviceWithoutRepo.getUserTrustedDevices(1);
452
-
453
- expect(result).toEqual([]);
454
- });
455
-
456
- it('should filter out expired devices', async () => {
457
- const devices = [
458
- { ...mockTrustedDevice, id: 1, trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) },
459
- { ...mockTrustedDevice, id: 2, trustedUntil: new Date(Date.now() - 1000) }, // Expired
460
- ];
461
-
462
- mockRepository.find.mockResolvedValue(devices as any);
463
-
464
- const result = await service.getUserTrustedDevices(1);
465
-
466
- expect(result.length).toBe(1);
467
- expect(result[0].id).toBe(1);
468
- });
469
-
470
- it('should order devices by lastUsedAt DESC', async () => {
471
- const devices = [
472
- { ...mockTrustedDevice, id: 1, lastUsedAt: new Date('2025-01-01') },
473
- { ...mockTrustedDevice, id: 2, lastUsedAt: new Date('2025-01-02') },
474
- ];
475
-
476
- mockRepository.find.mockResolvedValue(devices as any);
477
-
478
- await service.getUserTrustedDevices(1);
479
-
480
- expect(mockRepository.find).toHaveBeenCalledWith({
481
- where: { userId: 1 },
482
- order: { lastUsedAt: 'DESC' },
483
- });
484
- });
485
-
486
- it('should exclude deviceTokenHash from results', async () => {
487
- const devices = [
488
- {
489
- ...mockTrustedDevice,
490
- id: 1,
491
- deviceTokenHash: 'hash-123',
492
- trustedUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
493
- },
494
- ];
495
-
496
- mockRepository.find.mockResolvedValue(devices as any);
497
-
498
- const result = await service.getUserTrustedDevices(1);
499
-
500
- expect('deviceTokenHash' in result[0]).toBe(false);
501
- expect('id' in result[0]).toBe(true);
502
- expect('userId' in result[0]).toBe(true);
503
- });
504
- });
505
- });