Files
n8n/packages/cli/test/integration/credentials/credentials-overwrites.integration.test.ts
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

421 lines
14 KiB
TypeScript

import { mockInstance, testDb } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import { SettingsRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { Cipher } from 'n8n-core';
import { CredentialsOverwrites } from '@/credentials-overwrites';
import { CredentialTypes } from '@/credential-types';
import type { ICredentialsOverwrite } from '@/interfaces';
import { FrontendService } from '@/services/frontend.service';
describe('CredentialsOverwrites - Integration Tests', () => {
let credentialsOverwrites: CredentialsOverwrites;
let globalConfig: GlobalConfig;
let settingsRepository: SettingsRepository;
let cipher: Cipher;
let credentialTypes: CredentialTypes;
beforeAll(async () => {
// Mock FrontendService to prevent initialization issues during tests
mockInstance(FrontendService);
// Initialize real database
await testDb.init();
// Get real instances from DI container
globalConfig = Container.get(GlobalConfig);
settingsRepository = Container.get(SettingsRepository);
cipher = Container.get(Cipher);
credentialTypes = Container.get(CredentialTypes);
// Configure globalConfig for integration tests
(globalConfig as any).credentials = {
overwrite: {
data: '', // No static data for integration tests
persistence: true, // Enable persistence for integration tests
endpointAuthToken: 'integration-test-token',
endpoint: 'integration-credentials-overwrite',
},
};
// Create instance for integration testing
credentialsOverwrites = Container.get(CredentialsOverwrites);
});
beforeEach(async () => {
// Clean up database before each test
await testDb.truncate(['Settings']);
});
afterAll(async () => {
// Clean up database after all tests
await testDb.terminate();
});
describe('Integration Test Coverage', () => {
describe('Complete Credential Update Flow', () => {
it('should execute full API → Database → PubSub → Frontend chain', async () => {
const testOverwriteData: ICredentialsOverwrite = {
test: { username: 'integrationUser', password: 'integrationPass' },
};
// Step 1: API call (setData) - this will save to real database
await credentialsOverwrites.setData(testOverwriteData, true, false); // Skip frontend reload for integration test
// Step 2: Verify data was saved to database by reading it back
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
expect(savedSetting?.value).toBeTruthy();
// Step 3: Decrypt and verify the saved data
const decryptedData = cipher.decrypt(savedSetting!.value);
const parsedData = JSON.parse(decryptedData);
expect(parsedData).toEqual(testOverwriteData);
// Step 4: Test that the data was set in memory
const allData = credentialsOverwrites.getAll();
expect(allData).toEqual(testOverwriteData);
});
it('should maintain data consistency across sequential operations', async () => {
const data1: ICredentialsOverwrite = { type1: { key1: 'value1' } };
const data2: ICredentialsOverwrite = { type2: { key2: 'value2' } };
// Execute sequential operations (avoid UNIQUE constraint issues)
await credentialsOverwrites.setData(data1, true, false);
// Verify first operation
let savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
let decryptedData = cipher.decrypt(savedSetting!.value);
expect(JSON.parse(decryptedData)).toEqual(data1);
await credentialsOverwrites.setData(data2, true, false);
// Verify second operation overwrote the first
savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
decryptedData = cipher.decrypt(savedSetting!.value);
expect(JSON.parse(decryptedData)).toEqual(data2);
// The final state should be the last write
const finalData = credentialsOverwrites.getAll();
expect(finalData).toEqual(data2);
});
});
describe('PubSub Event Flow Integration', () => {
it('should execute complete PubSub event → Database load → Frontend reload chain', async () => {
const testData: ICredentialsOverwrite = {
pubsub: { token: 'pubsubToken', secret: 'pubsubSecret' },
};
// Step 1: Save data to database first
const encryptedData = cipher.encrypt(JSON.stringify(testData));
const setting = settingsRepository.create({
key: 'credentialsOverwrite',
value: encryptedData,
loadOnStartup: false,
});
await settingsRepository.save(setting);
// Step 2: Simulate PubSub event (this will load from database)
await credentialsOverwrites.reloadOverwriteCredentials();
// Step 3: Verify data was loaded from database
const loadedData = credentialsOverwrites.getAll();
expect(loadedData).toEqual(testData);
});
it('should prevent race conditions between PubSub reload calls', async () => {
const testData: ICredentialsOverwrite = { race: { condition: 'test' } };
const encryptedData = cipher.encrypt(JSON.stringify(testData));
// Save test data to database
const setting = settingsRepository.create({
key: 'credentialsOverwrite',
value: encryptedData,
loadOnStartup: false,
});
await settingsRepository.save(setting);
// Fire multiple concurrent PubSub events
const promises = Array(3)
.fill(0)
.map(async () => await credentialsOverwrites.reloadOverwriteCredentials());
await Promise.all(promises);
// Should complete without errors and load the data
const loadedData = credentialsOverwrites.getAll();
expect(loadedData).toEqual(testData);
});
});
describe('Mixed Configuration Scenarios', () => {
it('should handle static config + database persistence working together', async () => {
const staticData: ICredentialsOverwrite = { static: { key: 'staticValue' } };
const dbData: ICredentialsOverwrite = { database: { key: 'dbValue' } };
// Configure static config
(globalConfig as any).credentials.overwrite.data = JSON.stringify(staticData);
// Save database data
const encryptedDbData = cipher.encrypt(JSON.stringify(dbData));
const dbSetting = settingsRepository.create({
key: 'credentialsOverwrite',
value: encryptedDbData,
loadOnStartup: false,
});
await settingsRepository.save(dbSetting);
// Initialize with both static and database data
const mixedConfig = new CredentialsOverwrites(
globalConfig,
credentialTypes,
{ debug: jest.fn(), warn: jest.fn(), error: jest.fn() } as any,
settingsRepository,
cipher,
);
await mixedConfig.init();
// Should have database data (loaded after static)
const finalData = mixedConfig.getAll();
expect(finalData).toEqual(dbData);
// Reset config
(globalConfig as any).credentials.overwrite.data = '';
});
it('should handle configuration changes at runtime', async () => {
const initialData: ICredentialsOverwrite = { initial: { key: 'initialValue' } };
const updatedData: ICredentialsOverwrite = { updated: { key: 'updatedValue' } };
// Set initial data
await credentialsOverwrites.setData(initialData, true, false);
expect(credentialsOverwrites.getAll()).toEqual(initialData);
// Update configuration at runtime
await credentialsOverwrites.setData(updatedData, true, false);
expect(credentialsOverwrites.getAll()).toEqual(updatedData);
// Verify updated data was saved to database
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
const decryptedData = cipher.decrypt(savedSetting!.value);
expect(JSON.parse(decryptedData)).toEqual(updatedData);
});
});
describe('Multi-Instance Coordination', () => {
it('should broadcast changes to multiple instances via PubSub', async () => {
const testData: ICredentialsOverwrite = { broadcast: { message: 'hello' } };
// Simulate saving data with broadcast enabled (default behavior)
await credentialsOverwrites.setData(testData, true, false);
// Verify data was saved
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
// Verify data is accessible
const loadedData = credentialsOverwrites.getAll();
expect(loadedData).toEqual(testData);
});
it('should coordinate multiple instances receiving same PubSub event', async () => {
const testData: ICredentialsOverwrite = { coordination: { test: 'value' } };
const encryptedData = cipher.encrypt(JSON.stringify(testData));
// Save data to database
const setting = settingsRepository.create({
key: 'credentialsOverwrite',
value: encryptedData,
loadOnStartup: false,
});
await settingsRepository.save(setting);
// Simulate multiple instances receiving the same PubSub event
const instance1Promise = credentialsOverwrites.reloadOverwriteCredentials();
const instance2Promise = credentialsOverwrites.reloadOverwriteCredentials();
const instance3Promise = credentialsOverwrites.reloadOverwriteCredentials();
await Promise.all([instance1Promise, instance2Promise, instance3Promise]);
// All instances should coordinate properly without conflicts
const loadedData = credentialsOverwrites.getAll();
expect(loadedData).toEqual(testData);
});
});
describe('Error Resilience Integration', () => {
it('should handle transient failures and recovery', async () => {
const testData: ICredentialsOverwrite = { recovery: { test: 'data' } };
// First attempt with failing repository
const failingRepo = {
...settingsRepository,
create: jest.fn(() => {
throw new Error('Temporary failure');
}),
};
const failingInstance = new CredentialsOverwrites(
globalConfig,
credentialTypes,
{ debug: jest.fn(), warn: jest.fn(), error: jest.fn() } as any,
failingRepo as any,
cipher,
);
// First call with failing repo (memory only)
await failingInstance.setData(testData, false, false);
// Second call with working repo (should succeed)
await credentialsOverwrites.setData(testData, true, false);
// Verify recovery worked
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
});
});
describe('Backwards Compatibility Integration', () => {
it('should maintain compatibility with existing workflow patterns', async () => {
const legacyData: ICredentialsOverwrite = {
existing: { username: 'legacy', password: 'legacy' },
};
// Test existing setData call with explicit parameters to avoid frontend reload
await credentialsOverwrites.setData(legacyData, true, false);
// Verify backwards compatibility
expect(credentialsOverwrites.getAll()).toEqual(legacyData);
// Verify data was saved to database (default behavior)
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
// Test credential application (existing interface)
const appliedData = credentialsOverwrites.applyOverwrite('existing', {
username: '',
password: '',
});
expect(appliedData).toEqual({
username: 'legacy',
password: 'legacy',
});
});
it('should work with existing middleware and endpoint patterns', async () => {
// Test middleware functionality
const middleware = credentialsOverwrites.getOverwriteEndpointMiddleware();
expect(middleware).toBeDefined();
// Test overwrite application
const testData: ICredentialsOverwrite = { middleware: { token: 'middlewareTest' } };
await credentialsOverwrites.setData(testData, true, false);
const appliedData = credentialsOverwrites.applyOverwrite('middleware', {
token: '',
});
expect(appliedData).toEqual({ token: 'middlewareTest' });
});
});
describe('End-to-End Validation', () => {
it('should complete full system integration successfully', async () => {
const e2eData: ICredentialsOverwrite = {
e2eTest: {
username: 'e2eUser',
password: 'e2ePass',
token: 'e2eToken',
},
};
// Step 1: Complete integration flow (skip frontend for test)
await credentialsOverwrites.setData(e2eData, true, false);
// Step 2: Verify database persistence
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
const decryptedData = cipher.decrypt(savedSetting!.value);
expect(JSON.parse(decryptedData)).toEqual(e2eData);
// Step 3: Test PubSub reload
const freshInstance = new CredentialsOverwrites(
globalConfig,
credentialTypes,
{ debug: jest.fn(), warn: jest.fn(), error: jest.fn() } as any,
settingsRepository,
cipher,
);
await freshInstance.reloadOverwriteCredentials();
expect(freshInstance.getAll()).toEqual(e2eData);
// Step 4: Test that overwrites work correctly with new data
const appliedOverwrite = credentialsOverwrites.applyOverwrite('e2eTest', {
username: '',
password: '',
token: '',
});
expect(appliedOverwrite).toMatchObject({
username: 'e2eUser',
password: 'e2ePass',
token: 'e2eToken',
});
// Step 5: Test middleware still works
const middleware = credentialsOverwrites.getOverwriteEndpointMiddleware();
expect(middleware).toBeDefined();
});
it('should maintain data integrity across complete integration lifecycle', async () => {
const lifecycleData: ICredentialsOverwrite = {
lifecycle: { phase1: 'init', phase2: 'process', phase3: 'complete' },
};
// Phase 1: Initial save
await credentialsOverwrites.setData(lifecycleData, true, false);
// Phase 2: Database verification
const savedSetting = await settingsRepository.findByKey('credentialsOverwrite');
expect(savedSetting).toBeTruthy();
// Phase 3: Fresh instance reload
const newInstance = new CredentialsOverwrites(
globalConfig,
credentialTypes,
{ debug: jest.fn(), warn: jest.fn(), error: jest.fn() } as any,
settingsRepository,
cipher,
);
await newInstance.reloadOverwriteCredentials();
// Phase 4: Verify complete data integrity
expect(newInstance.getAll()).toEqual(lifecycleData);
// Phase 5: Apply overwrites and verify functionality
const result = newInstance.applyOverwrite('lifecycle', {
phase1: '',
phase2: '',
phase3: '',
});
expect(result).toEqual({
phase1: 'init',
phase2: 'process',
phase3: 'complete',
});
});
});
});
});