@kibibit/configit 1.0.0-beta.25 → 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.
- package/README.md +419 -0
- package/lib/scripts/test-vault-comprehensive.d.ts +2 -0
- package/lib/scripts/test-vault-comprehensive.d.ts.map +1 -0
- package/lib/scripts/test-vault-comprehensive.js +422 -0
- package/lib/scripts/test-vault-comprehensive.js.map +1 -0
- package/lib/scripts/test-vault-dynamic.d.ts +2 -0
- package/lib/scripts/test-vault-dynamic.d.ts.map +1 -0
- package/lib/scripts/test-vault-dynamic.js +193 -0
- package/lib/scripts/test-vault-dynamic.js.map +1 -0
- package/lib/scripts/test-vault-gcp-ttl.d.ts +3 -0
- package/lib/scripts/test-vault-gcp-ttl.d.ts.map +1 -0
- package/lib/scripts/test-vault-gcp-ttl.js +218 -0
- package/lib/scripts/test-vault-gcp-ttl.js.map +1 -0
- package/lib/scripts/test-vault.d.ts +2 -0
- package/lib/scripts/test-vault.d.ts.map +1 -0
- package/lib/scripts/test-vault.js +167 -0
- package/lib/scripts/test-vault.js.map +1 -0
- package/lib/src/config.errors.d.ts.map +1 -0
- package/lib/src/config.errors.js.map +1 -0
- package/lib/src/config.model.d.ts.map +1 -0
- package/lib/src/config.model.js.map +1 -0
- package/lib/{config.service.d.ts → src/config.service.d.ts} +10 -1
- package/lib/src/config.service.d.ts.map +1 -0
- package/lib/{config.service.js → src/config.service.js} +75 -9
- package/lib/src/config.service.js.map +1 -0
- package/lib/src/environment.service.d.ts.map +1 -0
- package/lib/src/environment.service.js.map +1 -0
- package/lib/{index.d.ts → src/index.d.ts} +1 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/{index.js → src/index.js} +1 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/json-schema.validator.d.ts.map +1 -0
- package/lib/src/json-schema.validator.js.map +1 -0
- package/lib/src/vault/__tests__/vault-integration.test.d.ts +2 -0
- package/lib/src/vault/__tests__/vault-integration.test.d.ts.map +1 -0
- package/lib/src/vault/__tests__/vault-integration.test.js +190 -0
- package/lib/src/vault/__tests__/vault-integration.test.js.map +1 -0
- package/lib/src/vault/decorators.d.ts +17 -0
- package/lib/src/vault/decorators.d.ts.map +1 -0
- package/lib/src/vault/decorators.js +149 -0
- package/lib/src/vault/decorators.js.map +1 -0
- package/lib/src/vault/index.d.ts +7 -0
- package/lib/src/vault/index.d.ts.map +1 -0
- package/lib/src/vault/index.js +42 -0
- package/lib/src/vault/index.js.map +1 -0
- package/lib/src/vault/secret-refresh-manager.d.ts +23 -0
- package/lib/src/vault/secret-refresh-manager.d.ts.map +1 -0
- package/lib/src/vault/secret-refresh-manager.js +149 -0
- package/lib/src/vault/secret-refresh-manager.js.map +1 -0
- package/lib/src/vault/types.d.ts +149 -0
- package/lib/src/vault/types.d.ts.map +1 -0
- package/lib/src/vault/types.js +4 -0
- package/lib/src/vault/types.js.map +1 -0
- package/lib/src/vault/vault-cache.d.ts +20 -0
- package/lib/src/vault/vault-cache.d.ts.map +1 -0
- package/lib/src/vault/vault-cache.js +139 -0
- package/lib/src/vault/vault-cache.js.map +1 -0
- package/lib/src/vault/vault-integration.d.ts +27 -0
- package/lib/src/vault/vault-integration.d.ts.map +1 -0
- package/lib/src/vault/vault-integration.js +211 -0
- package/lib/src/vault/vault-integration.js.map +1 -0
- package/lib/src/vault/vault-provider.d.ts +37 -0
- package/lib/src/vault/vault-provider.d.ts.map +1 -0
- package/lib/src/vault/vault-provider.js +354 -0
- package/lib/src/vault/vault-provider.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -74
- package/src/config.service.ts +155 -10
- package/src/config.service.vault.spec.ts +859 -0
- package/src/index.ts +1 -0
- package/src/vault/__tests__/vault-integration.test.ts +226 -0
- package/src/vault/decorators.ts +228 -0
- package/src/vault/index.ts +31 -0
- package/src/vault/secret-refresh-manager.ts +241 -0
- package/src/vault/types.ts +487 -0
- package/src/vault/vault-cache.ts +240 -0
- package/src/vault/vault-integration.ts +332 -0
- package/src/vault/vault-provider.ts +576 -0
- package/lib/config.errors.d.ts.map +0 -1
- package/lib/config.errors.js.map +0 -1
- package/lib/config.model.d.ts.map +0 -1
- package/lib/config.model.js.map +0 -1
- package/lib/config.service.d.ts.map +0 -1
- package/lib/config.service.js.map +0 -1
- package/lib/environment.service.d.ts.map +0 -1
- package/lib/environment.service.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/json-schema.validator.d.ts.map +0 -1
- package/lib/json-schema.validator.js.map +0 -1
- /package/lib/{config.errors.d.ts → src/config.errors.d.ts} +0 -0
- /package/lib/{config.errors.js → src/config.errors.js} +0 -0
- /package/lib/{config.model.d.ts → src/config.model.d.ts} +0 -0
- /package/lib/{config.model.js → src/config.model.js} +0 -0
- /package/lib/{environment.service.d.ts → src/environment.service.d.ts} +0 -0
- /package/lib/{environment.service.js → src/environment.service.js} +0 -0
- /package/lib/{json-schema.validator.d.ts → src/json-schema.validator.d.ts} +0 -0
- /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
|
+
});
|