first commit
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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,74 @@
import { test, expect } from '../../../../fixtures/base';
const MOCK_PACKAGE = {
createdAt: '2024-07-22T19:08:06.505Z',
updatedAt: '2024-07-22T19:08:06.505Z',
packageName: 'n8n-nodes-chatwork',
installedVersion: '1.0.0',
authorName: null,
authorEmail: null,
installedNodes: [
{
name: 'Chatwork',
type: 'n8n-nodes-chatwork.chatwork',
latestVersion: 1,
},
],
updateAvailable: '1.1.2',
};
test.describe('Community nodes management', {
annotation: [
{ type: 'owner', description: 'NODES' },
],
}, () => {
test('can install, update and uninstall community nodes', async ({ n8n }) => {
await n8n.page.route('**/api.npms.io/v2/search*', async (route) => {
await route.fulfill({ status: 200, json: {} });
});
await n8n.page.route('/rest/community-packages', async (route) => {
if (route.request().method() === 'GET') {
await route.fulfill({ status: 200, json: { data: [] } });
}
});
await n8n.navigate.toCommunityNodes();
await n8n.page.route('/rest/community-packages', async (route) => {
if (route.request().method() === 'POST') {
await route.fulfill({ status: 200, json: { data: MOCK_PACKAGE } });
} else if (route.request().method() === 'GET') {
await route.fulfill({ status: 200, json: { data: [MOCK_PACKAGE] } });
}
});
await n8n.communityNodes.installPackage('n8n-nodes-chatwork@1.0.0');
await expect(n8n.communityNodes.getCommunityCards()).toHaveCount(1);
await expect(n8n.communityNodes.getCommunityCards().first()).toContainText('v1.0.0');
const updatedPackage = {
...MOCK_PACKAGE,
installedVersion: '1.2.0',
updateAvailable: undefined,
};
await n8n.page.route('/rest/community-packages', async (route) => {
if (route.request().method() === 'PATCH') {
await route.fulfill({ status: 200, json: { data: updatedPackage } });
}
});
await n8n.communityNodes.updatePackage();
await expect(n8n.communityNodes.getCommunityCards()).toHaveCount(1);
await expect(n8n.communityNodes.getCommunityCards().first()).not.toContainText('v1.0.0');
await n8n.page.route('/rest/community-packages*', async (route) => {
if (route.request().method() === 'DELETE') {
await route.fulfill({ status: 204 });
}
});
await n8n.communityNodes.uninstallPackage();
await expect(n8n.communityNodes.getActionBox()).toBeVisible();
});
});
@@ -0,0 +1,145 @@
import { expect, test } from '../../../../fixtures/base';
import type { n8nPage } from '../../../../pages/n8nPage';
import {
buildRepoUrl,
generateUniqueRepoName,
initSourceControl,
} from '../../../../utils/source-control-helper';
test.use({ capability: 'source-control' });
async function saveSettings(n8n: n8nPage) {
await n8n.settingsEnvironment.getSaveButton().click();
await n8n.page.waitForResponse(
(response) =>
response.url().includes('/rest/source-control/preferences') &&
response.request().method() === 'PATCH',
);
}
// Skipped: These tests are flaky. Re-enable when PAY-4365 is resolved.
// https://linear.app/n8n/issue/PAY-4365/bug-source-control-operations-fail-in-multi-main-deployment
test.describe(
'Source Control Settings @capability:source-control',
{
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
},
() => {
test.fixme();
let repoUrl: string;
let repoName: string;
test.beforeEach(async ({ n8n, services }) => {
await n8n.api.enableFeature('sourceControl');
const gitea = services.gitea;
await initSourceControl({ n8n, gitea });
// Create unique repo with branches via API (not UI)
repoName = generateUniqueRepoName();
await gitea.createRepo(repoName);
repoUrl = buildRepoUrl(repoName);
});
test('should connect to Git repository using SSH', async ({ n8n }) => {
// Test UI connection flow with unique repo
await n8n.navigate.toEnvironments();
await n8n.settingsEnvironment.fillRepoUrl(repoUrl);
await expect(n8n.settingsEnvironment.getConnectButton()).toBeEnabled();
await n8n.settingsEnvironment.getConnectButton().click();
await expect(n8n.settingsEnvironment.getDisconnectButton()).toBeVisible();
await expect(n8n.settingsEnvironment.getBranchSelect()).toBeVisible();
await n8n.settingsEnvironment.getBranchSelect().click();
await expect(n8n.page.getByRole('option', { name: 'main' })).toBeVisible();
// Verify source control connected indicator is visible
await n8n.navigate.toHome();
await expect(n8n.sideBar.getSourceControlConnectedIndicator()).toBeVisible();
});
test('should switch between branches', async ({ n8n, services }) => {
const gitea = services.gitea;
await gitea.createBranch(repoName, 'development');
await gitea.createBranch(repoName, 'staging');
await gitea.createBranch(repoName, 'production');
await n8n.api.sourceControl.connect({ repositoryUrl: repoUrl });
await n8n.navigate.toEnvironments();
// Switch to 'development' branch
await n8n.settingsEnvironment.getBranchSelect().click();
await expect(n8n.page.getByRole('option', { name: 'main' })).toBeVisible();
await expect(n8n.page.getByRole('option', { name: 'development' })).toBeVisible();
await expect(n8n.page.getByRole('option', { name: 'staging' })).toBeVisible();
await expect(n8n.page.getByRole('option', { name: 'production' })).toBeVisible();
await n8n.page.getByRole('option', { name: 'development' }).click();
await saveSettings(n8n);
// Verify branch switched by checking preferences
let preferencesResponse = await n8n.page.request.get('/rest/source-control/preferences');
let preferences = await preferencesResponse.json();
expect(preferences.data.branchName).toBe('development');
// Switch back to 'main'
await n8n.settingsEnvironment.selectBranch('main');
await saveSettings(n8n);
// Verify switched back
preferencesResponse = await n8n.page.request.get('/rest/source-control/preferences');
preferences = await preferencesResponse.json();
expect(preferences.data.branchName).toBe('main');
});
test('should enable read-only mode and restrict operations', async ({ n8n }) => {
await n8n.api.sourceControl.connect({ repositoryUrl: repoUrl });
await n8n.navigate.toEnvironments();
await n8n.settingsEnvironment.enableReadOnlyMode();
await saveSettings(n8n);
// Verify push button is disabled in read-only mode
await n8n.navigate.toHome();
await expect(n8n.sideBar.getSourceControlPushButton()).toBeDisabled();
await expect(n8n.sideBar.getSourceControlPullButton()).toBeEnabled();
await n8n.navigate.toEnvironments();
await n8n.settingsEnvironment.disableReadOnlyMode();
await saveSettings(n8n);
// Verify push button is enabled again
await n8n.navigate.toHome();
await expect(n8n.sideBar.getSourceControlPushButton()).toBeEnabled();
await expect(n8n.sideBar.getSourceControlPullButton()).toBeEnabled();
});
test('should disconnect and reconnect with existing keys', async ({ n8n }) => {
await n8n.api.sourceControl.connect({ repositoryUrl: repoUrl });
await n8n.navigate.toEnvironments();
await n8n.settingsEnvironment.disconnect();
// check that source control is disconnected
await n8n.navigate.toHome();
await expect(n8n.sideBar.getSourceControlConnectedIndicator()).toBeHidden();
// Reconnect
await n8n.navigate.toEnvironments();
await n8n.settingsEnvironment.fillRepoUrl(repoUrl);
await expect(n8n.settingsEnvironment.getConnectButton()).toBeEnabled();
await n8n.settingsEnvironment.getConnectButton().click();
await expect(n8n.settingsEnvironment.getDisconnectButton()).toBeVisible();
await expect(n8n.settingsEnvironment.getBranchSelect()).toBeVisible();
// check that source control is connected
await n8n.navigate.toHome();
await expect(n8n.sideBar.getSourceControlConnectedIndicator()).toBeVisible();
});
},
);
@@ -0,0 +1,157 @@
import { customAlphabet } from 'nanoid';
import { test, expect } from '../../../../fixtures/base';
const generateValidId = customAlphabet(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
8,
);
test.describe('Variables', {
annotation: [
{ type: 'owner', description: 'Lifecycle & Governance' },
],
}, () => {
// These tests are serial since it's at an instance level and they interact with the same variables
test.describe.configure({ mode: 'serial' });
test.describe('unlicensed', () => {
test('should show the unlicensed action box when the feature is disabled', async ({ n8n }) => {
await n8n.api.disableFeature('variables');
await n8n.navigate.toVariables();
await expect(n8n.variables.getUnavailableResourcesList()).toBeVisible();
await expect(n8n.variables.getResourcesList()).toBeHidden();
});
});
test.describe('licensed', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.enableFeature('variables');
await n8n.api.variables.deleteAllVariables();
await n8n.navigate.toVariables();
});
test('should create a new variable using empty state', async ({ n8n }) => {
const key = `ENV_VAR_${generateValidId()}`;
const value = 'test_value';
await n8n.variables.createVariableFromEmptyState(key, value);
const variableRow = n8n.variables.getVariableRow(key);
await expect(variableRow).toContainText(value);
await expect(variableRow).toBeVisible();
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
});
test('should create multiple variables', async ({ n8n }) => {
const key1 = `ENV_VAR_NEW_${generateValidId()}`;
const value1 = 'test_value_1';
await n8n.variables.createVariableFromEmptyState(key1, value1);
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
const key2 = `ENV_EXAMPLE_${generateValidId()}`;
const value2 = 'test_value_2';
await n8n.variables.createVariable(key2, value2);
await expect(n8n.variables.getVariablesRows()).toHaveCount(2);
const variableRow1 = n8n.variables.getVariableRow(key1);
await expect(variableRow1).toContainText(value1);
await expect(variableRow1).toBeVisible();
const variableRow2 = n8n.variables.getVariableRow(key2);
await expect(variableRow2).toContainText(value2);
await expect(variableRow2).toBeVisible();
});
test('should get validation errors and cancel variable creation', async ({ n8n }) => {
await n8n.variables.createVariableFromEmptyState(
`ENV_BASE_${generateValidId()}`,
'base_value',
);
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
const initialCount = await n8n.variables.getVariablesRows().count();
const key = `ENV_VAR_INVALID_${generateValidId()}$`; // Invalid key with special character
const value = 'test_value';
await n8n.variables.createVariable(key, value, { shouldSave: false });
const saveButton = n8n.variables.variableModal.getSaveButton();
await expect(saveButton).toBeDisabled();
await n8n.variables.variableModal.close();
await expect(n8n.variables.getVariablesRows()).toHaveCount(initialCount);
});
test('should edit a variable', async ({ n8n }) => {
const key = `ENV_VAR_EDIT_${generateValidId()}`;
const initialValue = 'initial_value';
await n8n.variables.createVariableFromEmptyState(key, initialValue);
const newValue = 'updated_value';
await n8n.variables.editVariable(key, newValue, { shouldSave: true });
const variableRow = n8n.variables.getVariableRow(key);
await expect(variableRow).toContainText(newValue);
await expect(variableRow).toBeVisible();
});
test('should delete a variable', async ({ n8n }) => {
const key = `TO_DELETE_${generateValidId()}`;
const value = 'delete_test_value';
await n8n.variables.createVariableFromEmptyState(key, value);
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
const initialCount = await n8n.variables.getVariablesRows().count();
await n8n.variables.deleteVariable(key);
await expect(n8n.variables.getVariablesRows()).toHaveCount(initialCount - 1);
await expect(n8n.variables.getVariableRow(key)).toBeHidden();
});
test('should search for a variable', async ({ n8n }) => {
const uniqueId = generateValidId();
const key1 = `SEARCH_VAR_${uniqueId}`;
const key2 = `SEARCH_VAR_NEW_${uniqueId}`;
const key3 = `SEARCH_EXAMPLE_${uniqueId}`;
await n8n.variables.createVariableFromEmptyState(key1, 'search_value_1');
await n8n.variables.createVariable(key2, 'search_value_2');
await n8n.variables.createVariable(key3, 'search_value_3');
await n8n.variables.getSearchBar().fill('NEW_');
await n8n.variables.getSearchBar().press('Enter');
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
await expect(n8n.page).toHaveURL(new RegExp('search=NEW_'));
await n8n.variables.getSearchBar().clear();
await n8n.variables.getSearchBar().fill('SEARCH_VAR_');
await n8n.variables.getSearchBar().press('Enter');
await expect(n8n.variables.getVariablesRows()).toHaveCount(2);
await expect(n8n.variables.getVariableRow(key1)).toBeVisible();
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
await expect(n8n.page).toHaveURL(new RegExp('search=SEARCH_VAR_'));
await n8n.variables.getSearchBar().clear();
await n8n.variables.getSearchBar().fill('SEARCH_');
await n8n.variables.getSearchBar().press('Enter');
await expect(n8n.variables.getVariablesRows()).toHaveCount(3);
await expect(n8n.variables.getVariableRow(key1)).toBeVisible();
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
await expect(n8n.variables.getVariableRow(key3)).toBeVisible();
await expect(n8n.page).toHaveURL(new RegExp('search=SEARCH_'));
await n8n.variables.getSearchBar().clear();
await n8n.variables.getSearchBar().fill(`NonExistent_${generateValidId()}`);
await n8n.variables.getSearchBar().press('Enter');
await expect(n8n.variables.getVariablesRows()).toBeHidden();
await expect(n8n.page).toHaveURL(/search=NonExistent_/);
await expect(n8n.variables.getNoVariablesFoundMessage()).toBeVisible();
});
});
});
@@ -0,0 +1,45 @@
import { expect, test } from '../../../../fixtures/base';
test.use({ capability: 'external-secrets' });
test.setTimeout(180_000);
test.describe(
'AWS Secrets Manager with LocalStack @capability:external-secrets @licensed',
{
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
},
() => {
const PROVIDER_NAME = 'awsSecretsManager';
const PROVIDER_SETTINGS = {
region: 'us-east-1',
authMethod: 'iamUser',
accessKeyId: 'test',
secretAccessKey: 'test',
};
test.beforeEach(async ({ n8n, services }) => {
await services.localstack.secretsManager.clear();
await n8n.api.enableFeature('externalSecrets');
});
test('can configure, connect, and sync secrets from LocalStack', async ({ n8n, services }) => {
const { secretsManager } = services.localstack;
await secretsManager.createSecret('api-key', 'secret-123');
await n8n.api.externalSecrets.saveProviderSettings(PROVIDER_NAME, PROVIDER_SETTINGS);
await n8n.api.externalSecrets.testProvider(PROVIDER_NAME, PROVIDER_SETTINGS);
await n8n.api.externalSecrets.connectProvider(PROVIDER_NAME);
await n8n.api.externalSecrets.updateProvider(PROVIDER_NAME);
expect(await n8n.api.externalSecrets.getSecrets(PROVIDER_NAME)).toContain('api-key');
await secretsManager.createSecret('new-secret', 'value-2');
await n8n.api.externalSecrets.updateProvider(PROVIDER_NAME);
const secrets = await n8n.api.externalSecrets.getSecrets(PROVIDER_NAME);
expect(secrets).toContain('api-key');
expect(secrets).toContain('new-secret');
});
},
);
@@ -0,0 +1,71 @@
import { expect, test } from '../../../../fixtures/base';
test.use({ capability: 'external-secrets' });
// LocalStack can take time to start up
test.setTimeout(180_000);
test.describe(
'Secret Providers Connections with LocalStack @capability:external-secrets @licensed',
{
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
},
() => {
const PROVIDER_KEY = 'aws-localstack-e2e';
const PROVIDER_TYPE = 'awsSecretsManager';
test.beforeEach(async ({ n8n, services }) => {
// N8N_ENV_FEAT_EXTERNAL_SECRETS_FOR_PROJECTS is set at container startup
// via the external-secrets capability config
// Enable the external secrets license feature
await n8n.api.enableFeature('externalSecrets');
// Clear any existing secrets from previous tests
await services.localstack.secretsManager.clear();
});
test.afterEach(async ({ n8n }) => {
// Clean up: delete the test connection if it exists
try {
await n8n.api.externalSecrets.deleteConnection(PROVIDER_KEY);
} catch {
// Ignore errors if connection doesn't exist
}
});
test('can create a connection pointing to LocalStack', async ({ n8n, services }) => {
// Arrange: Seed secrets in LocalStack
await services.localstack.secretsManager.createSecret('e2e-api-key', 'secret-123');
await services.localstack.secretsManager.createSecret(
'e2e-db-credentials',
JSON.stringify({ username: 'admin', password: 'hunter2' }),
);
// Verify secrets exist in LocalStack
const secrets = await services.localstack.secretsManager.listSecrets();
expect(secrets).toContain('e2e-api-key');
expect(secrets).toContain('e2e-db-credentials');
// Act: Create a connection with settings that would work with LocalStack
// (n8n container has AWS_ENDPOINT_URL set to point to LocalStack)
const created = await n8n.api.externalSecrets.createConnection({
providerKey: PROVIDER_KEY,
type: PROVIDER_TYPE,
projectIds: [],
settings: {
region: 'us-east-1',
authMethod: 'iamUser',
accessKeyId: 'test',
secretAccessKey: 'test',
},
});
// Assert: Connection created successfully
expect(created.name).toBe(PROVIDER_KEY);
expect(created.type).toBe(PROVIDER_TYPE);
// TODO - this test should verify that the secrets are loaded - but that functionality is not there yet
});
},
);
@@ -0,0 +1,89 @@
/**
* E2E tests for log streaming to VictoriaLogs via syslog.
*
* These tests verify that n8n log streaming events are correctly
* sent to VictoriaLogs and can be queried using LogsQL.
*
* Prerequisites:
* - Log streaming feature enabled (enterprise license)
* - @capability:observability tag to bring up VictoriaLogs
*/
import { test, expect } from '../../../../fixtures/base';
// Worker-scoped fixtures must be at top level
test.use({ capability: 'observability' });
test.describe('Log Streaming to VictoriaLogs @capability:observability', {
annotation: [
{ type: 'owner', description: 'Lifecycle & Governance' },
],
}, () => {
test.beforeEach(async ({ n8n }) => {
// Enable log streaming feature for the test
await n8n.api.enableFeature('logStreaming');
});
test('should configure syslog destination and send test message', async ({ api, services }) => {
const obs = services.observability;
// Configure syslog destination pointing to VictoriaLogs
// syslog contains: host, port, protocol, facility, appName
const destination = await api.createSyslogDestination({
host: obs.syslog.host,
port: obs.syslog.port,
protocol: obs.syslog.protocol,
facility: obs.syslog.facility,
app_name: obs.syslog.appName,
label: 'VictoriaLogs Test Destination',
});
expect(destination.id).toBeDefined();
console.log(`Created syslog destination with ID: ${destination.id}`);
// Send test message to the destination
const testResult = await api.testLogStreamingDestination(destination.id);
expect(testResult).toBe(true);
// Wait for the test message to appear in VictoriaLogs
// Use wildcard - LogsQL interprets dots as word separators
const logEntry = await obs.logs.waitForLog('*destination.test*', {
timeoutMs: 30000,
start: '-1m',
});
expect(logEntry).toBeTruthy();
// Clean up - delete the destination
await api.deleteLogStreamingDestination(destination.id);
});
test('should query metrics from VictoriaMetrics', async ({ api, services }) => {
const obs = services.observability;
// Import and activate a webhook workflow to generate metrics
const { webhookPath, workflowId } = await api.workflows.importWorkflowFromFile(
'simple-webhook-test.json',
);
// Trigger the workflow via webhook to generate metrics
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`, {
method: 'POST',
data: { test: 'metrics' },
});
expect(webhookResponse.ok()).toBe(true);
// Wait for workflow execution to complete
const execution = await api.workflows.waitForExecution(workflowId, 10000);
expect(execution.status).toBe('success');
// Wait for metrics to be scraped (VictoriaMetrics scrapes every 5s)
// Query for n8n version info metric (always present)
const versionMetric = await obs.metrics.waitForMetric('n8n_version_info', {
timeoutMs: 30000,
});
expect(versionMetric).toBeTruthy();
console.log('n8n version metric:', versionMetric?.labels);
});
});
@@ -0,0 +1,51 @@
/**
* End-to-end UI test for log streaming feature.
*
* This test verifies:
* 1. Log streaming can be configured via the UI
* 2. Test events are streamed to VictoriaLogs via syslog
* 3. Events can be queried from VictoriaLogs
*/
import { test, expect } from '../../../../fixtures/base';
test.use({ capability: 'observability' });
test.describe('Log Streaming UI E2E @capability:observability', {
annotation: [
{ type: 'owner', description: 'Lifecycle & Governance' },
],
}, () => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.enableFeature('logStreaming');
});
test('should configure syslog destination via UI and send test event', async ({
n8n,
services,
}) => {
const obs = services.observability;
// ========== STEP 1: Configure Log Streaming via UI ==========
await n8n.navigate.toLogStreaming();
await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).toBeVisible();
// Create syslog destination pointing to VictoriaLogs
await n8n.settingsLogStreaming.createSyslogDestination({
name: 'VictoriaLogs E2E Test',
host: obs.syslog.host,
port: obs.syslog.port,
});
// Send test event
await n8n.settingsLogStreaming.sendTestEvent();
// ========== STEP 3: Verify Event in VictoriaLogs ==========
// Use wildcard search - LogsQL interprets dots as word separators
const testEvent = await obs.logs.waitForLog('*destination.test*', {
timeoutMs: 30000,
start: '-2m',
});
expect(testEvent).toBeTruthy();
});
});
@@ -0,0 +1,91 @@
import { test, expect } from '../../../../fixtures/base';
const DESTINATION_NAMES = {
FIRST: 'Destination 0',
SECOND: 'Destination 1',
} as const;
const MODAL_MAX_WIDTH = 500;
test.describe('Log Streaming Settings', {
annotation: [
{ type: 'owner', description: 'Lifecycle & Governance' },
],
}, () => {
test.describe.configure({ mode: 'serial' });
test.describe('unlicensed', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.disableFeature('logStreaming');
});
test('should show the unlicensed view when the feature is disabled', async ({ n8n }) => {
await n8n.navigate.toLogStreaming();
await expect(n8n.settingsLogStreaming.getActionBoxUnlicensed()).toBeVisible();
await expect(n8n.settingsLogStreaming.getContactUsButton()).toBeVisible();
await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).not.toBeAttached();
});
});
// @licensed - requires enterprise license (module routes only exist with license at startup)
test.describe('licensed @licensed', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.enableFeature('logStreaming');
await n8n.api.deleteAllLogStreamingDestinations();
await n8n.navigate.toLogStreaming();
});
test('should show the licensed view when the feature is enabled', async ({ n8n }) => {
await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).toBeVisible();
await expect(n8n.settingsLogStreaming.getAddFirstDestinationButton()).toBeVisible();
await expect(n8n.settingsLogStreaming.getActionBoxUnlicensed()).not.toBeAttached();
});
test('should show the add destination modal', async ({ n8n }) => {
await n8n.settingsLogStreaming.addDestination();
await expect(n8n.settingsLogStreaming.getDestinationModal()).toBeVisible();
await expect(n8n.settingsLogStreaming.getSelectDestinationType()).toBeVisible();
await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeVisible();
await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeDisabled();
const modal = n8n.settingsLogStreaming.getDestinationModal();
const width = await modal.evaluate((element) => {
return parseInt(window.getComputedStyle(element).width.replace('px', ''));
});
expect(width).toBeLessThan(MODAL_MAX_WIDTH);
await n8n.settingsLogStreaming.clickSelectDestinationType();
await n8n.settingsLogStreaming.selectDestinationType(0);
await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeEnabled();
await n8n.settingsLogStreaming.closeModalByClickingOverlay();
await expect(n8n.settingsLogStreaming.getDestinationModal()).not.toBeAttached();
});
test('should create a destination and delete it', async ({ n8n }) => {
await n8n.settingsLogStreaming.createDestination(DESTINATION_NAMES.FIRST);
await n8n.page.reload();
await n8n.settingsLogStreaming.clickDestinationCard(0);
await expect(n8n.settingsLogStreaming.getDestinationDeleteButton()).toBeVisible();
await n8n.settingsLogStreaming.deleteDestination();
await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible();
await n8n.settingsLogStreaming.cancelDialog();
await n8n.settingsLogStreaming.deleteDestination();
await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible();
await n8n.settingsLogStreaming.confirmDialog();
});
test('should create a destination and delete it via card actions', async ({ n8n }) => {
await n8n.settingsLogStreaming.createDestination(DESTINATION_NAMES.SECOND);
await n8n.page.reload();
await n8n.settingsLogStreaming.clickDestinationCardDropdown(0);
await n8n.settingsLogStreaming.clickDropdownMenuItem(0);
await expect(n8n.settingsLogStreaming.getDestinationSaveButton()).not.toBeAttached();
await n8n.settingsLogStreaming.closeModalByClickingOverlay();
await n8n.settingsLogStreaming.clickDestinationCardDropdown(0);
await n8n.settingsLogStreaming.clickDropdownMenuItem(1);
await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible();
await n8n.settingsLogStreaming.confirmDialog();
});
});
});
@@ -0,0 +1,61 @@
import { test, expect } from '../../../../fixtures/base';
const INVALID_NAMES = [
'https://n8n.io',
'http://n8n.io',
'www.n8n.io',
'n8n.io',
'n8n.бг',
'n8n.io/home',
'n8n.io/home?send=true',
'<a href="#">Jack</a>',
'<script>alert("Hello")</script>',
];
const VALID_NAMES = [
['a', 'a'],
['alice', 'alice'],
['Robert', 'Downey Jr.'],
['Mia', 'Mia-Downey'],
['Mark', "O'neil"],
['Thomas', 'Müler'],
['ßáçøñ', 'ßáçøñ'],
['أحمد', 'فلسطين'],
['Милорад', 'Филиповић'],
];
test.describe(
'Personal Settings',
{
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},
() => {
test('should allow to change first and last name', async ({ n8n }) => {
await n8n.settingsPersonal.goto();
for (const name of VALID_NAMES) {
await n8n.settingsPersonal.fillPersonalData(name[0], name[1]);
await n8n.settingsPersonal.saveSettings();
await expect(
n8n.notifications.getNotificationByTitleOrContent('Personal details updated'),
).toBeVisible();
await n8n.notifications.closeNotificationByText('Personal details updated');
}
});
test('should not allow malicious values for personal data', async ({ n8n }) => {
await n8n.settingsPersonal.goto();
for (const name of INVALID_NAMES) {
await n8n.settingsPersonal.fillPersonalData(name, name);
await n8n.settingsPersonal.saveSettings();
await expect(
n8n.notifications.getNotificationByTitleOrContent('Problem updating your details'),
).toBeVisible();
await n8n.notifications.closeNotificationByText('Problem updating your details');
}
});
},
);
@@ -0,0 +1,112 @@
import { authenticator } from 'otplib';
import { INSTANCE_OWNER_CREDENTIALS } from '../../../../config/test-users';
import { test, expect } from '../../../../fixtures/base';
test.use({ capability: { env: { TEST_ISOLATION: 'two-factor-auth' } } });
const TEST_DATA = {
NEW_EMAIL: 'newemail@test.com',
NEW_FIRST_NAME: 'newFirstName',
NEW_LAST_NAME: 'newLastName',
};
const NOTIFICATIONS = {
PERSONAL_DETAILS_UPDATED: 'Personal details updated',
};
const { email, password, mfaSecret, mfaRecoveryCodes } = INSTANCE_OWNER_CREDENTIALS;
const RECOVERY_CODE = mfaRecoveryCodes![0];
test.describe(
'Two-factor authentication @auth:none @db:reset',
{
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},
() => {
test.describe.configure({ mode: 'serial' });
test('Should be able to login with MFA code', async ({ n8n }) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.sideBar.signOutFromWorkflows();
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
await expect(n8n.page).toHaveURL(/workflows/);
});
test('Should be able to login with MFA recovery code', async ({ n8n }) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.sideBar.signOutFromWorkflows();
await n8n.mfaComposer.loginWithMfaRecoveryCode(email, password, RECOVERY_CODE);
await expect(n8n.page).toHaveURL(/workflows/);
});
test('Should be able to disable MFA in account with MFA code', async ({ n8n }) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.sideBar.signOutFromWorkflows();
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
const disableToken = authenticator.generate(mfaSecret!);
await n8n.settingsPersonal.triggerDisableMfa();
await n8n.settingsPersonal.fillMfaCodeAndSave(disableToken);
await expect(n8n.settingsPersonal.getEnableMfaButton()).toBeVisible();
});
test('Should prompt for MFA code when email changes', async ({ n8n }) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.settingsPersonal.goto();
await n8n.settingsPersonal.fillEmail(TEST_DATA.NEW_EMAIL);
await n8n.settingsPersonal.pressEnterOnEmail();
const mfaCode = authenticator.generate(mfaSecret!);
await n8n.settingsPersonal.fillMfaCodeAndSave(mfaCode);
await expect(
n8n.notifications.getNotificationByTitleOrContent(NOTIFICATIONS.PERSONAL_DETAILS_UPDATED),
).toBeVisible();
});
test('Should prompt for MFA recovery code when email changes', async ({ n8n }) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.settingsPersonal.goto();
await n8n.settingsPersonal.fillEmail(TEST_DATA.NEW_EMAIL);
await n8n.settingsPersonal.pressEnterOnEmail();
await expect(n8n.settingsPersonal.getMfaCodeOrRecoveryCodeInput()).toBeVisible();
});
test('Should not prompt for MFA code or recovery code when first name or last name changes', async ({
n8n,
}) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.settingsPersonal.updateFirstAndLastName(
TEST_DATA.NEW_FIRST_NAME,
TEST_DATA.NEW_LAST_NAME,
);
await expect(
n8n.notifications.getNotificationByTitleOrContent(NOTIFICATIONS.PERSONAL_DETAILS_UPDATED),
).toBeVisible();
});
test('Should be able to disable MFA in account with recovery code', async ({ n8n }) => {
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
await n8n.sideBar.signOutFromWorkflows();
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
await n8n.settingsPersonal.triggerDisableMfa();
await n8n.settingsPersonal.fillMfaCodeAndSave(RECOVERY_CODE);
await expect(n8n.settingsPersonal.getEnableMfaButton()).toBeVisible();
});
},
);
@@ -0,0 +1,57 @@
import { INSTANCE_OWNER_CREDENTIALS } from '../../../../config/test-users';
import { test, expect } from '../../../../fixtures/base';
test.describe('Users Settings', {
annotation: [
{ type: 'owner', description: 'Identity & Access' },
],
}, () => {
test('should prevent non-owners to access UM settings', async ({ n8n }) => {
// This creates a new user in the same context, so the cookies are refreshed and owner is no longer logged in
await n8n.api.users.create();
await n8n.navigate.toUsers();
await expect.poll(() => n8n.page.url()).not.toContain('/settings/users');
});
test('should allow instance owner to access UM settings', async ({ n8n }) => {
await n8n.navigate.toUsers();
expect(n8n.page.url()).toContain('/settings/users');
});
test('should be able to change user role to Admin and back', async ({ n8n, api }) => {
const user = await api.users.create();
await n8n.navigate.toUsers();
await n8n.settingsUsers.search(user.email);
await n8n.settingsUsers.selectAccountType(user.email, 'Admin');
await expect(n8n.settingsUsers.getAccountType(user.email)).toHaveText('Admin');
await n8n.settingsUsers.selectAccountType(user.email, 'Member');
await expect(n8n.settingsUsers.getAccountType(user.email)).toHaveText('Member');
});
test('should delete user and their data', async ({ n8n, api }) => {
const user = await api.users.create();
await n8n.navigate.toUsers();
await n8n.page.reload();
await n8n.settingsUsers.search(user.email);
await expect(n8n.settingsUsers.getRow(user.email)).toBeVisible();
await n8n.settingsUsers.clickDeleteUser(user.email);
await n8n.settingsUsers.deleteData();
await expect(n8n.notifications.getNotificationByTitleOrContent('User deleted')).toBeVisible();
});
test('should delete user and transfer their data', async ({ n8n, api }) => {
const ownerEmail = INSTANCE_OWNER_CREDENTIALS.email;
const user = await api.users.create();
await n8n.navigate.toUsers();
await n8n.page.reload();
await n8n.settingsUsers.search(user.email);
await n8n.settingsUsers.getRow(user.email).isVisible();
await n8n.settingsUsers.clickDeleteUser(user.email);
await n8n.settingsUsers.transferData(ownerEmail);
await expect(n8n.notifications.getNotificationByTitleOrContent('User deleted')).toBeVisible();
});
});
@@ -0,0 +1,46 @@
import { test, expect } from '../../../../fixtures/base';
test.describe
.serial('Worker View', () => {
test.describe(
'unlicensed',
{
annotation: [{ type: 'owner', description: 'Catalysts' }],
},
() => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.disableFeature('workerView');
await n8n.api.disableFeature('workerView');
await n8n.api.setQueueMode(false);
});
test('should not show up in the menu sidebar', async ({ n8n }) => {
await n8n.workerView.goto();
await expect(n8n.workerView.getWorkerMenuItem()).toBeHidden();
});
test('should show action box', async ({ n8n }) => {
await n8n.workerView.goto();
await expect(n8n.workerView.getWorkerViewUnlicensed()).toBeVisible();
});
},
);
test.describe('licensed', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.enableFeature('workerView');
await n8n.api.setQueueMode(true);
});
test('should show up in the menu sidebar', async ({ n8n }) => {
await n8n.goHome();
await n8n.workerView.goto();
await expect(n8n.workerView.getWorkerMenuItem()).toBeVisible();
});
test('should show worker list view', async ({ n8n }) => {
await n8n.workerView.goto();
await expect(n8n.workerView.getWorkerViewLicensed()).toBeVisible();
});
});
});