import { Logger } from '@n8n/backend-common'; import { mockInstance, mockLogger, testDb, testModules } from '@n8n/backend-test-utils'; import { SecretsProviderConnectionRepository, SettingsRepository } from '@n8n/db'; import { ShutdownMetadata } from '@n8n/decorators'; import { Container } from '@n8n/di'; import { Cipher } from 'n8n-core'; import { ExternalSecretsProviders } from '@/modules/external-secrets.ee/external-secrets-providers.ee'; import { ExternalSecretsConfig } from '@/modules/external-secrets.ee/external-secrets.config'; import { ExternalSecretsModule } from '@/modules/external-secrets.ee/external-secrets.module'; import { AnotherDummyProvider, DummyProvider, MockProviders, } from '../../shared/external-secrets/utils'; const mockProvidersInstance = new MockProviders(); mockInstance(ExternalSecretsProviders, mockProvidersInstance); describe('ExternalSecretsModule', () => { beforeAll(async () => { await testModules.loadModules(['external-secrets']); await testDb.init(); Container.set(Logger, mockLogger()); }); afterAll(async () => { await testDb.terminate(); }); describe('decorator verification', () => { it('should have shutdown method decorated with @OnShutdown', () => { const shutdownMetadata = Container.get(ShutdownMetadata); const handlers = shutdownMetadata.getHandlersByPriority().flat(); const hasShutdownHandler = handlers.some( (handler) => handler.serviceClass.name === 'ExternalSecretsModule' && handler.methodName === 'shutdown', ); expect(hasShutdownHandler).toBe(true); }); }); describe('using settings store', () => { let module: ExternalSecretsModule; beforeAll(async () => { mockProvidersInstance.setProviders({ dummy: DummyProvider, another_dummy: AnotherDummyProvider, }); const settingsRepository = Container.get(SettingsRepository); const cipher = Container.get(Cipher); const config = Container.get(ExternalSecretsConfig); config.externalSecretsForProjects = false; config.externalSecretsMultipleConnections = false; module = Container.get(ExternalSecretsModule); const settings = { dummy: { connected: true, connectedAt: new Date(), settings: {}, }, another_dummy: { connected: false, connectedAt: new Date(), settings: {}, }, }; await settingsRepository.save({ key: 'feature.externalSecrets', value: cipher.encrypt(settings), loadOnStartup: false, }); }); afterEach(async () => { await module.shutdown(); }); it('should load enabled providers on init', async () => { const initSpy = jest.spyOn(DummyProvider.prototype, 'init'); const connectSpy = jest.spyOn(DummyProvider.prototype, 'connect'); const updateSpy = jest.spyOn(DummyProvider.prototype, 'update'); const initDisabledSpy = jest.spyOn(AnotherDummyProvider.prototype, 'init'); const connectDisabledSpy = jest.spyOn(AnotherDummyProvider.prototype, 'connect'); const updateDisabledSpy = jest.spyOn(AnotherDummyProvider.prototype, 'update'); await module.init(); expect(initSpy).toHaveBeenCalled(); expect(connectSpy).toHaveBeenCalled(); expect(updateSpy).toHaveBeenCalled(); expect(initDisabledSpy).toHaveBeenCalled(); expect(connectDisabledSpy).not.toHaveBeenCalled(); expect(updateDisabledSpy).not.toHaveBeenCalled(); initSpy.mockRestore(); connectSpy.mockRestore(); updateSpy.mockRestore(); initDisabledSpy.mockRestore(); connectDisabledSpy.mockRestore(); updateDisabledSpy.mockRestore(); }); it('should disconnect providers after shutdown', async () => { await module.init(); const disconnectSpy = jest.spyOn(DummyProvider.prototype, 'disconnect'); await module.shutdown(); expect(disconnectSpy).toHaveBeenCalled(); disconnectSpy.mockRestore(); }); }); describe('using provider connections entities', () => { let connectionRepository: SecretsProviderConnectionRepository; let cipher: Cipher; let module: ExternalSecretsModule; beforeAll(async () => { mockProvidersInstance.setProviders({ dummy: DummyProvider, another_dummy: AnotherDummyProvider, }); connectionRepository = Container.get(SecretsProviderConnectionRepository); cipher = Container.get(Cipher); const config = Container.get(ExternalSecretsConfig); config.externalSecretsForProjects = true; module = Container.get(ExternalSecretsModule); const encryptedSettings = cipher.encrypt(JSON.stringify({})); await connectionRepository.save({ providerKey: 'my-vault', type: 'dummy', encryptedSettings, isEnabled: true, }); await connectionRepository.save({ providerKey: 'another-vault', type: 'another_dummy', encryptedSettings, isEnabled: true, }); }); afterEach(async () => { await module.shutdown(); }); it('should load and connect all providers on init', async () => { const initSpy = jest.spyOn(DummyProvider.prototype, 'init'); const connectSpy = jest.spyOn(DummyProvider.prototype, 'connect'); const updateSpy = jest.spyOn(DummyProvider.prototype, 'update'); const initAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'init'); const connectAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'connect'); const updateAnotherSpy = jest.spyOn(AnotherDummyProvider.prototype, 'update'); await module.init(); expect(initSpy).toHaveBeenCalled(); expect(connectSpy).toHaveBeenCalled(); expect(updateSpy).toHaveBeenCalled(); expect(initAnotherSpy).toHaveBeenCalled(); expect(connectAnotherSpy).toHaveBeenCalled(); expect(updateAnotherSpy).toHaveBeenCalled(); initSpy.mockRestore(); connectSpy.mockRestore(); updateSpy.mockRestore(); initAnotherSpy.mockRestore(); connectAnotherSpy.mockRestore(); updateAnotherSpy.mockRestore(); }); it('should disconnect providers after shutdown', async () => { await module.init(); const disconnectSpy = jest.spyOn(DummyProvider.prototype, 'disconnect'); await module.shutdown(); expect(disconnectSpy).toHaveBeenCalled(); disconnectSpy.mockRestore(); }); }); });