import { testDb, testModules } from '@n8n/backend-test-utils'; import { CredentialsRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { DynamicCredentialEntry } from '@/modules/dynamic-credentials.ee/database/entities/dynamic-credential-entry'; import { DynamicCredentialResolverRepository } from '@/modules/dynamic-credentials.ee/database/repositories/credential-resolver.repository'; import { DynamicCredentialEntryRepository } from '@/modules/dynamic-credentials.ee/database/repositories/dynamic-credential-entry.repository'; import { createDynamicCredentialResolver } from './shared/db-helpers'; import { createCredentials } from '../shared/db/credentials'; describe('DynamicCredentialEntryRepository', () => { let repository: DynamicCredentialEntryRepository; 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(); repository = Container.get(DynamicCredentialEntryRepository); }); afterAll(async () => { process.env.N8N_ENV_FEAT_DYNAMIC_CREDENTIALS = previousEnvVar; await testDb.terminate(); }); beforeEach(async () => { await testDb.truncate([ 'DynamicCredentialEntry', 'DynamicCredentialResolver', 'CredentialsEntity', ]); }); it('should save and retrieve a dynamic credential entry', 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 entry = new DynamicCredentialEntry(); entry.credentialId = credential.id; entry.subjectId = 'subject-123'; entry.resolverId = resolver.id; entry.data = 'encrypted-test-data'; // ACT const savedEntry = await repository.save(entry); // Retrieve it back const foundEntry = await repository.findOne({ where: { credentialId: credential.id, subjectId: 'subject-123', resolverId: resolver.id, }, }); // ASSERT expect(savedEntry).toBeDefined(); expect(savedEntry.credentialId).toBe(credential.id); expect(savedEntry.subjectId).toBe('subject-123'); expect(savedEntry.resolverId).toBe(resolver.id); expect(savedEntry.data).toBe('encrypted-test-data'); expect(savedEntry.createdAt).toBeInstanceOf(Date); expect(savedEntry.updatedAt).toBeInstanceOf(Date); expect(foundEntry).toBeDefined(); expect(foundEntry?.data).toBe('encrypted-test-data'); }); it('should cascade delete entries when credential is deleted', 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', }); // Create multiple entries for the same credential const entry1 = new DynamicCredentialEntry(); entry1.credentialId = credential.id; entry1.subjectId = 'subject-1'; entry1.resolverId = resolver.id; entry1.data = 'data-1'; const entry2 = new DynamicCredentialEntry(); entry2.credentialId = credential.id; entry2.subjectId = 'subject-2'; entry2.resolverId = resolver.id; entry2.data = 'data-2'; await repository.save(entry1); await repository.save(entry2); // Verify entries exist const entriesBeforeDelete = await repository.find({ where: { credentialId: credential.id, }, }); expect(entriesBeforeDelete).toHaveLength(2); // ACT - Delete the credential const credentialsRepository = Container.get(CredentialsRepository); await credentialsRepository.delete({ id: credential.id }); // ASSERT - All entries for this credential should be cascade deleted const entriesAfterDelete = await repository.find({ where: { credentialId: credential.id, }, }); expect(entriesAfterDelete).toHaveLength(0); }); it('should cascade delete entries when resolver is deleted', 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 resolver = await createDynamicCredentialResolver({ name: 'test-resolver', type: 'test', config: 'test-data', }); // Create entries for multiple credentials using the same resolver const entry1 = new DynamicCredentialEntry(); entry1.credentialId = credential1.id; entry1.subjectId = 'subject-1'; entry1.resolverId = resolver.id; entry1.data = 'data-1'; const entry2 = new DynamicCredentialEntry(); entry2.credentialId = credential2.id; entry2.subjectId = 'subject-2'; entry2.resolverId = resolver.id; entry2.data = 'data-2'; await repository.save(entry1); await repository.save(entry2); // Verify entries exist const entriesBeforeDelete = await repository.find({ where: { resolverId: resolver.id, }, }); expect(entriesBeforeDelete).toHaveLength(2); // ACT - Delete the resolver const resolverRepository = Container.get(DynamicCredentialResolverRepository); await resolverRepository.delete({ id: resolver.id }); // ASSERT - All entries for this resolver should be cascade deleted const entriesAfterDelete = await repository.find({ where: { resolverId: resolver.id, }, }); expect(entriesAfterDelete).toHaveLength(0); }); it('should fetch CredentialsEntity through ManyToOne relationship', async () => { // ARRANGE const credential = await createCredentials({ name: 'Test Credential for Relationship', type: 'testType', data: 'test-data', }); const resolver = await createDynamicCredentialResolver({ name: 'test-resolver', type: 'test', config: 'test-data', }); const entry = new DynamicCredentialEntry(); entry.credentialId = credential.id; entry.subjectId = 'subject-123'; entry.resolverId = resolver.id; entry.data = 'encrypted-test-data'; await repository.save(entry); // ACT - Fetch entry with credential relationship loaded const foundEntry = await repository.findOne({ where: { credentialId: credential.id, subjectId: 'subject-123', resolverId: resolver.id, }, relations: ['credential'], }); // ASSERT expect(foundEntry).toBeDefined(); expect(foundEntry?.credential).toBeDefined(); expect(foundEntry?.credential.id).toBe(credential.id); expect(foundEntry?.credential.name).toBe('Test Credential for Relationship'); expect(foundEntry?.credential.type).toBe('testType'); }); it('should fetch DynamicCredentialResolver through ManyToOne relationship', async () => { // ARRANGE const credential = await createCredentials({ name: 'Test Credential', type: 'testType', data: 'test-data', }); const resolver = await createDynamicCredentialResolver({ name: 'test-resolver-for-relationship', type: 'test-type', config: 'test-config-data', }); const entry = new DynamicCredentialEntry(); entry.credentialId = credential.id; entry.subjectId = 'subject-456'; entry.resolverId = resolver.id; entry.data = 'encrypted-test-data'; await repository.save(entry); // ACT - Fetch entry with resolver relationship loaded const foundEntry = await repository.findOne({ where: { credentialId: credential.id, subjectId: 'subject-456', resolverId: resolver.id, }, relations: ['resolver'], }); // ASSERT expect(foundEntry).toBeDefined(); expect(foundEntry?.resolver).toBeDefined(); expect(foundEntry?.resolver.id).toBe(resolver.id); expect(foundEntry?.resolver.name).toBe('test-resolver-for-relationship'); expect(foundEntry?.resolver.type).toBe('test-type'); }); it('should filter entries by credential type using find method', async () => { // ARRANGE const credential1 = await createCredentials({ name: 'OAuth Credential', type: 'oAuth2Api', data: 'oauth-data', }); const credential2 = await createCredentials({ name: 'API Key Credential', type: 'apiKeyAuth', data: 'api-key-data', }); const credential3 = await createCredentials({ name: 'Another OAuth Credential', type: 'oAuth2Api', data: 'oauth-data-2', }); const resolver = await createDynamicCredentialResolver({ name: 'test-resolver', type: 'test', config: 'test-data', }); // Create entries for different credential types const entry1 = new DynamicCredentialEntry(); entry1.credentialId = credential1.id; entry1.subjectId = 'subject-1'; entry1.resolverId = resolver.id; entry1.data = 'data-1'; const entry2 = new DynamicCredentialEntry(); entry2.credentialId = credential2.id; entry2.subjectId = 'subject-2'; entry2.resolverId = resolver.id; entry2.data = 'data-2'; const entry3 = new DynamicCredentialEntry(); entry3.credentialId = credential3.id; entry3.subjectId = 'subject-3'; entry3.resolverId = resolver.id; entry3.data = 'data-3'; await repository.save([entry1, entry2, entry3]); // ACT - Query entries where credential type is 'oAuth2Api' const oauthEntries = await repository.find({ where: { credential: { type: 'oAuth2Api', }, }, relations: ['credential'], }); // ASSERT expect(oauthEntries).toHaveLength(2); expect(oauthEntries.every((entry) => entry.credential.type === 'oAuth2Api')).toBe(true); expect(oauthEntries.map((e) => e.subjectId).sort()).toEqual(['subject-1', 'subject-3']); }); it('should filter entries by resolver type using find method', async () => { // ARRANGE const credential = await createCredentials({ name: 'Test Credential', type: 'testType', data: 'test-data', }); const resolver1 = await createDynamicCredentialResolver({ name: 'AWS Resolver', type: 'aws-secrets-manager', config: 'aws-config', }); const resolver2 = await createDynamicCredentialResolver({ name: 'Azure Resolver', type: 'azure-key-vault', config: 'azure-config', }); const resolver3 = await createDynamicCredentialResolver({ name: 'Another AWS Resolver', type: 'aws-secrets-manager', config: 'aws-config-2', }); // Create entries for different resolver types const entry1 = new DynamicCredentialEntry(); entry1.credentialId = credential.id; entry1.subjectId = 'subject-1'; entry1.resolverId = resolver1.id; entry1.data = 'data-1'; const entry2 = new DynamicCredentialEntry(); entry2.credentialId = credential.id; entry2.subjectId = 'subject-2'; entry2.resolverId = resolver2.id; entry2.data = 'data-2'; const entry3 = new DynamicCredentialEntry(); entry3.credentialId = credential.id; entry3.subjectId = 'subject-3'; entry3.resolverId = resolver3.id; entry3.data = 'data-3'; await repository.save([entry1, entry2, entry3]); // ACT - Query entries where resolver type is 'aws-secrets-manager' const awsEntries = await repository.find({ where: { resolver: { type: 'aws-secrets-manager', }, }, relations: ['resolver'], }); // ASSERT expect(awsEntries).toHaveLength(2); expect(awsEntries.every((entry) => entry.resolver.type === 'aws-secrets-manager')).toBe(true); expect(awsEntries.map((e) => e.subjectId).sort()).toEqual(['subject-1', 'subject-3']); }); it('should filter entries by both credential type and resolver type using find method', async () => { // ARRANGE const credential1 = await createCredentials({ name: 'OAuth Credential 1', type: 'oAuth2Api', data: 'oauth-data-1', }); const credential2 = await createCredentials({ name: 'OAuth Credential 2', type: 'oAuth2Api', data: 'oauth-data-2', }); const credential3 = await createCredentials({ name: 'API Key Credential', type: 'apiKeyAuth', data: 'api-key-data', }); const resolver1 = await createDynamicCredentialResolver({ name: 'AWS Resolver', type: 'aws-secrets-manager', config: 'aws-config', }); const resolver2 = await createDynamicCredentialResolver({ name: 'Azure Resolver', type: 'azure-key-vault', config: 'azure-config', }); // Create entries with various combinations const entry1 = new DynamicCredentialEntry(); entry1.credentialId = credential1.id; entry1.subjectId = 'subject-1'; entry1.resolverId = resolver1.id; entry1.data = 'data-1'; const entry2 = new DynamicCredentialEntry(); entry2.credentialId = credential1.id; entry2.subjectId = 'subject-2'; entry2.resolverId = resolver2.id; entry2.data = 'data-2'; const entry3 = new DynamicCredentialEntry(); entry3.credentialId = credential2.id; entry3.subjectId = 'subject-3'; entry3.resolverId = resolver1.id; entry3.data = 'data-3'; const entry4 = new DynamicCredentialEntry(); entry4.credentialId = credential3.id; entry4.subjectId = 'subject-4'; entry4.resolverId = resolver1.id; entry4.data = 'data-4'; await repository.save([entry1, entry2, entry3, entry4]); // ACT - Query entries where credential type is 'oAuth2Api' AND resolver type is 'aws-secrets-manager' const filteredEntries = await repository.find({ where: { credential: { type: 'oAuth2Api', }, resolver: { type: 'aws-secrets-manager', }, }, relations: ['credential', 'resolver'], }); // ASSERT - Should only return entries with both OAuth credentials and AWS resolver expect(filteredEntries).toHaveLength(2); expect( filteredEntries.every( (entry) => entry.credential.type === 'oAuth2Api' && entry.resolver.type === 'aws-secrets-manager', ), ).toBe(true); expect(filteredEntries.map((e) => e.subjectId).sort()).toEqual(['subject-1', 'subject-3']); }); });