import { testDb, testModules } from '@n8n/backend-test-utils'; import { Container } from '@n8n/di'; import { DynamicCredentialEntryStorage } from '@/modules/dynamic-credentials.ee/credential-resolvers/storage/dynamic-credential-entry-storage'; import { createDynamicCredentialResolver } from './shared/db-helpers'; import { createCredentials } from '../shared/db/credentials'; describe('DynamicCredentialEntryStorage', () => { let storage: DynamicCredentialEntryStorage; let previousEnvVar: string | undefined; beforeAll(async () => { previousEnvVar = process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS; process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS = 'true'; await testModules.loadModules(['dynamic-credentials']); await testDb.init(); storage = Container.get(DynamicCredentialEntryStorage); }); afterAll(async () => { process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS = previousEnvVar; await testDb.terminate(); }); beforeEach(async () => { await testDb.truncate([ 'DynamicCredentialEntry', 'DynamicCredentialResolver', 'CredentialsEntity', ]); }); it('should store and retrieve credential data', async () => { // ARRANGE const credential = await createCredentials({ name: 'Test Credential', type: 'testType', data: 'test-data', }); const resolver = await createDynamicCredentialResolver({ name: 'test-resolver', type: 'test', config: 'test-data', }); const testData = 'encrypted-credential-data'; // ACT - Store await storage.setCredentialData(credential.id, 'test-subject', resolver.id, testData, {}); // ACT - Retrieve const retrievedData = await storage.getCredentialData( credential.id, 'test-subject', resolver.id, {}, ); // ASSERT expect(retrievedData).toBe(testData); }); it('should update existing credential data (upsert)', async () => { // ARRANGE const credential = await createCredentials({ name: 'Test Credential', type: 'testType', data: 'test-data', }); const resolver = await createDynamicCredentialResolver({ name: 'test-resolver', type: 'test', config: 'test-data', }); // ACT - Insert await storage.setCredentialData( credential.id, 'upsert-subject', resolver.id, 'original-data', {}, ); // ACT - Update await storage.setCredentialData( credential.id, 'upsert-subject', resolver.id, 'updated-data', {}, ); // ACT - Retrieve const data = await storage.getCredentialData(credential.id, 'upsert-subject', resolver.id, {}); // ASSERT expect(data).toBe('updated-data'); }); it('should delete credential data', async () => { // ARRANGE const credential = await createCredentials({ name: 'Test Credential', type: 'testType', data: 'test-data', }); const resolver = await createDynamicCredentialResolver({ name: 'test-resolver', type: 'test', config: 'test-data', }); const testData = 'data-to-delete'; // Store data first await storage.setCredentialData(credential.id, 'delete-subject', resolver.id, testData, {}); // Verify it exists const beforeDelete = await storage.getCredentialData( credential.id, 'delete-subject', resolver.id, {}, ); expect(beforeDelete).toBe(testData); // ACT - Delete await storage.deleteCredentialData(credential.id, 'delete-subject', resolver.id, {}); // ASSERT - Verify it's gone const afterDelete = await storage.getCredentialData( credential.id, 'delete-subject', resolver.id, {}, ); expect(afterDelete).toBeNull(); }); it('should isolate entries by composite key (multiple entries do not affect each other)', async () => { // ARRANGE const credential1 = await createCredentials({ name: 'Credential 1', type: 'testType', data: 'test-data-1', }); const credential2 = await createCredentials({ name: 'Credential 2', type: 'testType', data: 'test-data-2', }); const resolver1 = await createDynamicCredentialResolver({ name: 'resolver-1', type: 'test', config: 'test-config-1', }); const resolver2 = await createDynamicCredentialResolver({ name: 'resolver-2', type: 'test', config: 'test-config-2', }); // ACT - Create multiple entries with different combinations // Same credential, different subjects await storage.setCredentialData( credential1.id, 'subject-A', resolver1.id, 'data-cred1-subjA-res1', {}, ); await storage.setCredentialData( credential1.id, 'subject-B', resolver1.id, 'data-cred1-subjB-res1', {}, ); // Same credential and subject, different resolver await storage.setCredentialData( credential1.id, 'subject-A', resolver2.id, 'data-cred1-subjA-res2', {}, ); // Different credential, same subject and resolver await storage.setCredentialData( credential2.id, 'subject-A', resolver1.id, 'data-cred2-subjA-res1', {}, ); // ASSERT - Each entry should be isolated and return correct data const data1 = await storage.getCredentialData(credential1.id, 'subject-A', resolver1.id, {}); expect(data1).toBe('data-cred1-subjA-res1'); const data2 = await storage.getCredentialData(credential1.id, 'subject-B', resolver1.id, {}); expect(data2).toBe('data-cred1-subjB-res1'); const data3 = await storage.getCredentialData(credential1.id, 'subject-A', resolver2.id, {}); expect(data3).toBe('data-cred1-subjA-res2'); const data4 = await storage.getCredentialData(credential2.id, 'subject-A', resolver1.id, {}); expect(data4).toBe('data-cred2-subjA-res1'); // ACT - Update one entry await storage.setCredentialData( credential1.id, 'subject-A', resolver1.id, 'updated-data-cred1-subjA-res1', {}, ); // ASSERT - Only the updated entry should change, others remain unchanged const updatedData1 = await storage.getCredentialData( credential1.id, 'subject-A', resolver1.id, {}, ); expect(updatedData1).toBe('updated-data-cred1-subjA-res1'); const unchangedData2 = await storage.getCredentialData( credential1.id, 'subject-B', resolver1.id, {}, ); expect(unchangedData2).toBe('data-cred1-subjB-res1'); const unchangedData3 = await storage.getCredentialData( credential1.id, 'subject-A', resolver2.id, {}, ); expect(unchangedData3).toBe('data-cred1-subjA-res2'); // ACT - Delete one entry await storage.deleteCredentialData(credential1.id, 'subject-A', resolver1.id, {}); // ASSERT - Deleted entry should be gone, others remain const deletedData = await storage.getCredentialData( credential1.id, 'subject-A', resolver1.id, {}, ); expect(deletedData).toBeNull(); const stillExistingData = await storage.getCredentialData( credential1.id, 'subject-B', resolver1.id, {}, ); expect(stillExistingData).toBe('data-cred1-subjB-res1'); }); });