mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-01 15:47:41 +08:00
test: Add E2E Playwright coverage for redaction enforcement (#31963)
This commit is contained in:
@@ -105,6 +105,16 @@ export class NavigationHelper {
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific execution within a workflow
|
||||
* URLs:
|
||||
* - Existing workflow: /workflow/{workflowId}/executions/{executionId}
|
||||
*/
|
||||
async toExecution(workflowId: string, executionId: string): Promise<void> {
|
||||
const url = `/workflow/${workflowId}/executions/${executionId}`;
|
||||
await this.page.goto(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific folder
|
||||
* URL: /projects/{projectId}/folders/{folderId}/workflows or /home/folders/{folderId}/workflows
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
import { LogsPanel } from './components/LogsPanel';
|
||||
import { RunDataPanel } from './components/RunDataPanel';
|
||||
|
||||
export class ExecutionsPage extends BasePage {
|
||||
async goto(projectId?: string) {
|
||||
@@ -10,6 +11,7 @@ export class ExecutionsPage extends BasePage {
|
||||
}
|
||||
|
||||
readonly logsPanel = new LogsPanel(this.getPreviewIframe().getByTestId('logs-panel'));
|
||||
readonly outputPanel = new RunDataPanel(this.getPreviewIframe().getByTestId('output-panel'));
|
||||
|
||||
async clickDebugInEditorButton(): Promise<void> {
|
||||
await this.clickButtonByName('Debug in editor');
|
||||
@@ -113,6 +115,12 @@ export class ExecutionsPage extends BasePage {
|
||||
await this.getFilterButton().click();
|
||||
}
|
||||
|
||||
async openNodeExecutionDetails(name: string): Promise<void> {
|
||||
await this.getPreviewIframe()
|
||||
.locator(`[data-test-id="canvas-node"][data-node-name="${name}"]`)
|
||||
.dblclick();
|
||||
}
|
||||
|
||||
getFilterBadge(): Locator {
|
||||
return this.page.getByTestId('execution-filter-badge');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
type RedactionScope = 'production' | 'all';
|
||||
|
||||
const SCOPE_OPTION_LABEL: Record<RedactionScope, string> = {
|
||||
production: 'Production executions (Recommended)',
|
||||
all: 'Manual and production executions',
|
||||
};
|
||||
|
||||
export class SecuritySettingsPage extends BasePage {
|
||||
async goto() {
|
||||
// Wait for the settings to load so all components are available. Attach the
|
||||
// listener before navigating, or the response can land before we wait for it.
|
||||
await Promise.all([
|
||||
this.waitForRestResponse('/rest/settings/security', 'GET'),
|
||||
this.page.goto('/settings/security'),
|
||||
]);
|
||||
}
|
||||
|
||||
getEnforcementToggle(): Locator {
|
||||
return this.page.getByTestId('enable-redaction-enforcement');
|
||||
}
|
||||
|
||||
getEnforcementScopeSelect(): Locator {
|
||||
return this.page.getByTestId('redaction-enforcement-scope-select');
|
||||
}
|
||||
|
||||
getEnforcementSummary(): Locator {
|
||||
return this.page.getByTestId('redaction-enforcement-summary');
|
||||
}
|
||||
|
||||
private getConfirmDialog(): Locator {
|
||||
return this.page.getByRole('dialog');
|
||||
}
|
||||
|
||||
async enableEnforcement(): Promise<void> {
|
||||
await this.getEnforcementToggle().click();
|
||||
await this.getConfirmDialog().getByRole('button', { name: 'Enable' }).click();
|
||||
}
|
||||
|
||||
async disableEnforcement(): Promise<void> {
|
||||
await this.getEnforcementToggle().click();
|
||||
await this.getConfirmDialog().getByRole('button', { name: 'Disable' }).click();
|
||||
}
|
||||
|
||||
async selectScope(scope: RedactionScope): Promise<void> {
|
||||
await this.getEnforcementScopeSelect().click();
|
||||
await this.page.getByRole('option', { name: SCOPE_OPTION_LABEL[scope] }).click();
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,50 @@ export class WorkflowSettingsModal extends BasePage {
|
||||
await this.getUnpublishModal().getByRole('button', { name: 'Unpublish' }).click();
|
||||
}
|
||||
|
||||
getRedactionPolicyRow(): Locator {
|
||||
return this.container.getByTestId('workflow-settings-redaction-policy');
|
||||
}
|
||||
|
||||
getRedactProductionSelect(): Locator {
|
||||
return this.container.getByTestId('workflow-settings-redact-production-select');
|
||||
}
|
||||
|
||||
getRedactManualSelect(): Locator {
|
||||
return this.container.getByTestId('workflow-settings-redact-manual-select');
|
||||
}
|
||||
|
||||
getRedactProductionInput(): Locator {
|
||||
return this.getRedactProductionSelect().locator('input');
|
||||
}
|
||||
|
||||
getRedactManualInput(): Locator {
|
||||
return this.getRedactManualSelect().locator('input');
|
||||
}
|
||||
|
||||
async hoverRedactProductionSelect(): Promise<void> {
|
||||
await this.getRedactProductionSelect().hover();
|
||||
}
|
||||
|
||||
async hoverRedactManualSelect(): Promise<void> {
|
||||
await this.getRedactManualSelect().hover();
|
||||
}
|
||||
|
||||
async selectProductionRedactMode(mode: string): Promise<void> {
|
||||
await this.getRedactProductionSelect().click();
|
||||
await this.page.getByRole('option', { name: mode, exact: true }).click();
|
||||
}
|
||||
|
||||
async selectManualRedactMode(mode: string): Promise<void> {
|
||||
await this.getRedactManualSelect().click();
|
||||
await this.page.getByRole('option', { name: mode, exact: true }).click();
|
||||
}
|
||||
|
||||
getTooltip(): Locator {
|
||||
return this.page.getByTestId('tooltip-content');
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.container.getByRole('button', { name: 'Save' });
|
||||
return this.page.getByTestId('workflow-settings-save-button').getByRole('button');
|
||||
}
|
||||
|
||||
getDuplicateModal(): Locator {
|
||||
@@ -114,6 +156,12 @@ export class WorkflowSettingsModal extends BasePage {
|
||||
async open(): Promise<void> {
|
||||
await this.getWorkflowMenu().click();
|
||||
await this.getSettingsMenuItem().click();
|
||||
await this.waitUntilLoaded();
|
||||
}
|
||||
|
||||
private async waitUntilLoaded(): Promise<void> {
|
||||
// `v-loading` directive's class
|
||||
await this.container.locator('.el-loading-mask').waitFor({ state: 'detached' });
|
||||
}
|
||||
|
||||
async clickSave(): Promise<void> {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
import { OAuthConsentPage } from './OAuthConsentPage';
|
||||
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
import { SecretsProviderSettingsPage } from './SecretsProviderSettingsPage';
|
||||
import { SecuritySettingsPage } from './SecuritySettingsPage';
|
||||
import { SettingsEnvironmentPage } from './SettingsEnvironmentPage';
|
||||
import { SettingsLogStreamingPage } from './SettingsLogStreamingPage';
|
||||
import { SettingsPersonalPage } from './SettingsPersonalPage';
|
||||
@@ -115,6 +116,7 @@ export class n8nPage {
|
||||
|
||||
readonly settingsEnvironment: SettingsEnvironmentPage;
|
||||
readonly secretsProviderSettings: SecretsProviderSettingsPage;
|
||||
readonly securitySettings: SecuritySettingsPage;
|
||||
|
||||
// Modals
|
||||
readonly workflowActivationModal: WorkflowActivationModal;
|
||||
@@ -189,6 +191,7 @@ export class n8nPage {
|
||||
this.dataTableDetails = new DataTableDetails(page);
|
||||
this.settingsEnvironment = new SettingsEnvironmentPage(page);
|
||||
this.secretsProviderSettings = new SecretsProviderSettingsPage(page);
|
||||
this.securitySettings = new SecuritySettingsPage(page);
|
||||
|
||||
this.settingsUsers = new SettingsUsersPage(page);
|
||||
this.settingsSso = new SettingsSsoPage(page);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { McpOAuthApiHelper } from './mcp-oauth-api-helper';
|
||||
import { ProjectApiHelper } from './project-api-helper';
|
||||
import { PublicApiHelper } from './public-api-helper';
|
||||
import { RoleApiHelper } from './role-api-helper';
|
||||
import { SecuritySettingsApiHelper } from './security-settings-api-helper';
|
||||
import { SourceControlApiHelper } from './source-control-api-helper';
|
||||
import { TagApiHelper } from './tag-api-helper';
|
||||
import { UserApiHelper, type TestUser } from './user-api-helper';
|
||||
@@ -78,6 +79,7 @@ export class ApiHelpers {
|
||||
tags: TagApiHelper;
|
||||
roles: RoleApiHelper;
|
||||
sourceControl: SourceControlApiHelper;
|
||||
securitySettings: SecuritySettingsApiHelper;
|
||||
|
||||
publicApi: PublicApiHelper;
|
||||
|
||||
@@ -96,6 +98,7 @@ export class ApiHelpers {
|
||||
this.tags = new TagApiHelper(this);
|
||||
this.roles = new RoleApiHelper(this);
|
||||
this.sourceControl = new SourceControlApiHelper(this);
|
||||
this.securitySettings = new SecuritySettingsApiHelper(this);
|
||||
|
||||
this.publicApi = new PublicApiHelper(this);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { RedactionFloor } from '@n8n/api-types';
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
import { TestError } from '../Types';
|
||||
|
||||
/**
|
||||
* Helpers for the instance Security & Policies settings, focused on the
|
||||
* redaction-enforcement floor (`off` / `production` / `all`).
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /rest/settings/security
|
||||
* - POST /rest/settings/security
|
||||
*
|
||||
* Both are gated by the `feat:personalSpacePolicy` license and the
|
||||
* `securitySettings:manage` global scope, so enable that license before use.
|
||||
*/
|
||||
export class SecuritySettingsApiHelper {
|
||||
constructor(private readonly api: ApiHelpers) {}
|
||||
|
||||
async getRedactionFloor(): Promise<RedactionFloor | undefined> {
|
||||
const response = await this.api.request.get('/rest/settings/security');
|
||||
if (!response.ok()) {
|
||||
throw new TestError(
|
||||
`GET /rest/settings/security failed (${response.status()}): ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
const result = await response.json();
|
||||
const settings = result.data ?? result;
|
||||
return settings.redactionEnforcement?.floor;
|
||||
}
|
||||
|
||||
async setRedactionFloor(floor: RedactionFloor): Promise<void> {
|
||||
const response = await this.api.request.post('/rest/settings/security', {
|
||||
data: { redactionEnforcement: { floor } },
|
||||
});
|
||||
|
||||
if (!response.ok()) {
|
||||
throw new TestError(
|
||||
`POST /rest/settings/security failed (${response.status()}): ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { APIResponse } from '@playwright/test';
|
||||
import { readFileSync } from 'fs';
|
||||
import type { IWorkflowBase, ExecutionSummary } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -118,6 +119,21 @@ export class WorkflowApiHelper {
|
||||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link update}, but returns the raw response instead of throwing on a
|
||||
* non-2xx status — for asserting a specific status code (e.g. the `422` from
|
||||
* the redaction floor-enforcement guard).
|
||||
*/
|
||||
async updateRaw(
|
||||
workflowId: string,
|
||||
versionId: string,
|
||||
data: Partial<IWorkflowBase>,
|
||||
): Promise<APIResponse> {
|
||||
return await this.api.request.patch(`/rest/workflows/${workflowId}`, {
|
||||
data: { ...data, versionId },
|
||||
});
|
||||
}
|
||||
|
||||
async deactivate(workflowId: string) {
|
||||
const response = await this.api.request.post(`/rest/workflows/${workflowId}/deactivate`);
|
||||
|
||||
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
|
||||
import { manualWorkflow, REDACT_OPTION, webhookWorkflow } from './redaction-helpers';
|
||||
import { expect, test } from '../../../fixtures/base';
|
||||
import type { ApiHelpers } from '../../../services/api-helper';
|
||||
|
||||
test.use({
|
||||
capability: {
|
||||
env: {
|
||||
TEST_ISOLATION: 'redaction-enforcement',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function runProductionExecution(
|
||||
api: ApiHelpers,
|
||||
settings?: Partial<IWorkflowBase['settings']>,
|
||||
) {
|
||||
const { workflowId, webhookPath, createdWorkflow } =
|
||||
await api.workflows.createWorkflowFromDefinition(webhookWorkflow(settings));
|
||||
|
||||
await api.workflows.activate(workflowId, createdWorkflow.versionId!);
|
||||
await api.webhooks.trigger(`/webhook/${webhookPath}`, { maxNotFoundRetries: 5 });
|
||||
const summary = await api.workflows.waitForExecution(workflowId, 15_000, 'webhook');
|
||||
|
||||
return { executionId: summary.id, workflowId, workflow: createdWorkflow };
|
||||
}
|
||||
|
||||
async function runManualExecution(api: ApiHelpers, settings?: Partial<IWorkflowBase['settings']>) {
|
||||
const createdWorkflow = await api.workflows.createWorkflow(manualWorkflow(settings));
|
||||
const result = await api.workflows.runManually(createdWorkflow.id, createdWorkflow.nodes[0].name);
|
||||
|
||||
return {
|
||||
workflowId: createdWorkflow.id,
|
||||
executionId: result.executionId,
|
||||
workflow: createdWorkflow,
|
||||
};
|
||||
}
|
||||
|
||||
test.describe(
|
||||
'Redaction enforcement',
|
||||
{ annotation: [{ type: 'owner', description: 'Enterprise Node & Partnerships' }] },
|
||||
() => {
|
||||
// The redaction floor is a single instance-global value, so these tests cannot
|
||||
// run in parallel against the shared instance without racing on it. Force serial
|
||||
// execution; each test then sets the floor it needs from a clean baseline.
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeEach(async ({ api }) => {
|
||||
await api.enableFeature('personalSpacePolicy');
|
||||
await api.enableFeature('dataRedaction');
|
||||
// Reset the instance-global floor to a known-clean value before each test.
|
||||
await api.securitySettings.setRedactionFloor('off');
|
||||
});
|
||||
|
||||
test('can be enabled and scoped from the Security & Policies UI', async ({ n8n }) => {
|
||||
await n8n.securitySettings.goto();
|
||||
|
||||
await n8n.securitySettings.enableEnforcement();
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Data redaction enforced'),
|
||||
).toBeVisible();
|
||||
await expect(n8n.securitySettings.getEnforcementSummary()).toContainText(
|
||||
'Production executions',
|
||||
);
|
||||
expect(await n8n.api.securitySettings.getRedactionFloor()).toBe('production');
|
||||
|
||||
await n8n.securitySettings.selectScope('all');
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Redaction scope updated'),
|
||||
).toBeVisible();
|
||||
await expect(n8n.securitySettings.getEnforcementSummary()).toContainText(
|
||||
'Manual and production executions',
|
||||
);
|
||||
expect(await n8n.api.securitySettings.getRedactionFloor()).toBe('all');
|
||||
});
|
||||
|
||||
test.describe('when floor is "production"', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.securitySettings.setRedactionFloor('production');
|
||||
});
|
||||
|
||||
test('locks only the production select in the workflow settings', async ({ n8n }) => {
|
||||
const workflow = await n8n.api.workflows.createWorkflow(webhookWorkflow());
|
||||
await n8n.navigate.toWorkflow(workflow.id);
|
||||
await n8n.workflowSettingsModal.open();
|
||||
|
||||
await expect(n8n.workflowSettingsModal.getRedactProductionInput()).toHaveValue('Redact');
|
||||
await expect(n8n.workflowSettingsModal.getRedactProductionInput()).toBeDisabled();
|
||||
|
||||
await n8n.workflowSettingsModal.hoverRedactProductionSelect();
|
||||
await expect(n8n.workflowSettingsModal.getTooltip()).toHaveText(
|
||||
/This option is enforced by your instance's redaction policy./,
|
||||
);
|
||||
|
||||
await expect(n8n.workflowSettingsModal.getRedactManualInput()).toBeEnabled();
|
||||
});
|
||||
|
||||
test('lets set stricter policy in the workflow settings', async ({ n8n }) => {
|
||||
const workflow = await n8n.api.workflows.createWorkflow(webhookWorkflow());
|
||||
await n8n.navigate.toWorkflow(workflow.id);
|
||||
|
||||
await n8n.workflowSettingsModal.open();
|
||||
await expect(n8n.workflowSettingsModal.getRedactManualInput()).toBeEnabled();
|
||||
|
||||
await n8n.workflowSettingsModal.selectManualRedactMode(REDACT_OPTION.redact);
|
||||
await n8n.workflowSettingsModal.clickSave();
|
||||
await expect(n8n.workflowSettingsModal.getModal()).toBeHidden();
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Workflow settings saved'),
|
||||
).toBeVisible();
|
||||
|
||||
const saved = await n8n.api.workflows.getWorkflow(workflow.id);
|
||||
expect(saved.settings?.redactionPolicy).toBe('all');
|
||||
});
|
||||
|
||||
test('redacts production executions', async ({ api, n8n }) => {
|
||||
const { executionId, workflowId, workflow } = await runProductionExecution(api);
|
||||
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
|
||||
test('leaves manual executions unredacted', async ({ api, n8n }) => {
|
||||
const { executionId, workflowId, workflow } = await runManualExecution(api);
|
||||
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/This is an item, but it's empty/,
|
||||
);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).not.toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects workflow redaction changes that fall below the floor', async ({ api }) => {
|
||||
const workflow = await api.workflows.createWorkflow(webhookWorkflow());
|
||||
|
||||
const current = await api.workflows.getWorkflow(workflow.id);
|
||||
const rejected = await api.workflows.updateRaw(workflow.id, current.versionId!, {
|
||||
settings: { ...current.settings, redactionPolicy: 'none' },
|
||||
});
|
||||
expect(rejected.status()).toBe(422);
|
||||
|
||||
const refreshed = await api.workflows.getWorkflow(workflow.id);
|
||||
const accepted = await api.workflows.updateRaw(workflow.id, refreshed.versionId!, {
|
||||
settings: { ...refreshed.settings, redactionPolicy: 'all' },
|
||||
});
|
||||
expect(accepted.ok()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('when floor is "all"', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.securitySettings.setRedactionFloor('all');
|
||||
});
|
||||
|
||||
test('locks both selects in the workflow settings', async ({ n8n }) => {
|
||||
const workflow = await n8n.api.workflows.createWorkflow(webhookWorkflow());
|
||||
await n8n.navigate.toWorkflow(workflow.id);
|
||||
await n8n.workflowSettingsModal.open();
|
||||
|
||||
await expect(n8n.workflowSettingsModal.getRedactProductionInput()).toHaveValue('Redact');
|
||||
await expect(n8n.workflowSettingsModal.getRedactManualInput()).toHaveValue('Redact');
|
||||
await expect(n8n.workflowSettingsModal.getRedactProductionInput()).toBeDisabled();
|
||||
await expect(n8n.workflowSettingsModal.getRedactManualInput()).toBeDisabled();
|
||||
|
||||
await n8n.workflowSettingsModal.hoverRedactProductionSelect();
|
||||
await expect(n8n.workflowSettingsModal.getTooltip()).toHaveText(
|
||||
/This option is enforced by your instance's redaction policy./,
|
||||
);
|
||||
|
||||
await n8n.workflowSettingsModal.hoverRedactManualSelect();
|
||||
await expect(n8n.workflowSettingsModal.getTooltip()).toHaveText(
|
||||
/This option is enforced by your instance's redaction policy./,
|
||||
);
|
||||
});
|
||||
|
||||
test('redacts production executions', async ({ api, n8n }) => {
|
||||
const { executionId, workflowId, workflow } = await runProductionExecution(api);
|
||||
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
|
||||
test('redacts manual executions', async ({ api, n8n }) => {
|
||||
const { executionId, workflowId, workflow } = await runManualExecution(api);
|
||||
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('when floor is "off" and workflow redaction is "non-manual"', () => {
|
||||
test('redacts production executions', async ({ api, n8n }) => {
|
||||
const { executionId, workflowId, workflow } = await runProductionExecution(api, {
|
||||
redactionPolicy: 'non-manual',
|
||||
});
|
||||
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
|
||||
test('leaves manual executions unredacted', async ({ api, n8n }) => {
|
||||
const { executionId, workflowId, workflow } = await runManualExecution(api, {
|
||||
redactionPolicy: 'non-manual',
|
||||
});
|
||||
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/This is an item, but it's empty/,
|
||||
);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).not.toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('does not retroactively redact executions captured before the floor was raised', async ({
|
||||
api,
|
||||
n8n,
|
||||
}) => {
|
||||
const { executionId, workflowId, workflow } = await runProductionExecution(api);
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/executionMode[\s\S]*production/i,
|
||||
);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).not.toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
|
||||
await api.securitySettings.setRedactionFloor('all');
|
||||
await n8n.navigate.toExecution(workflowId, executionId);
|
||||
await n8n.executions.openNodeExecutionDetails(workflow.nodes[0].name);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).toHaveText(
|
||||
/executionMode[\s\S]*production/i,
|
||||
);
|
||||
await expect(n8n.executions.outputPanel.getDataContainer()).not.toHaveText(
|
||||
/Output data redacted/,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const REDACT_OPTION = {
|
||||
redact: 'Redact',
|
||||
default: 'Default - Do not redact',
|
||||
} as const;
|
||||
|
||||
const SAVE_ALL_SETTINGS = {
|
||||
saveManualExecutions: true,
|
||||
saveDataSuccessExecution: 'all',
|
||||
saveDataErrorExecution: 'all',
|
||||
} as const;
|
||||
|
||||
export function webhookWorkflow(
|
||||
settings: Partial<IWorkflowBase['settings']> = {},
|
||||
): Partial<IWorkflowBase> {
|
||||
const webhookId = nanoid();
|
||||
|
||||
return {
|
||||
name: `Redaction ${nanoid(8)}`,
|
||||
nodes: [
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Webhook',
|
||||
webhookId,
|
||||
parameters: {
|
||||
path: webhookId,
|
||||
options: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [0, 0] as [number, number],
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
settings: { ...SAVE_ALL_SETTINGS, ...settings },
|
||||
};
|
||||
}
|
||||
|
||||
export function manualWorkflow(
|
||||
settings: Partial<IWorkflowBase['settings']> = {},
|
||||
): Partial<IWorkflowBase> {
|
||||
return {
|
||||
name: `Manual ${nanoid(8)}`,
|
||||
nodes: [
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Manual',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0] as [number, number],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
settings: { ...SAVE_ALL_SETTINGS, ...settings },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user