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