@nauth-toolkit/core 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +9 -0
  3. package/package.json +8 -3
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,603 +0,0 @@
1
- import { GeoLocationService } from './geo-location.service';
2
- import { StorageAdapter } from '../interfaces/storage-adapter.interface';
3
- import { NAuthLogger } from '../utils/nauth-logger';
4
- import { NAuthException } from '../exceptions/nauth.exception';
5
- import { NAuthConfig } from '../interfaces/config.interface';
6
- import * as fs from 'fs/promises';
7
- import * as path from 'path';
8
- import * as os from 'os';
9
-
10
- // Mock fs/promises
11
- jest.mock('fs/promises');
12
- const mockedFs = fs as jest.Mocked<typeof fs>;
13
-
14
- // Mock child_process
15
- jest.mock('child_process', () => ({
16
- exec: jest.fn(),
17
- }));
18
-
19
- /**
20
- * GeoLocation Service Unit Tests
21
- *
22
- * Tests IP geolocation functionality using MaxMind GeoIP2 databases.
23
- * Covers initialization, database loading, IP lookup, and error handling.
24
- *
25
- * Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
26
- */
27
- describe('GeoLocationService', () => {
28
- let service: GeoLocationService;
29
- let mockStorageAdapter: jest.Mocked<StorageAdapter>;
30
- let mockLogger: jest.Mocked<NAuthLogger>;
31
- let mockMaxMindLib: any;
32
- let mockCityReader: any;
33
- let mockCountryReader: any;
34
-
35
- const mockConfig: Partial<NAuthConfig> = {
36
- geoLocation: {
37
- maxMind: {
38
- accountId: 12345,
39
- licenseKey: 'test-license-key',
40
- dbPath: '/tmp/test-maxmind',
41
- autoDownloadOnStartup: false,
42
- skipDownloads: false,
43
- editions: ['GeoLite2-City', 'GeoLite2-Country'],
44
- },
45
- },
46
- };
47
-
48
- beforeEach(() => {
49
- // Create mock MaxMind readers
50
- mockCityReader = {
51
- city: jest.fn(),
52
- };
53
-
54
- mockCountryReader = {
55
- country: jest.fn(),
56
- };
57
-
58
- // Create mock MaxMind library
59
- mockMaxMindLib = {
60
- Reader: {
61
- open: jest.fn(),
62
- },
63
- };
64
-
65
- // Create mock storage adapter
66
- mockStorageAdapter = {
67
- initialize: jest.fn(),
68
- isHealthy: jest.fn(),
69
- get: jest.fn(),
70
- set: jest.fn(),
71
- del: jest.fn(),
72
- exists: jest.fn(),
73
- incr: jest.fn(),
74
- decr: jest.fn(),
75
- expire: jest.fn(),
76
- ttl: jest.fn(),
77
- hget: jest.fn(),
78
- hset: jest.fn(),
79
- hgetall: jest.fn(),
80
- hdel: jest.fn(),
81
- lpush: jest.fn(),
82
- lrange: jest.fn(),
83
- llen: jest.fn(),
84
- keys: jest.fn(),
85
- scan: jest.fn(),
86
- cleanup: jest.fn(),
87
- disconnect: jest.fn(),
88
- } as any;
89
-
90
- // Create mock logger
91
- mockLogger = {
92
- log: jest.fn(),
93
- error: jest.fn(),
94
- warn: jest.fn(),
95
- debug: jest.fn(),
96
- } as any;
97
-
98
- // Reset mocks
99
- jest.clearAllMocks();
100
- mockedFs.mkdir.mockResolvedValue(undefined);
101
- mockedFs.readdir.mockResolvedValue([]);
102
- mockedFs.stat.mockResolvedValue({ isDirectory: () => false } as any);
103
- mockedFs.writeFile.mockResolvedValue(undefined);
104
- mockedFs.unlink.mockResolvedValue(undefined);
105
- mockedFs.rename.mockResolvedValue(undefined);
106
- });
107
-
108
- afterEach(() => {
109
- jest.clearAllMocks();
110
- });
111
-
112
- // ============================================================================
113
- // Service Initialization
114
- // ============================================================================
115
-
116
- it('should be defined', () => {
117
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
118
- expect(service).toBeDefined();
119
- });
120
-
121
- it('should warn when MaxMind configured but library not installed', () => {
122
- new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, null, mockLogger);
123
-
124
- expect(mockLogger.warn).toHaveBeenCalledWith(
125
- (expect as any).stringContaining(
126
- 'MaxMind GeoIP2 is configured but @maxmind/geoip2-node package is not installed',
127
- ),
128
- );
129
- });
130
-
131
- it('should use configured dbPath when provided', () => {
132
- const customPath = '/custom/path';
133
- const configWithPath: Partial<NAuthConfig> = {
134
- geoLocation: {
135
- maxMind: {
136
- ...mockConfig.geoLocation!.maxMind!,
137
- dbPath: customPath,
138
- },
139
- },
140
- };
141
-
142
- service = new GeoLocationService(configWithPath as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
143
-
144
- // Service should use the configured path
145
- expect(service).toBeDefined();
146
- });
147
-
148
- it('should use system temp directory when dbPath not configured', () => {
149
- const configWithoutPath: Partial<NAuthConfig> = {
150
- geoLocation: {
151
- maxMind: {
152
- accountId: 12345,
153
- licenseKey: 'test-key',
154
- },
155
- },
156
- };
157
-
158
- const systemTemp = os.tmpdir();
159
- service = new GeoLocationService(configWithoutPath as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
160
-
161
- expect(service).toBeDefined();
162
- // Path should default to system temp
163
- });
164
-
165
- // ============================================================================
166
- // onModuleInit() Method
167
- // ============================================================================
168
-
169
- describe('onModuleInit', () => {
170
- it('should return early when config not provided', async () => {
171
- service = new GeoLocationService({} as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
172
-
173
- await service.onModuleInit();
174
-
175
- expect(mockedFs.mkdir).not.toHaveBeenCalled();
176
- });
177
-
178
- it('should return early when MaxMind library not available', async () => {
179
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, null, mockLogger);
180
-
181
- await service.onModuleInit();
182
-
183
- expect(mockLogger.warn).toHaveBeenCalledWith(
184
- (expect as any).stringContaining('MaxMind GeoIP2 library not available'),
185
- );
186
- });
187
-
188
- it('should ensure database directory exists', async () => {
189
- mockMaxMindLib.Reader.open.mockRejectedValue(new Error('File not found'));
190
-
191
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
192
-
193
- await service.onModuleInit();
194
-
195
- expect(mockedFs.mkdir).toHaveBeenCalledWith(mockConfig.geoLocation!.maxMind!.dbPath!, { recursive: true });
196
- });
197
-
198
- it('should load existing database files', async () => {
199
- mockMaxMindLib.Reader.open.mockResolvedValueOnce(mockCityReader).mockResolvedValueOnce(mockCountryReader);
200
-
201
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
202
-
203
- await service.onModuleInit();
204
-
205
- expect(mockMaxMindLib.Reader.open).toHaveBeenCalledWith(
206
- path.join(mockConfig.geoLocation!.maxMind!.dbPath!, 'GeoLite2-City.mmdb'),
207
- );
208
- expect(mockMaxMindLib.Reader.open).toHaveBeenCalledWith(
209
- path.join(mockConfig.geoLocation!.maxMind!.dbPath!, 'GeoLite2-Country.mmdb'),
210
- );
211
- });
212
-
213
- it('should handle missing database files gracefully', async () => {
214
- mockMaxMindLib.Reader.open.mockRejectedValue(new Error('File not found'));
215
-
216
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
217
-
218
- await service.onModuleInit();
219
-
220
- expect(mockLogger.debug).toHaveBeenCalledWith((expect as any).stringContaining('Failed to load City database'));
221
- expect(mockLogger.debug).toHaveBeenCalledWith(
222
- (expect as any).stringContaining('Failed to load Country database'),
223
- );
224
- });
225
-
226
- it('should auto-download databases if enabled and files missing', async () => {
227
- mockMaxMindLib.Reader.open.mockRejectedValue(new Error('File not found'));
228
- mockStorageAdapter.set.mockResolvedValue('lock-value');
229
- mockStorageAdapter.del.mockResolvedValue();
230
-
231
- // Mock fetch to fail quickly to prevent timeout
232
- global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
233
-
234
- const configWithAutoDownload: Partial<NAuthConfig> = {
235
- geoLocation: {
236
- maxMind: {
237
- ...mockConfig.geoLocation!.maxMind!,
238
- autoDownloadOnStartup: true,
239
- },
240
- },
241
- };
242
-
243
- service = new GeoLocationService(
244
- configWithAutoDownload as NAuthConfig,
245
- mockStorageAdapter,
246
- mockMaxMindLib,
247
- mockLogger,
248
- );
249
-
250
- // This will fail in test environment but should attempt download
251
- await Promise.race([service.onModuleInit(), new Promise((resolve) => setTimeout(() => resolve('timeout'), 100))]);
252
-
253
- // Should attempt to acquire lock
254
- expect(mockStorageAdapter.set).toHaveBeenCalled();
255
- }, 1000);
256
-
257
- it('should skip auto-download when skipDownloads is true', async () => {
258
- mockMaxMindLib.Reader.open.mockRejectedValue(new Error('File not found'));
259
-
260
- const configWithSkipDownloads: Partial<NAuthConfig> = {
261
- geoLocation: {
262
- maxMind: {
263
- ...mockConfig.geoLocation!.maxMind!,
264
- skipDownloads: true,
265
- autoDownloadOnStartup: true,
266
- },
267
- },
268
- };
269
-
270
- service = new GeoLocationService(
271
- configWithSkipDownloads as NAuthConfig,
272
- mockStorageAdapter,
273
- mockMaxMindLib,
274
- mockLogger,
275
- );
276
-
277
- await service.onModuleInit();
278
-
279
- // Should not attempt download
280
- expect(mockStorageAdapter.set).not.toHaveBeenCalled();
281
- });
282
- });
283
-
284
- // ============================================================================
285
- // getIpGeolocation() Method
286
- // ============================================================================
287
-
288
- describe('getIpGeolocation', () => {
289
- beforeEach(() => {
290
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
291
- });
292
-
293
- it('should return empty object when config not provided', async () => {
294
- const serviceWithoutConfig = new GeoLocationService(
295
- {} as NAuthConfig,
296
- mockStorageAdapter,
297
- mockMaxMindLib,
298
- mockLogger,
299
- );
300
-
301
- const result = await serviceWithoutConfig.getIpGeolocation('8.8.8.8');
302
-
303
- expect(result).toEqual({});
304
- });
305
-
306
- it('should return empty object when MaxMind library not available', async () => {
307
- const serviceWithoutLib = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, null, mockLogger);
308
-
309
- const result = await serviceWithoutLib.getIpGeolocation('8.8.8.8');
310
-
311
- expect(result).toEqual({});
312
- });
313
-
314
- it('should skip private IP addresses', async () => {
315
- // Set up service with loaded readers
316
- (service as any).cityReader = mockCityReader;
317
- (service as any).countryReader = mockCountryReader;
318
-
319
- const result = await service.getIpGeolocation('192.168.1.1');
320
-
321
- expect(result).toEqual({});
322
- expect(mockCityReader.city).not.toHaveBeenCalled();
323
- expect(mockCountryReader.country).not.toHaveBeenCalled();
324
- expect(mockLogger.debug).toHaveBeenCalledWith((expect as any).stringContaining('Skipping private IP'));
325
- });
326
-
327
- it('should lookup IP in city database first', async () => {
328
- (service as any).cityReader = mockCityReader;
329
- (service as any).countryReader = mockCountryReader;
330
-
331
- mockCityReader.city.mockReturnValue({
332
- country: { isoCode: 'US' },
333
- city: { names: { en: 'Mountain View' } },
334
- });
335
-
336
- const result = await service.getIpGeolocation('8.8.8.8');
337
-
338
- expect(result).toEqual({
339
- country: 'US',
340
- city: 'Mountain View',
341
- });
342
- expect(mockCityReader.city).toHaveBeenCalledWith('8.8.8.8');
343
- expect(mockCountryReader.country).not.toHaveBeenCalled();
344
- });
345
-
346
- it('should fallback to country database when city lookup fails', async () => {
347
- (service as any).cityReader = mockCityReader;
348
- (service as any).countryReader = mockCountryReader;
349
-
350
- mockCityReader.city.mockImplementation(() => {
351
- throw new Error('City lookup failed');
352
- });
353
- mockCountryReader.country.mockReturnValue({
354
- country: { isoCode: 'US' },
355
- });
356
-
357
- const result = await service.getIpGeolocation('8.8.8.8');
358
-
359
- expect(result).toEqual({
360
- country: 'US',
361
- });
362
- expect(mockCityReader.city).toHaveBeenCalled();
363
- expect(mockCountryReader.country).toHaveBeenCalledWith('8.8.8.8');
364
- expect(mockLogger.debug).toHaveBeenCalledWith((expect as any).stringContaining('City lookup failed'));
365
- });
366
-
367
- it('should return empty object when both lookups fail', async () => {
368
- (service as any).cityReader = mockCityReader;
369
- (service as any).countryReader = mockCountryReader;
370
-
371
- mockCityReader.city.mockImplementation(() => {
372
- throw new Error('City lookup failed');
373
- });
374
- mockCountryReader.country.mockImplementation(() => {
375
- throw new Error('Country lookup failed');
376
- });
377
-
378
- const result = await service.getIpGeolocation('8.8.8.8');
379
-
380
- expect(result).toEqual({});
381
- expect(mockLogger.debug).toHaveBeenCalledWith((expect as any).stringContaining('Country lookup failed'));
382
- });
383
-
384
- it('should return empty object when no databases loaded', async () => {
385
- (service as any).cityReader = null;
386
- (service as any).countryReader = null;
387
-
388
- const result = await service.getIpGeolocation('8.8.8.8');
389
-
390
- expect(result).toEqual({});
391
- });
392
- });
393
-
394
- // ============================================================================
395
- // updateGeoLocationDatabase() Method
396
- // ============================================================================
397
-
398
- describe('updateGeoLocationDatabase', () => {
399
- beforeEach(() => {
400
- service = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, mockMaxMindLib, mockLogger);
401
- });
402
-
403
- it('should throw error when config not provided', async () => {
404
- const serviceWithoutConfig = new GeoLocationService(
405
- {} as NAuthConfig,
406
- mockStorageAdapter,
407
- mockMaxMindLib,
408
- mockLogger,
409
- );
410
-
411
- try {
412
- await serviceWithoutConfig.updateGeoLocationDatabase();
413
- fail('Should have thrown NAuthException');
414
- } catch (error: any) {
415
- expect(error).toBeInstanceOf(NAuthException);
416
- expect(error.message).toContain('MaxMind configuration not provided');
417
- }
418
- });
419
-
420
- it('should throw error when MaxMind library not available', async () => {
421
- const serviceWithoutLib = new GeoLocationService(mockConfig as NAuthConfig, mockStorageAdapter, null, mockLogger);
422
-
423
- try {
424
- await serviceWithoutLib.updateGeoLocationDatabase();
425
- fail('Should have thrown NAuthException');
426
- } catch (error: any) {
427
- expect(error).toBeInstanceOf(NAuthException);
428
- expect(error.message).toContain('MaxMind library not available');
429
- }
430
- });
431
-
432
- it('should throw error when skipDownloads is true', async () => {
433
- const configWithSkipDownloads: Partial<NAuthConfig> = {
434
- geoLocation: {
435
- maxMind: {
436
- ...mockConfig.geoLocation!.maxMind!,
437
- skipDownloads: true,
438
- },
439
- },
440
- };
441
-
442
- const serviceWithSkipDownloads = new GeoLocationService(
443
- configWithSkipDownloads as NAuthConfig,
444
- mockStorageAdapter,
445
- mockMaxMindLib,
446
- mockLogger,
447
- );
448
-
449
- try {
450
- await serviceWithSkipDownloads.updateGeoLocationDatabase();
451
- fail('Should have thrown NAuthException');
452
- } catch (error: any) {
453
- expect(error).toBeInstanceOf(NAuthException);
454
- expect(error.message).toContain('Database downloads are disabled');
455
- }
456
- });
457
-
458
- it('should throw error when licenseKey missing', async () => {
459
- const configWithoutKey: Partial<NAuthConfig> = {
460
- geoLocation: {
461
- maxMind: {
462
- accountId: 12345,
463
- // licenseKey missing
464
- },
465
- },
466
- };
467
-
468
- const serviceWithoutKey = new GeoLocationService(
469
- configWithoutKey as NAuthConfig,
470
- mockStorageAdapter,
471
- mockMaxMindLib,
472
- mockLogger,
473
- );
474
-
475
- try {
476
- await serviceWithoutKey.updateGeoLocationDatabase();
477
- fail('Should have thrown NAuthException');
478
- } catch (error: any) {
479
- expect(error).toBeInstanceOf(NAuthException);
480
- expect(error.message).toContain('MaxMind licenseKey and accountId are required');
481
- }
482
- });
483
-
484
- it('should throw error when accountId missing', async () => {
485
- const configWithoutAccountId: Partial<NAuthConfig> = {
486
- geoLocation: {
487
- maxMind: {
488
- licenseKey: 'test-key',
489
- // accountId missing
490
- },
491
- },
492
- };
493
-
494
- const serviceWithoutAccountId = new GeoLocationService(
495
- configWithoutAccountId as NAuthConfig,
496
- mockStorageAdapter,
497
- mockMaxMindLib,
498
- mockLogger,
499
- );
500
-
501
- try {
502
- await serviceWithoutAccountId.updateGeoLocationDatabase();
503
- fail('Should have thrown NAuthException');
504
- } catch (error: any) {
505
- expect(error).toBeInstanceOf(NAuthException);
506
- expect(error.message).toContain('MaxMind licenseKey and accountId are required');
507
- }
508
- });
509
-
510
- it('should skip update when lock already acquired', async () => {
511
- mockStorageAdapter.set.mockResolvedValue(null); // Lock not acquired
512
-
513
- await service.updateGeoLocationDatabase();
514
-
515
- expect(mockLogger.warn).toHaveBeenCalledWith('MaxMind database update already in progress, skipping...');
516
- expect(mockStorageAdapter.del).not.toHaveBeenCalled();
517
- });
518
-
519
- it('should acquire distributed lock before downloading', async () => {
520
- mockStorageAdapter.set.mockResolvedValue('lock-value');
521
- mockStorageAdapter.del.mockResolvedValue();
522
-
523
- // Mock fetch to throw error immediately to prevent actual download
524
- global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
525
-
526
- // This will fail but should attempt to acquire lock first
527
- try {
528
- await Promise.race([
529
- service.updateGeoLocationDatabase(),
530
- new Promise((resolve) => setTimeout(() => resolve('timeout'), 100)),
531
- ]);
532
- } catch {
533
- // Expected to fail
534
- }
535
-
536
- expect(mockStorageAdapter.set).toHaveBeenCalledWith(
537
- 'maxmind-db-update-lock',
538
- (expect as any).stringContaining('lock-'),
539
- 300,
540
- { nx: true },
541
- );
542
- }, 1000);
543
-
544
- it('should release lock after download completes', async () => {
545
- mockStorageAdapter.set.mockResolvedValue('lock-value');
546
- mockStorageAdapter.del.mockResolvedValue();
547
- global.fetch = jest.fn().mockResolvedValue({
548
- ok: true,
549
- arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0)),
550
- } as any);
551
-
552
- // Mock exec to prevent timeout
553
- const mockExec = jest.fn().mockResolvedValue({ stdout: '', stderr: '' });
554
- jest.doMock('child_process', () => ({
555
- exec: mockExec,
556
- }));
557
- jest.doMock('util', () => ({
558
- promisify: jest.fn(() => mockExec),
559
- }));
560
-
561
- // This will fail in test but should attempt to release lock
562
- try {
563
- await Promise.race([
564
- service.updateGeoLocationDatabase(),
565
- new Promise((resolve) => setTimeout(resolve, 100)), // Timeout after 100ms
566
- ]);
567
- } catch {
568
- // Expected to fail in test environment
569
- }
570
-
571
- // Should attempt to release lock in finally block
572
- expect(mockStorageAdapter.del).toHaveBeenCalledWith('maxmind-db-update-lock');
573
- }, 1000);
574
-
575
- it('should handle lock release errors gracefully', async () => {
576
- mockStorageAdapter.set.mockResolvedValue('lock-value');
577
- mockStorageAdapter.del.mockRejectedValue(new Error('Lock release failed'));
578
-
579
- // Mock exec to prevent timeout
580
- const mockExec = jest.fn().mockResolvedValue({ stdout: '', stderr: '' });
581
- jest.doMock('child_process', () => ({
582
- exec: mockExec,
583
- }));
584
- jest.doMock('util', () => ({
585
- promisify: jest.fn(() => mockExec),
586
- }));
587
-
588
- // This will fail in test but should handle lock release error
589
- try {
590
- await Promise.race([
591
- service.updateGeoLocationDatabase(),
592
- new Promise((resolve) => setTimeout(resolve, 100)), // Timeout after 100ms
593
- ]);
594
- } catch {
595
- // Expected to fail in test environment
596
- }
597
-
598
- expect(mockLogger.warn).toHaveBeenCalledWith(
599
- (expect as any).stringContaining('Failed to release MaxMind update lock'),
600
- );
601
- }, 1000);
602
- });
603
- });