mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
271 lines
9.3 KiB
TypeScript
271 lines
9.3 KiB
TypeScript
import type { Page } from '@playwright/test';
|
|
import type { ProxyServer } from 'n8n-containers/services/proxy';
|
|
import type { IWorkflowBase } from 'n8n-workflow';
|
|
|
|
import { test as base, expect } from '../../../fixtures/base';
|
|
import type { CredentialResponse } from '../../../services/credential-api-helper';
|
|
|
|
interface SlackBlock {
|
|
type: string;
|
|
elements?: Array<{ type: string; url?: string }>;
|
|
}
|
|
|
|
type SendAndWaitFixtures = {
|
|
slackCredential: CredentialResponse;
|
|
slackProxySetup: undefined;
|
|
};
|
|
|
|
const test = base.extend<SendAndWaitFixtures>({
|
|
slackProxySetup: [
|
|
async ({ services }, use) => {
|
|
await services.proxy.clearAllExpectations();
|
|
await services.proxy.createExpectation({
|
|
httpRequest: { method: 'POST', path: '/api/chat.postMessage' },
|
|
httpResponse: {
|
|
statusCode: 200,
|
|
headers: { 'Content-Type': ['application/json'] },
|
|
body: JSON.stringify({
|
|
ok: true,
|
|
channel: 'C12345678',
|
|
ts: '1234567890.123456',
|
|
message: { text: 'Approval Request' },
|
|
}),
|
|
},
|
|
times: { unlimited: true },
|
|
});
|
|
await use(undefined);
|
|
},
|
|
{ auto: true },
|
|
],
|
|
|
|
slackCredential: async ({ n8n }, use) => {
|
|
const credential = await n8n.api.credentials.createCredential({
|
|
name: `Slack Test ${crypto.randomUUID().slice(0, 8)}`,
|
|
type: 'slackApi',
|
|
data: { accessToken: 'xoxb-fake-token-for-testing' },
|
|
});
|
|
await use(credential);
|
|
await n8n.api.credentials.deleteCredential(credential.id);
|
|
},
|
|
});
|
|
|
|
const NOT_A_BOT_USER_AGENT =
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0';
|
|
|
|
function getBlocksFromBody(body: { blocks?: SlackBlock[] } | undefined): SlackBlock[] | null {
|
|
return body?.blocks ?? null;
|
|
}
|
|
|
|
function extractApprovalUrls(blocks: SlackBlock[]): { approveUrl?: string; rejectUrl?: string } {
|
|
const buttons = blocks
|
|
.filter((b) => b.type === 'actions' && b.elements)
|
|
.flatMap((b) => b.elements?.filter((el) => el.type === 'button' && el.url) ?? []);
|
|
|
|
return {
|
|
approveUrl: buttons.find((btn) => btn.url?.includes('approved=true'))?.url,
|
|
rejectUrl: buttons.find((btn) => btn.url?.includes('approved=false'))?.url,
|
|
};
|
|
}
|
|
|
|
function extractFormUrl(blocks: SlackBlock[]): string | undefined {
|
|
const buttons = blocks
|
|
.filter((b) => b.type === 'actions' && b.elements)
|
|
.flatMap((b) => b.elements?.filter((el) => el.type === 'button' && el.url) ?? []);
|
|
|
|
// For custom forms, there's a single button with the form URL (contains signature)
|
|
return buttons.find((btn) => btn.url?.includes('signature='))?.url;
|
|
}
|
|
|
|
async function waitForSlackRequest(proxyServer: ProxyServer) {
|
|
await expect(async () => {
|
|
const requests = await proxyServer.getAllRequestsMade();
|
|
const slackRequest = requests.find(
|
|
(r) => r.httpRequest?.path === '/api/chat.postMessage' && r.httpRequest?.method === 'POST',
|
|
);
|
|
expect(slackRequest).toBeDefined();
|
|
}).toPass();
|
|
}
|
|
|
|
async function getApprovalUrlsFromSlack(proxyServer: ProxyServer) {
|
|
const requests = await proxyServer.getAllRequestsMade();
|
|
const slackRequest = requests.find(
|
|
(r) => r.httpRequest?.path === '/api/chat.postMessage' && r.httpRequest?.method === 'POST',
|
|
);
|
|
const blocks = getBlocksFromBody(slackRequest?.httpRequest?.body as { blocks?: SlackBlock[] });
|
|
return extractApprovalUrls(blocks!);
|
|
}
|
|
|
|
async function getFormUrlFromSlack(proxyServer: ProxyServer) {
|
|
const requests = await proxyServer.getAllRequestsMade();
|
|
const slackRequest = requests.find(
|
|
(r) => r.httpRequest?.path === '/api/chat.postMessage' && r.httpRequest?.method === 'POST',
|
|
);
|
|
const blocks = getBlocksFromBody(slackRequest?.httpRequest?.body as { blocks?: SlackBlock[] });
|
|
return extractFormUrl(blocks!);
|
|
}
|
|
|
|
async function clickApprovalLink(page: Page, url: string) {
|
|
console.log('Clicking approval URL:', url);
|
|
const response = await page.request.get(url, {
|
|
headers: { 'User-Agent': NOT_A_BOT_USER_AGENT },
|
|
});
|
|
console.log('Response status:', response.status());
|
|
const body = await response.text();
|
|
console.log('Response body preview:', body.substring(0, 500));
|
|
expect(response.ok()).toBe(true);
|
|
}
|
|
|
|
function withSlackCredential(credential: CredentialResponse) {
|
|
return (workflow: Partial<IWorkflowBase>) => {
|
|
workflow.nodes?.forEach((node) => {
|
|
if (node.type === 'n8n-nodes-base.slack') {
|
|
node.credentials = { slackApi: { id: credential.id, name: credential.name } };
|
|
}
|
|
});
|
|
return workflow;
|
|
};
|
|
}
|
|
|
|
test.use({ capability: 'proxy' });
|
|
|
|
test.describe('Send and Wait @capability:proxy', () => {
|
|
test('should complete approval flow when clicking approve URL', async ({
|
|
n8n,
|
|
services,
|
|
slackCredential,
|
|
}) => {
|
|
const { workflowId } = await n8n.api.workflows.importWorkflowFromFile(
|
|
'send-and-wait-approval.json',
|
|
{ transform: withSlackCredential(slackCredential) },
|
|
);
|
|
await n8n.navigate.toWorkflow(workflowId);
|
|
|
|
await n8n.canvas.clickExecuteWorkflowButton('Manual Trigger');
|
|
await waitForSlackRequest(services.proxy);
|
|
await n8n.api.workflows.waitForWorkflowStatus(workflowId, 'waiting', 10000);
|
|
|
|
const { approveUrl } = await getApprovalUrlsFromSlack(services.proxy);
|
|
expect(approveUrl).toContain('signature=');
|
|
|
|
await clickApprovalLink(n8n.page, approveUrl!);
|
|
|
|
await expect(n8n.canvas.getNodeSuccessStatusIndicator('Capture Result')).toBeVisible();
|
|
});
|
|
|
|
test('should complete rejection flow when clicking reject URL', async ({
|
|
n8n,
|
|
services,
|
|
slackCredential,
|
|
}) => {
|
|
const { workflowId } = await n8n.api.workflows.importWorkflowFromFile(
|
|
'send-and-wait-approval.json',
|
|
{ transform: withSlackCredential(slackCredential) },
|
|
);
|
|
await n8n.navigate.toWorkflow(workflowId);
|
|
|
|
await n8n.canvas.clickExecuteWorkflowButton('Manual Trigger');
|
|
await waitForSlackRequest(services.proxy);
|
|
await n8n.api.workflows.waitForWorkflowStatus(workflowId, 'waiting', 10000);
|
|
|
|
const { rejectUrl } = await getApprovalUrlsFromSlack(services.proxy);
|
|
expect(rejectUrl).toContain('signature=');
|
|
|
|
await clickApprovalLink(n8n.page, rejectUrl!);
|
|
|
|
await expect(n8n.canvas.getNodeSuccessStatusIndicator('Capture Result')).toBeVisible();
|
|
});
|
|
|
|
test('should reject requests with invalid signatures', async ({
|
|
n8n,
|
|
services,
|
|
slackCredential,
|
|
}) => {
|
|
const { workflowId } = await n8n.api.workflows.importWorkflowFromFile(
|
|
'send-and-wait-approval.json',
|
|
{ transform: withSlackCredential(slackCredential) },
|
|
);
|
|
await n8n.navigate.toWorkflow(workflowId);
|
|
|
|
await n8n.canvas.clickExecuteWorkflowButton('Manual Trigger');
|
|
await waitForSlackRequest(services.proxy);
|
|
|
|
const execution = await n8n.api.workflows.waitForWorkflowStatus(workflowId, 'waiting', 10000);
|
|
|
|
const unsignedResponse = await n8n.api.webhooks.trigger(
|
|
`/webhook-waiting/${execution.id}/slack-send-wait`,
|
|
);
|
|
expect(unsignedResponse.status()).toBe(401);
|
|
|
|
const tamperedResponse = await n8n.api.webhooks.trigger(
|
|
`/webhook-waiting/${execution.id}/slack-send-wait?approved=true&signature=tampered`,
|
|
);
|
|
expect(tamperedResponse.status()).toBe(401);
|
|
});
|
|
|
|
test('should complete form submission flow', async ({ n8n, services, slackCredential }) => {
|
|
const { workflowId } = await n8n.api.workflows.importWorkflowFromFile(
|
|
'send-and-wait-form.json',
|
|
{ transform: withSlackCredential(slackCredential) },
|
|
);
|
|
await n8n.navigate.toWorkflow(workflowId);
|
|
|
|
await n8n.canvas.clickExecuteWorkflowButton();
|
|
await waitForSlackRequest(services.proxy);
|
|
await n8n.api.workflows.waitForWorkflowStatus(workflowId, 'waiting', 10000);
|
|
|
|
const formUrl = await getFormUrlFromSlack(services.proxy);
|
|
expect(formUrl).toContain('signature=');
|
|
|
|
const formPage = await n8n.page.context().newPage();
|
|
await formPage.goto(formUrl!);
|
|
|
|
await expect(formPage.getByText('Test Form')).toBeVisible({ timeout: 10000 });
|
|
await expect(formPage.getByText('Please provide your information')).toBeVisible();
|
|
|
|
await formPage.getByLabel('Name').fill('John Doe');
|
|
await formPage.getByLabel('Email').fill('john@example.com');
|
|
await formPage.getByLabel('Comments').fill('This is a test comment');
|
|
|
|
await formPage.getByRole('button', { name: 'Submit Form' }).click();
|
|
|
|
await expect(formPage.getByText('Got it, thanks')).toBeVisible({ timeout: 10000 });
|
|
|
|
await formPage.close();
|
|
|
|
await expect(n8n.canvas.getNodeSuccessStatusIndicator('Capture Result')).toBeVisible();
|
|
|
|
await n8n.canvas.openNode('Capture Result');
|
|
await expect(n8n.ndv.outputPanel.getTbodyCell(0, 0)).toHaveText('John Doe');
|
|
await expect(n8n.ndv.outputPanel.getTbodyCell(0, 1)).toHaveText('john@example.com');
|
|
await expect(n8n.ndv.outputPanel.getTbodyCell(0, 2)).toHaveText('This is a test comment');
|
|
});
|
|
|
|
test('should complete approval flow in production mode (activated workflow)', async ({
|
|
n8n,
|
|
services,
|
|
slackCredential,
|
|
}) => {
|
|
const { workflowId, webhookPath } = await n8n.api.workflows.importWorkflowFromFile(
|
|
'send-and-wait-approval.json',
|
|
{ transform: withSlackCredential(slackCredential) },
|
|
);
|
|
|
|
await n8n.navigate.toWorkflow(workflowId);
|
|
await n8n.canvas.publishWorkflow();
|
|
|
|
const triggerResponse = await n8n.api.webhooks.trigger(`/webhook/${webhookPath}`);
|
|
expect(triggerResponse.ok()).toBe(true);
|
|
|
|
await waitForSlackRequest(services.proxy);
|
|
await n8n.api.workflows.waitForWorkflowStatus(workflowId, 'waiting', 10000);
|
|
|
|
const { approveUrl } = await getApprovalUrlsFromSlack(services.proxy);
|
|
expect(approveUrl).toContain('signature=');
|
|
|
|
await clickApprovalLink(n8n.page, approveUrl!);
|
|
|
|
const execution = await n8n.api.workflows.waitForWorkflowStatus(workflowId, 'success', 10000);
|
|
expect(execution.status).toBe('success');
|
|
});
|
|
});
|