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((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((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 = ``; 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 = ``; 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(); } }); });