UNPKG

kubricate

Version:

A TypeScript framework for building reusable, type-safe Kubernetes infrastructure — without the YAML mess.

1,193 lines (1,191 loc) 39.7 kB
"use strict"; var _vitest = /*#__PURE__*/require("vitest"); var _InMemoryConnector = /*#__PURE__*/require("../connectors/InMemoryConnector.js"); var _InMemoryProvider = /*#__PURE__*/require("../providers/InMemoryProvider.js"); var _SecretManager = /*#__PURE__*/require("../SecretManager.js"); var _SecretRegistry = /*#__PURE__*/require("../SecretRegistry.js"); var _SecretsOrchestrator = /*#__PURE__*/require("./SecretsOrchestrator.js"); /* eslint-disable @typescript-eslint/no-explicit-any */ (0, _vitest.describe)('SecretsOrchestrator', () => { let mockSecretManager; let mockProvider; let orchestrator; let mockLogger; let options; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; mockProvider = { name: 'kubernetes', // This is critical secretType: 'Kubricate.InMemory', // Or your desired secretType allowMerge: true, getEffectIdentifier: effect => effect.value?.metadata?.name ?? 'no-id', prepare: _vitest.vi.fn((name, value) => [{ providerName: 'kubernetes', type: 'kubectl', value: { kind: 'Secret', metadata: { name: 'test-secret', provider: 'kubernetes' }, data: { [name]: value } } }]), mergeSecrets: _vitest.vi.fn(effects => effects) }; mockSecretManager = { getSecrets: _vitest.vi.fn(() => ({ DB_PASS: { name: 'DB_PASS', connector: 'env', provider: 'kubernetes' } })), resolveConnector: _vitest.vi.fn(() => ({ getWorkingDir: () => undefined, setWorkingDir: _vitest.vi.fn(), load: _vitest.vi.fn(() => Promise.resolve()), get: _vitest.vi.fn(() => 's3cr3t') })), resolveProvider: _vitest.vi.fn(() => mockProvider) }; options = { config: { stacks: { api: { getSecretManagers: () => ({ main: mockSecretManager }) } }, secret: { conflict: { strategies: { intraProvider: 'autoMerge', crossProvider: 'error', crossManager: 'error' } }, secretSpec: mockSecretManager } }, logger: mockLogger, effectOptions: { workingDir: '/project' } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); }); (0, _vitest.it)('should validate secrets without errors', async () => { await (0, _vitest.expect)(orchestrator.validate()).resolves.not.toThrow(); (0, _vitest.expect)(mockSecretManager.resolveConnector).toHaveBeenCalled(); }); (0, _vitest.it)('should apply and return prepared effects', async () => { const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(effects[0].value.kind).toBe('Secret'); (0, _vitest.expect)(mockProvider.prepare).toHaveBeenCalled(); (0, _vitest.expect)(mockProvider.mergeSecrets).toHaveBeenCalled(); }); (0, _vitest.it)('should cache provider in resolveProviderByName()', () => { const provider = orchestrator['resolveProviderByName']('kubernetes'); (0, _vitest.expect)(provider).toBe(mockProvider); const cached = orchestrator['providerCache'].get('kubernetes'); (0, _vitest.expect)(cached).toBe(mockProvider); }); (0, _vitest.it)('should throw if provider name is not found', () => { (0, _vitest.expect)(() => orchestrator['resolveProviderByName']('not-exist')).toThrowError('[SecretsOrchestrator] Provider "not-exist" not found in any registered SecretManager'); }); }); (0, _vitest.describe)('SecretsOrchestrator Multi-Level Merge Strategy', () => { let mockLogger; let mockProvider; let mockSecretManager; let orchestrator; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; mockProvider = { name: 'kubernetes', // This is critical secretType: 'Kubricate.InMemory', // Or your desired secretType allowMerge: true, getEffectIdentifier: effect => effect.value?.metadata?.name ?? 'no-id', prepare: _vitest.vi.fn((name, value) => [{ providerName: 'kubernetes', type: 'kubectl', value: { kind: 'Secret', metadata: { name: 'merged-secret', provider: 'kubernetes' }, data: { [name]: value } } }]), mergeSecrets: _vitest.vi.fn(effects => effects) }; mockSecretManager = (name, secrets) => ({ getSecrets: _vitest.vi.fn(() => secrets), resolveConnector: _vitest.vi.fn(() => ({ getWorkingDir: () => undefined, setWorkingDir: _vitest.vi.fn(), load: _vitest.vi.fn(() => Promise.resolve()), get: _vitest.vi.fn(key => secrets[key].value) })), resolveProvider: _vitest.vi.fn(() => mockProvider) }); }); (0, _vitest.it)('snapshots multiple secrets from the same manager', async () => { const _mockSecretManager = mockSecretManager('svc', { DB_USER: { connector: 'env', provider: 'kubernetes', value: 'admin' }, DB_PASS: { connector: 'env', provider: 'kubernetes', value: 's3cr3t' } }); const stacks = { stack1: { getSecretManagers: () => ({ svc: _mockSecretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { conflict: { strategies: { intraProvider: 'autoMerge', crossProvider: 'autoMerge', crossManager: 'autoMerge' } }, secretSpec: _mockSecretManager } } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toMatchSnapshot(); }); }); (0, _vitest.describe)('SecretsOrchestrator Advanced Merge Tests', () => { let mockLogger; let mockProvider; let mockSecretManager; let orchestrator; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; mockProvider = { name: 'kubernetes', // This is critical secretType: 'Kubricate.InMemory', // Or your desired secretType allowMerge: true, getEffectIdentifier: effect => effect.value?.metadata?.name ?? 'no-id', prepare: _vitest.vi.fn((name, value) => [{ providerName: 'kubernetes', type: 'kubectl', value: { kind: 'Secret', metadata: { name: 'test', provider: 'kubernetes' }, data: { [name]: JSON.stringify(value) } } }]), mergeSecrets: _vitest.vi.fn(effects => effects) }; mockSecretManager = (name, secrets) => ({ getSecrets: _vitest.vi.fn(() => secrets), resolveConnector: _vitest.vi.fn(() => ({ getWorkingDir: () => undefined, setWorkingDir: _vitest.vi.fn(), load: _vitest.vi.fn(() => Promise.resolve()), get: _vitest.vi.fn(key => secrets[key].value) })), resolveProvider: _vitest.vi.fn(() => mockProvider) }); }); (0, _vitest.it)('includes correct SecretOrigin metadata and snapshots effect', async () => { const _mockSecretManager = mockSecretManager('svc', { API_KEY: { connector: 'env', provider: 'kubernetes', value: '12345' } }); const stacks = { stack1: { getSecretManagers: () => ({ svc: _mockSecretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { conflict: { strategies: { intraProvider: 'autoMerge' } }, secretSpec: _mockSecretManager } } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toMatchSnapshot(); (0, _vitest.expect)(mockProvider.prepare).toHaveBeenCalledWith('API_KEY', '12345'); }); }); (0, _vitest.describe)('SecretsOrchestrator intraProvider (Integration Tests)', () => { let orchestrator; let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should throw on intraProvider conflict with strategy "error"', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'value1', SHARED_KEY_DUPLICATE: 'value2' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).addSecret({ name: 'SHARED_KEY', provider: 'InMemoryProvider' }).addSecret({ name: 'SHARED_KEY_DUPLICATE', provider: 'InMemoryProvider' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { intraProvider: 'error' // This will trigger the error } } } } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); await (0, _vitest.expect)(orchestrator.apply()).rejects.toThrowError(/\[conflict:error:intraProvider\]/); }); (0, _vitest.it)('merges secrets within same provider when intraProvider is "autoMerge"', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ API_KEY: 'abc123', API_SECRET: 'shhh' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).addSecret({ name: 'API_KEY', provider: 'InMemoryProvider' }).addSecret({ name: 'API_SECRET', provider: 'InMemoryProvider' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { intraProvider: 'autoMerge' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(effects[0].value.storeName).toBe('secret-application'); (0, _vitest.expect)(effects[0].value.rawData).toEqual({ API_KEY: 'abc123', API_SECRET: 'shhh' }); }); (0, _vitest.it)('overwrites previous secret when intraProvider is "overwrite"', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'value1', SHARED_KEY_DUPLICATE: 'value2' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).addSecret({ name: 'SHARED_KEY', provider: 'InMemoryProvider' }).addSecret({ name: 'SHARED_KEY_DUPLICATE', provider: 'InMemoryProvider' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { intraProvider: 'overwrite' } } } } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); const effect = effects[0]; // Should only contain last key (SHARED_KEY_DUPLICATE) (0, _vitest.expect)(effect.value.rawData).toEqual({ SHARED_KEY_DUPLICATE: 'value2' }); (0, _vitest.expect)(mockLogger.warn).toHaveBeenCalledWith(_vitest.expect.stringContaining('[conflict:overwrite:intraProvider]')); }); (0, _vitest.it)('keeps last secret and logs dropped in overwrite strategy (intraProvider)', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'v1', SHARED_KEY_DUPLICATE: 'v2' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'secret-app' })).addSecret({ name: 'SHARED_KEY', provider: 'InMemoryProvider' }).addSecret({ name: 'SHARED_KEY_DUPLICATE', provider: 'InMemoryProvider' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { intraProvider: 'overwrite' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(mockLogger.warn).toHaveBeenCalledWith(_vitest.expect.stringContaining('[conflict:overwrite:intraProvider]')); }); }); (0, _vitest.describe)('SecretsOrchestrator crossProvider (Integration Tests)', () => { let orchestrator; let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should throw on intraProvider conflict with strategy "error"', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ my_app_key: 'my_app_key_value', my_app_key_2: 'my_app_key_2_value' })).addProvider('InMemoryProvider1', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).addProvider('InMemoryProvider2', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).setDefaultProvider('InMemoryProvider1').addSecret({ name: 'my_app_key', provider: 'InMemoryProvider1' }).addSecret({ name: 'my_app_key_2', provider: 'InMemoryProvider2' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { crossProvider: 'error' } } } } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); await (0, _vitest.expect)(orchestrator.apply()).rejects.toThrowError(/\[conflict:error:crossProvider\]/); }); (0, _vitest.it)('merges secrets across providers with same storeName when crossProvider is "autoMerge"', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ my_app_key: 'my_app_key_value', my_app_key_2: 'my_app_key_2_value' })).addProvider('InMemoryProvider1', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).addProvider('InMemoryProvider2', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).setDefaultProvider('InMemoryProvider1').addSecret({ name: 'my_app_key', provider: 'InMemoryProvider1' }).addSecret({ name: 'my_app_key_2', provider: 'InMemoryProvider2' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { crossProvider: 'autoMerge' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); // ✅ only one merged result const effect = effects[0]; (0, _vitest.expect)(effect.providerName).toBe('InMemoryProvider1'); // or InMemoryProvider2, doesn't matter (0, _vitest.expect)(effect.value.storeName).toBe('secret-application'); (0, _vitest.expect)(effect.value.rawData).toEqual({ my_app_key: 'my_app_key_value', my_app_key_2: 'my_app_key_2_value' }); }); (0, _vitest.it)('overwrites previous secret when crossProvider is "overwrite"', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'one', SHARED_KEY_2: 'two' })).addProvider('InMemoryProvider1', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).addProvider('InMemoryProvider2', new _InMemoryProvider.InMemoryProvider({ name: 'secret-application' })).setDefaultProvider('InMemoryProvider1').addSecret({ name: 'SHARED_KEY', provider: 'InMemoryProvider1' }).addSecret({ name: 'SHARED_KEY_2', provider: 'InMemoryProvider2' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { crossProvider: 'overwrite' } } } } }; orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(effects[0].value.rawData).toEqual({ SHARED_KEY_2: 'two' }); (0, _vitest.expect)(mockLogger.warn).toHaveBeenCalledWith(_vitest.expect.stringContaining('[conflict:overwrite:crossProvider]')); }); (0, _vitest.it)('keeps last secret and logs dropped in overwrite strategy (crossProvider)', async () => { const secretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'v1', SHARED_KEY_DUPLICATE: 'v2' })).addProvider('InMemoryProvider1', new _InMemoryProvider.InMemoryProvider({ name: 'secret-app' })).addProvider('InMemoryProvider2', new _InMemoryProvider.InMemoryProvider({ name: 'secret-app' })).setDefaultProvider('InMemoryProvider1').addSecret({ name: 'SHARED_KEY', provider: 'InMemoryProvider1' }).addSecret({ name: 'SHARED_KEY_DUPLICATE', provider: 'InMemoryProvider2' }).build(); const stacks = { app: { getSecretManagers: () => ({ default: secretManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretManager, conflict: { strategies: { crossProvider: 'overwrite' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(mockLogger.warn).toHaveBeenCalledWith(_vitest.expect.stringContaining('[conflict:overwrite:crossProvider]')); }); }); (0, _vitest.describe)('SecretsOrchestrator crossManager (Integration Tests)', () => { let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should throw on crossManager conflict with strategy "error"', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY: 'A' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared' })).addSecret({ name: 'KEY', provider: 'InMemoryProvider' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY: 'B' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared' })).addSecret({ name: 'KEY', provider: 'InMemoryProvider' }).build(); const stack = { getSecretManagers: () => ({ svc1, svc2 }) }; const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: { app: stack }, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'error' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); await (0, _vitest.expect)(orchestrator.apply()).rejects.toThrowError(/\[conflict:error:crossManager]/); }); (0, _vitest.it)('merges on crossManager conflict with strategy "autoMerge"', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY1: 'A' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared' })).addSecret({ name: 'KEY1', provider: 'InMemoryProvider' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY2: 'B' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared' })).addSecret({ name: 'KEY2', provider: 'InMemoryProvider' }).build(); const stack = { getSecretManagers: () => ({ svc1, svc2 }) }; const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: { app: stack }, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'autoMerge' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(effects[0].value.rawData).toEqual({ KEY1: 'A', KEY2: 'B' }); }); (0, _vitest.it)('should overwrite secret when conflict occurs within the same SecretManager (crossManager)', async () => { const a = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'one' })).addProvider('P', new _InMemoryProvider.InMemoryProvider({ name: 'stack-secret' })).addSecret({ name: 'SHARED_KEY', provider: 'P' }).build(); const b = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ SHARED_KEY: 'two' })).addProvider('P', new _InMemoryProvider.InMemoryProvider({ name: 'stack-secret' })).addSecret({ name: 'SHARED_KEY', provider: 'P' }).build(); const stacks = { app: { getSecretManagers: () => ({ a, b }) } }; const secretRegistry = new _SecretRegistry.SecretRegistry().add('a', a).add('b', b); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'overwrite' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); }); (0, _vitest.it)('merges same identifier across different SecretManagers with strategy "overwrite"', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY: 'one' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-app' })).addSecret({ name: 'KEY' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY: 'two' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-app' })).addSecret({ name: 'KEY' }).build(); const stacks = { app: { getSecretManagers: () => ({ svc1, svc2 }) } }; const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'overwrite' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(mockLogger.warn).toHaveBeenCalledWith(_vitest.expect.stringContaining('[conflict:overwrite:crossManager]')); }); }); (0, _vitest.describe)('SecretsOrchestrator crossManager (Integration Tests)', () => { let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('throws conflict when same identifier across managers and strategy = "error"', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemory', new _InMemoryConnector.InMemoryConnector({ KEY: 'value1' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemory', new _InMemoryConnector.InMemoryConnector({ KEY: 'value2' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY' }).build(); const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: { app: { getSecretManagers: () => ({ svc1, svc2 }) } }, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'error' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); await (0, _vitest.expect)(orchestrator.apply()).rejects.toThrowError(/\[conflict:error:crossManager]/); }); (0, _vitest.it)('overwrites when same identifier across managers and strategy = "overwrite"', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemory', new _InMemoryConnector.InMemoryConnector({ KEY: 'value1' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemory', new _InMemoryConnector.InMemoryConnector({ KEY: 'value2' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY' }).build(); const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: { app: { getSecretManagers: () => ({ svc1, svc2 }) } }, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'overwrite' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(mockLogger.warn).toHaveBeenCalledWith(_vitest.expect.stringContaining('[conflict:overwrite:crossManager]')); }); (0, _vitest.it)('auto-merges when different keys across managers and strategy = "autoMerge"', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemory', new _InMemoryConnector.InMemoryConnector({ KEY1: 'one' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY1' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemory', new _InMemoryConnector.InMemoryConnector({ KEY2: 'two' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY2' }).build(); const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: { app: { getSecretManagers: () => ({ svc1, svc2 }) } }, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'autoMerge' } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(effects[0].value.rawData).toEqual({ KEY1: 'one', KEY2: 'two' }); }); }); (0, _vitest.describe)('SecretsOrchestrator cross-stack using single SecretManager (Integration)', () => { let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should not conflict when multiple stacks share the same SecretManager', async () => { const sharedManager = new _SecretManager.SecretManager().addConnector('InMemoryLoader', new _InMemoryConnector.InMemoryConnector({ API_KEY: 'abc123', DB_PASSWORD: 'secretpw' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'app-secret' })).setDefaultProvider('InMemoryProvider').addSecret({ name: 'API_KEY', provider: 'InMemoryProvider' }).addSecret({ name: 'DB_PASSWORD', provider: 'InMemoryProvider' }).build(); const stacks = { frontend: { getSecretManagers: () => ({ default: sharedManager }) }, backend: { getSecretManagers: () => ({ default: sharedManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: sharedManager } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); const effects = await orchestrator.apply(); (0, _vitest.expect)(effects).toHaveLength(1); (0, _vitest.expect)(effects[0].providerName).toBe('InMemoryProvider'); (0, _vitest.expect)(effects[0].value.rawData).toEqual({ API_KEY: 'abc123', DB_PASSWORD: 'secretpw' }); // Should not log any conflict or error (0, _vitest.expect)(mockLogger.warn).not.toHaveBeenCalled(); }); }); (0, _vitest.describe)('SecretsOrchestrator strictConflictMode (Negative Test)', () => { let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should throw if strictConflictMode=true but conflict strategies are relaxed', async () => { const sharedManager = new _SecretManager.SecretManager().addConnector('InMemoryLoader', new _InMemoryConnector.InMemoryConnector({ API_KEY: 'abc123', DB_PASSWORD: 'secretpw' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'app-secret' })).setDefaultProvider('InMemoryProvider').addSecret({ name: 'API_KEY', provider: 'InMemoryProvider' }).addSecret({ name: 'DB_PASSWORD', provider: 'InMemoryProvider' }).build(); const stacks = { frontend: { getSecretManagers: () => ({ default: sharedManager }) }, backend: { getSecretManagers: () => ({ default: sharedManager }) } }; const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { manager: sharedManager, conflict: { strict: true, strategies: { intraProvider: 'autoMerge' // ❌ should be error when strictConflictMode = true } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); // 💥 Expect an error thrown during validation, not apply phase await (0, _vitest.expect)(orchestrator.apply()).rejects.toThrowError(/\[config:strictConflictMode\]/); }); }); (0, _vitest.describe)('SecretsOrchestrator Cross-Manager Conflict Detection', () => { let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should detect conflict between different managers when strategy is error (needs managerName in conflictKey)', async () => { const svc1 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY: 'one' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY' }).build(); const svc2 = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ KEY: 'two' })).addProvider('InMemoryProvider', new _InMemoryProvider.InMemoryProvider({ name: 'shared-secret' })).addSecret({ name: 'KEY' }).build(); const stacks = { app: { getSecretManagers: () => ({ svc1, svc2 }) } }; const secretRegistry = new _SecretRegistry.SecretRegistry().add('svc1', svc1).add('svc2', svc2); const options = { logger: mockLogger, effectOptions: {}, config: { stacks: stacks, secret: { secretSpec: secretRegistry, conflict: { strategies: { crossManager: 'error' // ❗ สำคัญ } } } } }; const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create(options); await (0, _vitest.expect)(orchestrator.apply()).rejects.toThrowError(/\[conflict:error:crossManager] Duplicate resource identifier/); }); }); (0, _vitest.describe)('SecretsOrchestrator - Multiple SecretManagers with Same Provider Name', () => { let mockLogger; (0, _vitest.beforeEach)(() => { mockLogger = { info: _vitest.vi.fn(), warn: _vitest.vi.fn(), debug: _vitest.vi.fn() }; }); (0, _vitest.it)('should apply secrets correctly without conflict', async () => { // Arrange - Setup SecretManagers and Registry manually const frontendSecretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ frontend_app_key: 'value1' })).addProvider('OpaqueSecretProvider', new _InMemoryProvider.InMemoryProvider({ name: 'secret-frontend' })).addSecret({ name: 'frontend_app_key' }); const backendSecretManager = new _SecretManager.SecretManager().addConnector('InMemoryConnector', new _InMemoryConnector.InMemoryConnector({ backend_app_key: 'value2' })).addProvider('OpaqueSecretProvider', new _InMemoryProvider.InMemoryProvider({ name: 'secret-backend' })).addSecret({ name: 'backend_app_key' }); const secretRegistry = new _SecretRegistry.SecretRegistry().add('frontend', frontendSecretManager).add('backend', backendSecretManager); const orchestrator = _SecretsOrchestrator.SecretsOrchestrator.create({ effectOptions: {}, config: { secret: { secretSpec: secretRegistry }, stacks: {} }, logger: mockLogger // or mock logger if you want }); // Act const effects = await orchestrator.apply(); // Assert (0, _vitest.expect)(effects).toHaveLength(2); const metadata = effects.map(effect => { return effect.value; }); (0, _vitest.expect)(metadata).toStrictEqual([{ rawData: { frontend_app_key: 'value1' }, storeName: 'secret-frontend' }, { rawData: { backend_app_key: 'value2' }, storeName: 'secret-backend' }]); }); }); //# sourceMappingURL=SecretsOrchestrator.test.js.map