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
421 lines
14 KiB
TypeScript
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',
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|