mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
test: add E2E tests for webhook trigger on incoming mail (#878)
* test: add E2E tests for auto-reply trigger and webhook trigger - Improve mock reply() in e2e_test_api.ts to send auto-replies via SMTP (WorkerMailer) so they reach Mailpit for verification - Add auto-reply-trigger.spec.ts: verifies auto-reply is sent when incoming mail matches source_prefix, and NOT sent otherwise - Add webhook-trigger.spec.ts: starts a temporary HTTP server to receive webhook calls, verifies payload on mail arrival Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: fallback EmailMessage for E2E auto-reply when cloudflare:email is unavailable In wrangler dev mode without Email Routing binding, `import('cloudflare:email')` throws, silently caught by auto_reply's try-catch. Add a fallback that constructs a plain object with a ReadableStream `raw` property so the E2E mock reply() can send the auto-reply via SMTP to Mailpit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: handle both \r\n and \n line endings in MIME parser for E2E tests mimetext uses os.EOL which is \n on Linux (Docker). The parseMimeForReply function only looked for \r\n, causing it to fail parsing the auto-reply MIME content in the E2E environment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add debug logging and robust raw MIME extraction for E2E auto-reply - auto_reply.ts: add fallback ReadableStream when cloudflare:email is unavailable, attach rawMime directly to replyMessage - e2e_test_api.ts: try reading rawMime string first, then fallback to ReadableStream; add diagnostic console.log for CI debugging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: skip sealed EmailMessage in E2E mode, await webhook server listen - auto_reply.ts: use plain object with raw ReadableStream in E2E_TEST_MODE (cloudflare:email's EmailMessage is sealed, can't attach extra properties) - e2e_test_api.ts: simplify mock reply() to read raw ReadableStream directly, add defensive check for from without @ - webhook-trigger.spec.ts: await server.listen to ensure socket is bound before sending requests (CodeRabbit review feedback) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add missing await for async startWebhookReceiver in disabled test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: drop auto-reply E2E test, clean up webhook test - Remove auto-reply-trigger.spec.ts (cannot test without modifying production auto_reply.ts due to sealed EmailMessage from cloudflare:email) - Clean up e2e_test_api.ts: remove WorkerMailer, MIME parsing, and SMTP reply logic that was only needed for auto-reply testing - Improve webhook test: use dynamic port allocation (port 0) instead of hardcoded WEBHOOK_PORT to avoid port conflicts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: assert webhook request path in E2E test Add path assertion to verify webhook request hits /webhook endpoint, preventing false positives from incorrect routing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
161
e2e/tests/api/webhook-trigger.spec.ts
Normal file
161
e2e/tests/api/webhook-trigger.spec.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import http from 'node:http';
|
||||
import {
|
||||
WORKER_URL,
|
||||
createTestAddress,
|
||||
deleteAddress,
|
||||
} from '../../fixtures/test-helpers';
|
||||
|
||||
/**
|
||||
* Start a temporary HTTP server that records incoming requests.
|
||||
* Returns the server, a promise that resolves with the first request body,
|
||||
* and the URL to use as webhook target.
|
||||
*/
|
||||
async function startWebhookReceiver(): Promise<{
|
||||
server: http.Server;
|
||||
firstRequest: Promise<{ body: string; method: string; path: string; headers: http.IncomingHttpHeaders }>;
|
||||
url: string;
|
||||
}> {
|
||||
let resolve: (val: any) => void;
|
||||
const firstRequest = new Promise<any>((r) => { resolve = r; });
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk) => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
resolve({
|
||||
body: Buffer.concat(chunks).toString('utf-8'),
|
||||
method: req.method || '',
|
||||
path: req.url || '',
|
||||
headers: req.headers,
|
||||
});
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end('{"ok":true}');
|
||||
});
|
||||
});
|
||||
|
||||
// Use port 0 to let the OS assign a free port
|
||||
await new Promise<void>((resolve) => server.listen(0, '0.0.0.0', resolve));
|
||||
const addr = server.address();
|
||||
if (!addr || typeof addr === 'string') throw new Error('Failed to resolve webhook receiver port');
|
||||
const boundPort = addr.port;
|
||||
// In Docker network, e2e-runner container hostname is "e2e-runner"
|
||||
const hostname = process.env.CI ? 'e2e-runner' : 'localhost';
|
||||
return { server, firstRequest, url: `http://${hostname}:${boundPort}/webhook` };
|
||||
}
|
||||
|
||||
test.describe('Webhook — triggered on incoming mail', () => {
|
||||
let jwt: string;
|
||||
let address: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
({ jwt, address } = await createTestAddress(request, 'webhook-trigger'));
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
await deleteAddress(request, jwt);
|
||||
});
|
||||
|
||||
test('webhook is called with correct payload when mail arrives', async ({ request }) => {
|
||||
const { server, firstRequest, url } = await startWebhookReceiver();
|
||||
|
||||
try {
|
||||
// Configure user webhook
|
||||
const saveRes = await request.post(`${WORKER_URL}/api/webhook/settings`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: {
|
||||
enabled: true,
|
||||
url,
|
||||
method: 'POST',
|
||||
headers: JSON.stringify({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({
|
||||
from: '${from}',
|
||||
to: '${to}',
|
||||
subject: '${subject}',
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
// Send incoming mail via receive_mail endpoint
|
||||
const from = `webhook-sender@test.example.com`;
|
||||
const subject = `Webhook Test ${Date.now()}`;
|
||||
const messageId = `<webhook-${Date.now()}@test>`;
|
||||
const raw = [
|
||||
`From: ${from}`,
|
||||
`To: ${address}`,
|
||||
`Subject: ${subject}`,
|
||||
`Message-ID: ${messageId}`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: text/plain; charset=utf-8`,
|
||||
``,
|
||||
`Webhook trigger test body`,
|
||||
].join('\r\n');
|
||||
|
||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: { from, to: address, raw },
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
|
||||
// Wait for webhook to be called
|
||||
const received = await firstRequest;
|
||||
expect(received.method).toBe('POST');
|
||||
expect(received.path).toBe('/webhook');
|
||||
|
||||
const payload = JSON.parse(received.body);
|
||||
expect(payload.from).toContain('webhook-sender@test.example.com');
|
||||
expect(payload.to).toBe(address);
|
||||
expect(payload.subject).toBe(subject);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('webhook is NOT called when disabled', async ({ request }) => {
|
||||
const { server, firstRequest, url } = await startWebhookReceiver();
|
||||
|
||||
try {
|
||||
// Disable webhook
|
||||
const saveRes = await request.post(`${WORKER_URL}/api/webhook/settings`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: {
|
||||
enabled: false,
|
||||
url,
|
||||
method: 'POST',
|
||||
headers: JSON.stringify({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ from: '${from}' }),
|
||||
},
|
||||
});
|
||||
expect(saveRes.ok()).toBe(true);
|
||||
|
||||
// Send incoming mail
|
||||
const subject = `Webhook Disabled ${Date.now()}`;
|
||||
const messageId = `<webhook-off-${Date.now()}@test>`;
|
||||
const raw = [
|
||||
`From: sender@test.example.com`,
|
||||
`To: ${address}`,
|
||||
`Subject: ${subject}`,
|
||||
`Message-ID: ${messageId}`,
|
||||
`MIME-Version: 1.0`,
|
||||
`Content-Type: text/plain; charset=utf-8`,
|
||||
``,
|
||||
`Should not trigger webhook`,
|
||||
].join('\r\n');
|
||||
|
||||
const res = await request.post(`${WORKER_URL}/admin/test/receive_mail`, {
|
||||
data: { from: 'sender@test.example.com', to: address, raw },
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
|
||||
// Webhook should NOT be called — wait briefly then verify timeout
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('timeout')), 3_000)
|
||||
);
|
||||
await expect(
|
||||
Promise.race([firstRequest, timeoutPromise])
|
||||
).rejects.toThrow('timeout');
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user