@kibibit/configit 1.0.0-beta.26 → 1.0.0-beta.27

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 (98) hide show
  1. package/README.md +419 -0
  2. package/lib/scripts/test-vault-comprehensive.d.ts +2 -0
  3. package/lib/scripts/test-vault-comprehensive.d.ts.map +1 -0
  4. package/lib/scripts/test-vault-comprehensive.js +422 -0
  5. package/lib/scripts/test-vault-comprehensive.js.map +1 -0
  6. package/lib/scripts/test-vault-dynamic.d.ts +2 -0
  7. package/lib/scripts/test-vault-dynamic.d.ts.map +1 -0
  8. package/lib/scripts/test-vault-dynamic.js +193 -0
  9. package/lib/scripts/test-vault-dynamic.js.map +1 -0
  10. package/lib/scripts/test-vault-gcp-ttl.d.ts +3 -0
  11. package/lib/scripts/test-vault-gcp-ttl.d.ts.map +1 -0
  12. package/lib/scripts/test-vault-gcp-ttl.js +218 -0
  13. package/lib/scripts/test-vault-gcp-ttl.js.map +1 -0
  14. package/lib/scripts/test-vault.d.ts +2 -0
  15. package/lib/scripts/test-vault.d.ts.map +1 -0
  16. package/lib/scripts/test-vault.js +167 -0
  17. package/lib/scripts/test-vault.js.map +1 -0
  18. package/lib/src/config.errors.d.ts.map +1 -0
  19. package/lib/src/config.errors.js.map +1 -0
  20. package/lib/src/config.model.d.ts.map +1 -0
  21. package/lib/src/config.model.js.map +1 -0
  22. package/lib/{config.service.d.ts → src/config.service.d.ts} +10 -1
  23. package/lib/src/config.service.d.ts.map +1 -0
  24. package/lib/{config.service.js → src/config.service.js} +75 -9
  25. package/lib/src/config.service.js.map +1 -0
  26. package/lib/src/environment.service.d.ts.map +1 -0
  27. package/lib/src/environment.service.js.map +1 -0
  28. package/lib/{index.d.ts → src/index.d.ts} +1 -0
  29. package/lib/src/index.d.ts.map +1 -0
  30. package/lib/{index.js → src/index.js} +1 -0
  31. package/lib/src/index.js.map +1 -0
  32. package/lib/src/json-schema.validator.d.ts.map +1 -0
  33. package/lib/src/json-schema.validator.js.map +1 -0
  34. package/lib/src/vault/__tests__/vault-integration.test.d.ts +2 -0
  35. package/lib/src/vault/__tests__/vault-integration.test.d.ts.map +1 -0
  36. package/lib/src/vault/__tests__/vault-integration.test.js +190 -0
  37. package/lib/src/vault/__tests__/vault-integration.test.js.map +1 -0
  38. package/lib/src/vault/decorators.d.ts +17 -0
  39. package/lib/src/vault/decorators.d.ts.map +1 -0
  40. package/lib/src/vault/decorators.js +149 -0
  41. package/lib/src/vault/decorators.js.map +1 -0
  42. package/lib/src/vault/index.d.ts +7 -0
  43. package/lib/src/vault/index.d.ts.map +1 -0
  44. package/lib/src/vault/index.js +42 -0
  45. package/lib/src/vault/index.js.map +1 -0
  46. package/lib/src/vault/secret-refresh-manager.d.ts +23 -0
  47. package/lib/src/vault/secret-refresh-manager.d.ts.map +1 -0
  48. package/lib/src/vault/secret-refresh-manager.js +149 -0
  49. package/lib/src/vault/secret-refresh-manager.js.map +1 -0
  50. package/lib/src/vault/types.d.ts +149 -0
  51. package/lib/src/vault/types.d.ts.map +1 -0
  52. package/lib/src/vault/types.js +4 -0
  53. package/lib/src/vault/types.js.map +1 -0
  54. package/lib/src/vault/vault-cache.d.ts +20 -0
  55. package/lib/src/vault/vault-cache.d.ts.map +1 -0
  56. package/lib/src/vault/vault-cache.js +139 -0
  57. package/lib/src/vault/vault-cache.js.map +1 -0
  58. package/lib/src/vault/vault-integration.d.ts +27 -0
  59. package/lib/src/vault/vault-integration.d.ts.map +1 -0
  60. package/lib/src/vault/vault-integration.js +211 -0
  61. package/lib/src/vault/vault-integration.js.map +1 -0
  62. package/lib/src/vault/vault-provider.d.ts +37 -0
  63. package/lib/src/vault/vault-provider.d.ts.map +1 -0
  64. package/lib/src/vault/vault-provider.js +354 -0
  65. package/lib/src/vault/vault-provider.js.map +1 -0
  66. package/lib/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +5 -65
  68. package/src/config.service.ts +155 -10
  69. package/src/config.service.vault.spec.ts +859 -0
  70. package/src/index.ts +1 -0
  71. package/src/vault/__tests__/vault-integration.test.ts +226 -0
  72. package/src/vault/decorators.ts +228 -0
  73. package/src/vault/index.ts +31 -0
  74. package/src/vault/secret-refresh-manager.ts +241 -0
  75. package/src/vault/types.ts +487 -0
  76. package/src/vault/vault-cache.ts +240 -0
  77. package/src/vault/vault-integration.ts +332 -0
  78. package/src/vault/vault-provider.ts +576 -0
  79. package/lib/config.errors.d.ts.map +0 -1
  80. package/lib/config.errors.js.map +0 -1
  81. package/lib/config.model.d.ts.map +0 -1
  82. package/lib/config.model.js.map +0 -1
  83. package/lib/config.service.d.ts.map +0 -1
  84. package/lib/config.service.js.map +0 -1
  85. package/lib/environment.service.d.ts.map +0 -1
  86. package/lib/environment.service.js.map +0 -1
  87. package/lib/index.d.ts.map +0 -1
  88. package/lib/index.js.map +0 -1
  89. package/lib/json-schema.validator.d.ts.map +0 -1
  90. package/lib/json-schema.validator.js.map +0 -1
  91. /package/lib/{config.errors.d.ts → src/config.errors.d.ts} +0 -0
  92. /package/lib/{config.errors.js → src/config.errors.js} +0 -0
  93. /package/lib/{config.model.d.ts → src/config.model.d.ts} +0 -0
  94. /package/lib/{config.model.js → src/config.model.js} +0 -0
  95. /package/lib/{environment.service.d.ts → src/environment.service.d.ts} +0 -0
  96. /package/lib/{environment.service.js → src/environment.service.js} +0 -0
  97. /package/lib/{json-schema.validator.d.ts → src/json-schema.validator.d.ts} +0 -0
  98. /package/lib/{json-schema.validator.js → src/json-schema.validator.js} +0 -0
@@ -0,0 +1,859 @@
1
+ /**
2
+ * ConfigService + Vault Integration Unit Tests
3
+ * Tests ConfigService integration with Vault secrets management
4
+ */
5
+
6
+ import { IsOptional, IsString } from 'class-validator';
7
+ import nconf from 'nconf';
8
+
9
+ import { VaultKey, VaultOptional, VaultPath } from './vault/decorators';
10
+ import { VaultIntegration } from './vault/vault-integration';
11
+ import { BaseConfig } from './config.model';
12
+ import { ConfigService } from './config.service';
13
+ import { IVaultConfigOptions, IVaultFallbackConfig, VaultHealth } from './vault';
14
+
15
+ import 'reflect-metadata';
16
+
17
+ // Mock VaultIntegration
18
+ jest.mock('./vault/vault-integration');
19
+
20
+ /**
21
+ * Test config class with Vault secrets
22
+ */
23
+ class TestVaultConfig extends BaseConfig {
24
+ @VaultPath('test/api')
25
+ @VaultKey('api_key')
26
+ @IsOptional()
27
+ @IsString()
28
+ API_KEY?: string; // Optional initially, will be loaded from Vault
29
+
30
+ @VaultPath('test/database')
31
+ @VaultKey('password')
32
+ @IsOptional()
33
+ @IsString()
34
+ DB_PASSWORD?: string; // Optional initially, will be loaded from Vault
35
+
36
+ @VaultPath('test/optional')
37
+ @VaultKey('optional_secret')
38
+ @VaultOptional()
39
+ @IsOptional()
40
+ @IsString()
41
+ OPTIONAL_SECRET?: string;
42
+
43
+ @IsString()
44
+ REGULAR_CONFIG!: string;
45
+ }
46
+
47
+ /**
48
+ * Test config class without Vault secrets
49
+ */
50
+ class TestRegularConfig extends BaseConfig {
51
+ @IsString()
52
+ REGULAR_CONFIG!: string;
53
+ }
54
+
55
+ describe('ConfigService + Vault Integration', () => {
56
+ let mockVaultIntegration: jest.Mocked<VaultIntegration>;
57
+ let originalNconfGet: typeof nconf.get;
58
+ let originalNconfOverrides: typeof nconf.overrides;
59
+
60
+ beforeEach(() => {
61
+ jest.clearAllMocks();
62
+
63
+ // Mock VaultIntegration constructor - create a fresh mock instance each time
64
+ mockVaultIntegration = {
65
+ initialize: jest.fn().mockResolvedValue(undefined),
66
+ loadSecrets: jest.fn().mockResolvedValue(undefined),
67
+ getHealth: jest.fn().mockReturnValue({
68
+ connected: true,
69
+ authenticated: true,
70
+ cacheSize: 0,
71
+ refreshQueueSize: 0,
72
+ lastRefreshTime: 0,
73
+ errors: []
74
+ } as VaultHealth),
75
+ invalidateCache: jest.fn(),
76
+ invalidateProperty: jest.fn(),
77
+ shutdown: jest.fn(),
78
+ isInitialized: jest.fn().mockReturnValue(true),
79
+ getSecret: jest.fn().mockReturnValue(null)
80
+ } as unknown as jest.Mocked<VaultIntegration>;
81
+
82
+ // Ensure the mock implementation returns our mock instance
83
+ (VaultIntegration as jest.MockedClass<typeof VaultIntegration>).mockImplementation(() => {
84
+ return mockVaultIntegration;
85
+ });
86
+
87
+ // Reset the mock implementation to ensure it's fresh
88
+ (VaultIntegration as jest.MockedClass<typeof VaultIntegration>).mockClear();
89
+
90
+ // Save original nconf methods
91
+ originalNconfGet = nconf.get;
92
+ originalNconfOverrides = nconf.overrides;
93
+
94
+ // Mock nconf.get to return test config
95
+ (nconf.get as jest.Mock) = jest.fn().mockReturnValue({
96
+ NODE_ENV: 'test',
97
+ REGULAR_CONFIG: 'from-env'
98
+ });
99
+
100
+ // Mock nconf.overrides to track overrides
101
+ const overridesStore: Record<string, any> = {};
102
+ (nconf.overrides as jest.Mock) = jest.fn((values?: Record<string, any>) => {
103
+ if (values) {
104
+ Object.assign(overridesStore, values);
105
+ }
106
+ return {
107
+ store: overridesStore,
108
+ get: (key: string) => overridesStore[key]
109
+ };
110
+ });
111
+ });
112
+
113
+ afterEach(() => {
114
+ // Restore original nconf methods
115
+ nconf.get = originalNconfGet;
116
+ nconf.overrides = originalNconfOverrides;
117
+ });
118
+
119
+ describe('Backward Compatibility Tests', () => {
120
+ it('should work without vault option (existing behavior)', () => {
121
+ const configService = new ConfigService(TestRegularConfig, {
122
+ NODE_ENV: 'test',
123
+ REGULAR_CONFIG: 'test-value'
124
+ } as any);
125
+
126
+ expect(configService).toBeDefined();
127
+ expect(configService.config).toBeDefined();
128
+ expect((configService.config as any)?.REGULAR_CONFIG).toBe('test-value');
129
+ expect(VaultIntegration).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('should not error when initializeVault() called without vault config', async () => {
133
+ const configService = new ConfigService(TestRegularConfig, {
134
+ NODE_ENV: 'test',
135
+ REGULAR_CONFIG: 'test-value'
136
+ } as any);
137
+
138
+ await expect(configService.initializeVault()).resolves.not.toThrow();
139
+ expect(mockVaultIntegration.initialize).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it('should return null health when vault not configured', () => {
143
+ const configService = new ConfigService(TestRegularConfig, {
144
+ NODE_ENV: 'test',
145
+ REGULAR_CONFIG: 'test-value'
146
+ } as any);
147
+
148
+ expect(configService.getVaultHealth()).toBeNull();
149
+ });
150
+ });
151
+
152
+ describe('Source Hierarchy Tests', () => {
153
+ it('should override environment variables with Vault secrets', async () => {
154
+ // Set environment variable
155
+ process.env.API_KEY = 'env-api-key';
156
+
157
+ const vaultConfig: IVaultConfigOptions = {
158
+ endpoint: 'http://localhost:8200',
159
+ auth: {
160
+ methods: [
161
+ {
162
+ type: 'token',
163
+ config: {
164
+ type: 'token',
165
+ token: 'test-token'
166
+ }
167
+ }
168
+ ]
169
+ }
170
+ };
171
+
172
+ const configService = new ConfigService(TestVaultConfig, {
173
+ NODE_ENV: 'test',
174
+ API_KEY: 'env-api-key', // From env
175
+ REGULAR_CONFIG: 'regular'
176
+ } as any, {
177
+ vault: vaultConfig
178
+ });
179
+
180
+ // Mock vault secret loading - secrets injected into nconf overrides
181
+ mockVaultIntegration.loadSecrets.mockImplementation(async () => {
182
+ // Simulate vault secrets being injected into nconf overrides
183
+ nconf.overrides({
184
+ API_KEY: 'vault-api-key', // Vault secret overrides env
185
+ DB_PASSWORD: 'vault-password'
186
+ });
187
+ });
188
+
189
+ await configService.initializeVault();
190
+
191
+ // After initializeVault, config should be re-validated with vault secrets
192
+ // The vault secret should override the env variable
193
+ expect(mockVaultIntegration.loadSecrets).toHaveBeenCalled();
194
+
195
+ // Verify nconf.overrides was called with vault secrets
196
+ expect(nconf.overrides).toHaveBeenCalledWith(
197
+ expect.objectContaining({
198
+ API_KEY: 'vault-api-key'
199
+ })
200
+ );
201
+ });
202
+
203
+ it('should override config file values with Vault secrets', async () => {
204
+ // Mock nconf.get to return file config
205
+ (nconf.get as jest.Mock).mockReturnValue({
206
+ NODE_ENV: 'test',
207
+ API_KEY: 'file-api-key', // From config file
208
+ REGULAR_CONFIG: 'regular'
209
+ });
210
+
211
+ const vaultConfig: IVaultConfigOptions = {
212
+ endpoint: 'http://localhost:8200',
213
+ auth: {
214
+ methods: [
215
+ {
216
+ type: 'token',
217
+ config: {
218
+ type: 'token',
219
+ token: 'test-token'
220
+ }
221
+ }
222
+ ]
223
+ }
224
+ };
225
+
226
+ const configService = new ConfigService(TestVaultConfig, undefined, {
227
+ vault: vaultConfig
228
+ });
229
+
230
+ // Mock vault secret loading
231
+ mockVaultIntegration.loadSecrets.mockImplementation(async () => {
232
+ nconf.overrides({
233
+ API_KEY: 'vault-api-key', // Vault secret overrides file
234
+ DB_PASSWORD: 'vault-password'
235
+ });
236
+ });
237
+
238
+ await configService.initializeVault();
239
+
240
+ // Verify vault secrets override file values
241
+ expect(nconf.overrides).toHaveBeenCalledWith(
242
+ expect.objectContaining({
243
+ API_KEY: 'vault-api-key'
244
+ })
245
+ );
246
+ });
247
+
248
+ it('should allow CLI args to override Vault secrets (if applicable)', async () => {
249
+ // Note: CLI args have highest priority in nconf hierarchy
250
+ // This test verifies the hierarchy is maintained
251
+ const vaultConfig: IVaultConfigOptions = {
252
+ endpoint: 'http://localhost:8200',
253
+ auth: {
254
+ methods: [
255
+ {
256
+ type: 'token',
257
+ config: {
258
+ type: 'token',
259
+ token: 'test-token'
260
+ }
261
+ }
262
+ ]
263
+ }
264
+ };
265
+
266
+ // Mock nconf.get to simulate CLI args already set
267
+ (nconf.get as jest.Mock).mockReturnValue({
268
+ NODE_ENV: 'test',
269
+ API_KEY: 'cli-api-key', // From CLI (highest priority)
270
+ REGULAR_CONFIG: 'regular'
271
+ });
272
+
273
+ const configService = new ConfigService(TestVaultConfig, undefined, {
274
+ vault: vaultConfig
275
+ });
276
+
277
+ mockVaultIntegration.loadSecrets.mockImplementation(async () => {
278
+ // Vault secrets are injected, but CLI args should still win
279
+ nconf.overrides({
280
+ API_KEY: 'vault-api-key',
281
+ DB_PASSWORD: 'vault-password'
282
+ });
283
+ });
284
+
285
+ await configService.initializeVault();
286
+
287
+ // In nconf hierarchy: argv > vault overrides > env > file
288
+ // So CLI args should still be accessible via nconf.get()
289
+ const finalConfig = nconf.get();
290
+ expect(finalConfig.API_KEY).toBe('cli-api-key');
291
+ });
292
+ });
293
+
294
+ describe('Error Handling Tests', () => {
295
+ it('should throw error when Vault unavailable with fallback.required=true', async () => {
296
+ const vaultConfig: IVaultConfigOptions = {
297
+ endpoint: 'http://localhost:8200',
298
+ auth: {
299
+ methods: [
300
+ {
301
+ type: 'token',
302
+ config: {
303
+ type: 'token',
304
+ token: 'test-token'
305
+ }
306
+ }
307
+ ]
308
+ },
309
+ fallback: {
310
+ required: true, // Vault is required
311
+ useCacheOnFailure: false,
312
+ maxCacheAge: 3600000,
313
+ failFast: true
314
+ } as IVaultFallbackConfig
315
+ };
316
+
317
+ const configService = new ConfigService(TestVaultConfig, {
318
+ NODE_ENV: 'test',
319
+ REGULAR_CONFIG: 'regular'
320
+ } as any, {
321
+ vault: vaultConfig
322
+ });
323
+
324
+ // Mock initialization failure
325
+ mockVaultIntegration.initialize.mockRejectedValue(
326
+ new Error('Vault connection failed')
327
+ );
328
+
329
+ await expect(configService.initializeVault()).rejects.toThrow('Vault connection failed');
330
+ });
331
+
332
+ it('should log warning and continue when Vault unavailable with fallback.required=false', async () => {
333
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
334
+
335
+ // Create a fresh mock instance for this test that will throw
336
+ // MUST be created BEFORE ConfigService constructor (which creates VaultIntegration)
337
+ const failingMockVaultIntegration = {
338
+ initialize: jest.fn().mockRejectedValue(new Error('Vault connection failed')),
339
+ loadSecrets: jest.fn().mockResolvedValue(undefined),
340
+ getHealth: jest.fn().mockReturnValue({
341
+ connected: false,
342
+ authenticated: false,
343
+ cacheSize: 0,
344
+ refreshQueueSize: 0,
345
+ lastRefreshTime: 0,
346
+ errors: []
347
+ } as VaultHealth),
348
+ invalidateCache: jest.fn(),
349
+ invalidateProperty: jest.fn(),
350
+ shutdown: jest.fn(),
351
+ isInitialized: jest.fn().mockReturnValue(false),
352
+ getSecret: jest.fn().mockReturnValue(null)
353
+ } as unknown as jest.Mocked<VaultIntegration>;
354
+
355
+ // Override the mock implementation BEFORE creating ConfigService
356
+ (VaultIntegration as jest.MockedClass<typeof VaultIntegration>).mockImplementation(() => {
357
+ return failingMockVaultIntegration;
358
+ });
359
+
360
+ const vaultConfig: IVaultConfigOptions = {
361
+ endpoint: 'http://localhost:8200',
362
+ auth: {
363
+ methods: [
364
+ {
365
+ type: 'token',
366
+ config: {
367
+ type: 'token',
368
+ token: 'test-token'
369
+ }
370
+ }
371
+ ]
372
+ },
373
+ fallback: {
374
+ required: false, // Vault is optional - should log warning and continue
375
+ useCacheOnFailure: true,
376
+ maxCacheAge: 3600000,
377
+ failFast: false
378
+ } as IVaultFallbackConfig
379
+ };
380
+
381
+ const configService = new ConfigService(TestVaultConfig, {
382
+ NODE_ENV: 'test',
383
+ REGULAR_CONFIG: 'regular'
384
+ } as any, {
385
+ vault: vaultConfig
386
+ });
387
+
388
+ // With fallback.required=false, should log warning and continue (not throw)
389
+ await expect(configService.initializeVault()).resolves.not.toThrow();
390
+
391
+ // Verify initialize was called
392
+ expect(failingMockVaultIntegration.initialize).toHaveBeenCalled();
393
+
394
+ // Verify warning was logged
395
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
396
+ expect.stringContaining('Vault initialization failed')
397
+ );
398
+
399
+ consoleWarnSpy.mockRestore();
400
+ });
401
+
402
+ it('should throw error when required secret is missing', async () => {
403
+ const vaultConfig: IVaultConfigOptions = {
404
+ endpoint: 'http://localhost:8200',
405
+ auth: {
406
+ methods: [
407
+ {
408
+ type: 'token',
409
+ config: {
410
+ type: 'token',
411
+ token: 'test-token'
412
+ }
413
+ }
414
+ ]
415
+ },
416
+ fallback: {
417
+ required: true,
418
+ useCacheOnFailure: false,
419
+ maxCacheAge: 3600000,
420
+ failFast: true
421
+ } as IVaultFallbackConfig
422
+ };
423
+
424
+ const configService = new ConfigService(TestVaultConfig, {
425
+ NODE_ENV: 'test',
426
+ REGULAR_CONFIG: 'regular'
427
+ } as any, {
428
+ vault: vaultConfig
429
+ });
430
+
431
+ // Mock loadSecrets to throw error for missing required secret
432
+ mockVaultIntegration.loadSecrets.mockRejectedValue(
433
+ new Error('Failed to load required secret from test/api: Secret not found')
434
+ );
435
+
436
+ await expect(configService.initializeVault()).rejects.toThrow(
437
+ /Failed to load required secret/
438
+ );
439
+ });
440
+
441
+ it('should continue when optional secret is missing (@VaultOptional)', async () => {
442
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
443
+
444
+ const vaultConfig: IVaultConfigOptions = {
445
+ endpoint: 'http://localhost:8200',
446
+ auth: {
447
+ methods: [
448
+ {
449
+ type: 'token',
450
+ config: {
451
+ type: 'token',
452
+ token: 'test-token'
453
+ }
454
+ }
455
+ ]
456
+ },
457
+ fallback: {
458
+ required: false, // Optional fallback
459
+ useCacheOnFailure: true,
460
+ maxCacheAge: 3600000,
461
+ failFast: false
462
+ } as IVaultFallbackConfig
463
+ };
464
+
465
+ const configService = new ConfigService(TestVaultConfig, {
466
+ NODE_ENV: 'test',
467
+ REGULAR_CONFIG: 'regular'
468
+ } as any, {
469
+ vault: vaultConfig
470
+ });
471
+
472
+ // Mock loadSecrets to simulate missing optional secret
473
+ // The VaultIntegration should log warning and continue
474
+ mockVaultIntegration.loadSecrets.mockImplementation(async () => {
475
+ // Simulate warning for optional secret
476
+ console.warn('Failed to load optional secret from test/optional: Secret not found');
477
+ // Continue without throwing
478
+ });
479
+
480
+ await expect(configService.initializeVault()).resolves.not.toThrow();
481
+
482
+ consoleWarnSpy.mockRestore();
483
+ });
484
+ });
485
+
486
+ describe('Initialization Flow Tests', () => {
487
+ it('should warn/throw when config accessed before initializeVault() when Vault configured', () => {
488
+ const vaultConfig: IVaultConfigOptions = {
489
+ endpoint: 'http://localhost:8200',
490
+ auth: {
491
+ methods: [
492
+ {
493
+ type: 'token',
494
+ config: {
495
+ type: 'token',
496
+ token: 'test-token'
497
+ }
498
+ }
499
+ ]
500
+ }
501
+ };
502
+
503
+ const configService = new ConfigService(TestVaultConfig, {
504
+ NODE_ENV: 'test',
505
+ REGULAR_CONFIG: 'regular'
506
+ // API_KEY and DB_PASSWORD are missing - should come from Vault
507
+ } as any, {
508
+ vault: vaultConfig
509
+ });
510
+
511
+ // Config should be accessible, but vault secrets won't be loaded yet
512
+ expect(configService.config).toBeDefined();
513
+ // Vault secrets should not be available until initializeVault() is called
514
+ expect(mockVaultIntegration.initialize).not.toHaveBeenCalled();
515
+ expect(mockVaultIntegration.loadSecrets).not.toHaveBeenCalled();
516
+ });
517
+
518
+ it('should make config accessible after initializeVault()', async () => {
519
+ const vaultConfig: IVaultConfigOptions = {
520
+ endpoint: 'http://localhost:8200',
521
+ auth: {
522
+ methods: [
523
+ {
524
+ type: 'token',
525
+ config: {
526
+ type: 'token',
527
+ token: 'test-token'
528
+ }
529
+ }
530
+ ]
531
+ }
532
+ };
533
+
534
+ const configService = new ConfigService(TestVaultConfig, {
535
+ NODE_ENV: 'test',
536
+ REGULAR_CONFIG: 'regular'
537
+ } as any, {
538
+ vault: vaultConfig
539
+ });
540
+
541
+ // Mock successful vault initialization and secret loading
542
+ mockVaultIntegration.loadSecrets.mockImplementation(async () => {
543
+ // Simulate vault secrets being injected
544
+ nconf.overrides({
545
+ API_KEY: 'vault-api-key',
546
+ DB_PASSWORD: 'vault-password'
547
+ });
548
+ });
549
+
550
+ await configService.initializeVault();
551
+
552
+ // Verify initialization was called
553
+ expect(mockVaultIntegration.initialize).toHaveBeenCalled();
554
+ expect(mockVaultIntegration.loadSecrets).toHaveBeenCalled();
555
+
556
+ // Config should be accessible and re-validated with vault secrets
557
+ expect(configService.config).toBeDefined();
558
+ });
559
+
560
+ it('should re-validate config after vault secrets are loaded', async () => {
561
+ const vaultConfig: IVaultConfigOptions = {
562
+ endpoint: 'http://localhost:8200',
563
+ auth: {
564
+ methods: [
565
+ {
566
+ type: 'token',
567
+ config: {
568
+ type: 'token',
569
+ token: 'test-token'
570
+ }
571
+ }
572
+ ]
573
+ }
574
+ };
575
+
576
+ // Track nconf.get calls
577
+ const nconfGetSpy = jest.spyOn(nconf, 'get');
578
+
579
+ const configService = new ConfigService(TestVaultConfig, {
580
+ NODE_ENV: 'test',
581
+ REGULAR_CONFIG: 'regular'
582
+ } as any, {
583
+ vault: vaultConfig
584
+ });
585
+
586
+ const initialConfigCallCount = nconfGetSpy.mock.calls.length;
587
+
588
+ // Mock loadSecrets to simulate vault secrets being injected
589
+ mockVaultIntegration.loadSecrets.mockImplementation(async () => {
590
+ // Inject vault secrets into nconf overrides
591
+ nconf.overrides({
592
+ API_KEY: 'vault-api-key',
593
+ DB_PASSWORD: 'vault-password'
594
+ });
595
+ });
596
+
597
+ await configService.initializeVault();
598
+
599
+ // Verify that nconf.get was called again in initializeVault() for re-validation
600
+ // The config should be re-validated after vault secrets are loaded
601
+ expect(nconfGetSpy).toHaveBeenCalledTimes(initialConfigCallCount + 1);
602
+ expect(configService.config).toBeDefined();
603
+
604
+ nconfGetSpy.mockRestore();
605
+ });
606
+
607
+ it('should handle initializeVault() being called multiple times', async () => {
608
+ const vaultConfig: IVaultConfigOptions = {
609
+ endpoint: 'http://localhost:8200',
610
+ auth: {
611
+ methods: [
612
+ {
613
+ type: 'token',
614
+ config: {
615
+ type: 'token',
616
+ token: 'test-token'
617
+ }
618
+ }
619
+ ]
620
+ }
621
+ };
622
+
623
+ const configService = new ConfigService(TestVaultConfig, {
624
+ NODE_ENV: 'test',
625
+ REGULAR_CONFIG: 'regular'
626
+ } as any, {
627
+ vault: vaultConfig
628
+ });
629
+
630
+ await configService.initializeVault();
631
+ await configService.initializeVault();
632
+
633
+ // Should only initialize once (VaultIntegration handles idempotency)
634
+ expect(mockVaultIntegration.initialize).toHaveBeenCalledTimes(2);
635
+ expect(mockVaultIntegration.loadSecrets).toHaveBeenCalledTimes(2);
636
+ });
637
+ });
638
+
639
+ describe('Vault Health and Cache Management', () => {
640
+ it('should return vault health status', async () => {
641
+ const vaultConfig: IVaultConfigOptions = {
642
+ endpoint: 'http://localhost:8200',
643
+ auth: {
644
+ methods: [
645
+ {
646
+ type: 'token',
647
+ config: {
648
+ type: 'token',
649
+ token: 'test-token'
650
+ }
651
+ }
652
+ ]
653
+ }
654
+ };
655
+
656
+ const configService = new ConfigService(TestVaultConfig, {
657
+ NODE_ENV: 'test',
658
+ REGULAR_CONFIG: 'regular'
659
+ } as any, {
660
+ vault: vaultConfig
661
+ });
662
+
663
+ await configService.initializeVault();
664
+
665
+ const health = configService.getVaultHealth();
666
+ expect(health).not.toBeNull();
667
+ expect(health?.connected).toBe(true);
668
+ expect(health?.authenticated).toBe(true);
669
+ });
670
+
671
+ it('should invalidate vault cache for path', async () => {
672
+ const vaultConfig: IVaultConfigOptions = {
673
+ endpoint: 'http://localhost:8200',
674
+ auth: {
675
+ methods: [
676
+ {
677
+ type: 'token',
678
+ config: {
679
+ type: 'token',
680
+ token: 'test-token'
681
+ }
682
+ }
683
+ ]
684
+ }
685
+ };
686
+
687
+ const configService = new ConfigService(TestVaultConfig, {
688
+ NODE_ENV: 'test',
689
+ REGULAR_CONFIG: 'regular'
690
+ } as any, {
691
+ vault: vaultConfig
692
+ });
693
+
694
+ await configService.initializeVault();
695
+
696
+ configService.invalidateVaultCache('test/api');
697
+
698
+ expect(mockVaultIntegration.invalidateCache).toHaveBeenCalledWith('test/api');
699
+ });
700
+
701
+ it('should invalidate vault cache for property', async () => {
702
+ const vaultConfig: IVaultConfigOptions = {
703
+ endpoint: 'http://localhost:8200',
704
+ auth: {
705
+ methods: [
706
+ {
707
+ type: 'token',
708
+ config: {
709
+ type: 'token',
710
+ token: 'test-token'
711
+ }
712
+ }
713
+ ]
714
+ }
715
+ };
716
+
717
+ const configService = new ConfigService(TestVaultConfig, {
718
+ NODE_ENV: 'test',
719
+ REGULAR_CONFIG: 'regular'
720
+ } as any, {
721
+ vault: vaultConfig
722
+ });
723
+
724
+ await configService.initializeVault();
725
+
726
+ configService.invalidateVaultProperty('API_KEY');
727
+
728
+ expect(mockVaultIntegration.invalidateProperty).toHaveBeenCalledWith('API_KEY');
729
+ });
730
+
731
+ it('should shutdown vault integration gracefully', async () => {
732
+ const vaultConfig: IVaultConfigOptions = {
733
+ endpoint: 'http://localhost:8200',
734
+ auth: {
735
+ methods: [
736
+ {
737
+ type: 'token',
738
+ config: {
739
+ type: 'token',
740
+ token: 'test-token'
741
+ }
742
+ }
743
+ ]
744
+ }
745
+ };
746
+
747
+ const configService = new ConfigService(TestVaultConfig, {
748
+ NODE_ENV: 'test',
749
+ REGULAR_CONFIG: 'regular'
750
+ } as any, {
751
+ vault: vaultConfig
752
+ });
753
+
754
+ await configService.initializeVault();
755
+
756
+ configService.shutdownVault();
757
+
758
+ expect(mockVaultIntegration.shutdown).toHaveBeenCalled();
759
+ });
760
+ });
761
+
762
+ describe('Edge Cases', () => {
763
+ it('should handle config service not properly initialized', async () => {
764
+ const vaultConfig: IVaultConfigOptions = {
765
+ endpoint: 'http://localhost:8200',
766
+ auth: {
767
+ methods: [
768
+ {
769
+ type: 'token',
770
+ config: {
771
+ type: 'token',
772
+ token: 'test-token'
773
+ }
774
+ }
775
+ ]
776
+ }
777
+ };
778
+
779
+ // Create config service with vault config
780
+ const configService = new ConfigService(TestVaultConfig, {
781
+ NODE_ENV: 'test',
782
+ REGULAR_CONFIG: 'regular'
783
+ } as any, {
784
+ vault: vaultConfig
785
+ });
786
+
787
+ // Simulate genericClass being null (should not happen in practice)
788
+ // This tests the error handling in initializeVault
789
+ (configService as any).genericClass = null;
790
+
791
+ await expect(configService.initializeVault()).rejects.toThrow(
792
+ 'ConfigService not properly initialized'
793
+ );
794
+ });
795
+
796
+ it('should handle vault integration initialization failure gracefully', async () => {
797
+ const vaultConfig: IVaultConfigOptions = {
798
+ endpoint: 'http://localhost:8200',
799
+ auth: {
800
+ methods: [
801
+ {
802
+ type: 'token',
803
+ config: {
804
+ type: 'token',
805
+ token: 'test-token'
806
+ }
807
+ }
808
+ ]
809
+ }
810
+ };
811
+
812
+ const configService = new ConfigService(TestVaultConfig, {
813
+ NODE_ENV: 'test',
814
+ REGULAR_CONFIG: 'regular'
815
+ } as any, {
816
+ vault: vaultConfig
817
+ });
818
+
819
+ // Mock initialization failure
820
+ mockVaultIntegration.initialize.mockRejectedValue(
821
+ new Error('Network error: ECONNREFUSED')
822
+ );
823
+
824
+ await expect(configService.initializeVault()).rejects.toThrow('Network error');
825
+ });
826
+
827
+ it('should handle loadSecrets failure after successful initialization', async () => {
828
+ const vaultConfig: IVaultConfigOptions = {
829
+ endpoint: 'http://localhost:8200',
830
+ auth: {
831
+ methods: [
832
+ {
833
+ type: 'token',
834
+ config: {
835
+ type: 'token',
836
+ token: 'test-token'
837
+ }
838
+ }
839
+ ]
840
+ }
841
+ };
842
+
843
+ const configService = new ConfigService(TestVaultConfig, {
844
+ NODE_ENV: 'test',
845
+ REGULAR_CONFIG: 'regular'
846
+ } as any, {
847
+ vault: vaultConfig
848
+ });
849
+
850
+ // Mock successful initialization but failed loadSecrets
851
+ mockVaultIntegration.initialize.mockResolvedValue(undefined);
852
+ mockVaultIntegration.loadSecrets.mockRejectedValue(
853
+ new Error('Failed to load secrets: Permission denied')
854
+ );
855
+
856
+ await expect(configService.initializeVault()).rejects.toThrow('Failed to load secrets');
857
+ });
858
+ });
859
+ });