mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-28 19:48:01 +08:00
test: add Dockerized E2E test environment with Playwright + Mailpit (#860)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WORKER_URL, TEST_DOMAIN, createTestAddress, deleteAddress, requestSendAccess } from '../../fixtures/test-helpers';
|
||||
|
||||
test.describe('Address Lifecycle', () => {
|
||||
test('create address, request send access, fetch settings, then delete', async ({ request }) => {
|
||||
// Create address
|
||||
const { jwt, address } = await createTestAddress(request, 'lifecycle-test');
|
||||
expect(address).toContain('@' + TEST_DOMAIN);
|
||||
expect(jwt).toBeTruthy();
|
||||
|
||||
// Request send access (creates address_sender row with DEFAULT_SEND_BALANCE)
|
||||
await requestSendAccess(request, jwt);
|
||||
|
||||
// Fetch address settings — balance should match DEFAULT_SEND_BALANCE=10
|
||||
const settingsRes = await request.get(`${WORKER_URL}/api/settings`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
});
|
||||
expect(settingsRes.ok()).toBe(true);
|
||||
const settings = await settingsRes.json();
|
||||
expect(settings.send_balance).toBe(10);
|
||||
|
||||
// Delete address
|
||||
await deleteAddress(request, jwt);
|
||||
|
||||
// Verify address is gone — settings should fail
|
||||
const afterDelete = await request.get(`${WORKER_URL}/api/settings`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
});
|
||||
expect(afterDelete.ok()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WORKER_URL } from '../../fixtures/test-helpers';
|
||||
|
||||
test.describe('Health & Settings', () => {
|
||||
test('GET /health_check returns OK', async ({ request }) => {
|
||||
const res = await request.get(`${WORKER_URL}/health_check`);
|
||||
expect(res.ok()).toBe(true);
|
||||
expect(await res.text()).toBe('OK');
|
||||
});
|
||||
|
||||
test('GET /open_api/settings returns correct domains and sendMail enabled', async ({ request }) => {
|
||||
const res = await request.get(`${WORKER_URL}/open_api/settings`);
|
||||
expect(res.ok()).toBe(true);
|
||||
|
||||
const settings = await res.json();
|
||||
expect(settings.domains).toContain('test.example.com');
|
||||
expect(settings.defaultDomains).toContain('test.example.com');
|
||||
expect(settings.enableSendMail).toBe(true);
|
||||
expect(settings.enableUserCreateEmail).toBe(true);
|
||||
expect(settings.enableUserDeleteEmail).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
createTestAddress,
|
||||
deleteAddress,
|
||||
deleteAllMailpitMessages,
|
||||
requestSendAccess,
|
||||
onMailpitMessage,
|
||||
WORKER_URL,
|
||||
} from '../../fixtures/test-helpers';
|
||||
|
||||
test.describe('Send Mail via SMTP', () => {
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await deleteAllMailpitMessages(request);
|
||||
});
|
||||
|
||||
test('send HTML email and verify in Mailpit', async ({ request }) => {
|
||||
const { jwt, address } = await createTestAddress(request, 'sender-test');
|
||||
|
||||
// Must request send access before sending (creates address_sender row)
|
||||
await requestSendAccess(request, jwt);
|
||||
|
||||
const subject = `E2E Test ${Date.now()}`;
|
||||
const htmlContent = '<h1>Hello</h1><p>This is an <b>E2E test</b> email.</p>';
|
||||
|
||||
// Start listening for the message BEFORE sending
|
||||
const listener = onMailpitMessage((m) => m.Subject === subject);
|
||||
await listener.ready;
|
||||
|
||||
// Send mail via worker API
|
||||
const sendRes = await request.post(`${WORKER_URL}/api/send_mail`, {
|
||||
headers: { Authorization: `Bearer ${jwt}` },
|
||||
data: {
|
||||
from_name: 'E2E Sender',
|
||||
to_name: 'E2E Recipient',
|
||||
to_mail: 'recipient@test.example.com',
|
||||
subject,
|
||||
content: htmlContent,
|
||||
is_html: true,
|
||||
},
|
||||
});
|
||||
expect(sendRes.ok()).toBe(true);
|
||||
|
||||
// Wait for Mailpit WebSocket "new" event — no polling
|
||||
const mail = await listener.message;
|
||||
expect(mail.From.Address).toBe(address);
|
||||
expect(mail.To[0].Address).toBe('recipient@test.example.com');
|
||||
|
||||
// Cleanup
|
||||
await deleteAddress(request, jwt);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
FRONTEND_URL,
|
||||
createTestAddress,
|
||||
seedTestMail,
|
||||
deleteAddress,
|
||||
} from '../../fixtures/test-helpers';
|
||||
import { request as apiRequest } from '@playwright/test';
|
||||
|
||||
test.describe('Inbox Browser Flow', () => {
|
||||
test('login via JWT, view inbox, open email', async ({ page }) => {
|
||||
// Create API context for setup
|
||||
const api = await apiRequest.newContext();
|
||||
|
||||
const { jwt, address } = await createTestAddress(api, 'inbox-browser');
|
||||
|
||||
// Seed an email
|
||||
const subject = `Browser Test ${Date.now()}`;
|
||||
await seedTestMail(api, address, {
|
||||
subject,
|
||||
html: '<h1>Welcome</h1><p>This is a <b>browser test</b> email.</p>',
|
||||
});
|
||||
|
||||
// Login via JWT query param with /en/ path to force English locale
|
||||
await page.goto(`${FRONTEND_URL}/en/?jwt=${jwt}`);
|
||||
|
||||
// The mail subject should be visible in the inbox list item
|
||||
const mailItem = page.getByRole('listitem').getByText(subject);
|
||||
await expect(mailItem).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Click to open the email
|
||||
await mailItem.click();
|
||||
|
||||
// Verify the email detail panel shows the subject as a heading
|
||||
// (n-card-header wraps n-card-header__main, both match heading role — use .first())
|
||||
await expect(page.getByRole('heading', { name: subject }).first()).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Cleanup
|
||||
await deleteAddress(api, jwt);
|
||||
await api.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
FRONTEND_URL,
|
||||
createTestAddress,
|
||||
seedTestMail,
|
||||
deleteAddress,
|
||||
deleteAllMailpitMessages,
|
||||
requestSendAccess,
|
||||
} from '../../fixtures/test-helpers';
|
||||
import { request as apiRequest } from '@playwright/test';
|
||||
|
||||
test.describe('Reply HTML & XSS Sanitization', () => {
|
||||
test('reply to HTML email — XSS payloads stripped, HTML preserved', async ({ page }) => {
|
||||
const api = await apiRequest.newContext();
|
||||
await deleteAllMailpitMessages(api);
|
||||
|
||||
const { jwt, address } = await createTestAddress(api, 'reply-xss');
|
||||
|
||||
// Request send access so Reply can navigate to compose form
|
||||
await requestSendAccess(api, jwt);
|
||||
|
||||
// Seed email with XSS payloads embedded in HTML
|
||||
const xssHtml = [
|
||||
'<div>',
|
||||
' <h1>Important Message</h1>',
|
||||
' <p>Please review this content.</p>',
|
||||
' <script>alert("xss")</script>',
|
||||
' <img src=x onerror="alert(1)">',
|
||||
' <a href="javascript:alert(2)">click me</a>',
|
||||
' <p style="color:red">Styled paragraph</p>',
|
||||
'</div>',
|
||||
].join('\n');
|
||||
|
||||
await seedTestMail(api, address, {
|
||||
subject: 'XSS Test Email',
|
||||
html: xssHtml,
|
||||
from: 'attacker@test.example.com',
|
||||
});
|
||||
|
||||
// Single dialog handler with phase tracking.
|
||||
// During email rendering, the mail viewer uses an unsandboxed iframe so
|
||||
// inline event handlers like onerror may fire — we dismiss those.
|
||||
// After clicking Reply, any dialog means the compose path failed to sanitize.
|
||||
let inComposePhase = false;
|
||||
let composeDialogAppeared = false;
|
||||
page.on('dialog', async (dialog) => {
|
||||
if (inComposePhase) composeDialogAppeared = true;
|
||||
await dialog.dismiss();
|
||||
});
|
||||
|
||||
// Login with English locale
|
||||
await page.goto(`${FRONTEND_URL}/en/?jwt=${jwt}`);
|
||||
|
||||
// Open the email (use listitem to avoid strict mode violation
|
||||
// when detail panel also shows the subject)
|
||||
const mailItem = page.getByRole('listitem').getByText('XSS Test Email');
|
||||
await expect(mailItem).toBeVisible({ timeout: 10_000 });
|
||||
await mailItem.click();
|
||||
|
||||
// Wait for Reply button to appear — signals email content has rendered
|
||||
const replyButton = page.locator('button').filter({ hasText: /Reply/i }).first();
|
||||
await expect(replyButton).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Click Reply — from here on, dialogs indicate sanitization failure (#857)
|
||||
inComposePhase = true;
|
||||
await replyButton.click();
|
||||
|
||||
// In the reply compose area, check that the forwarded HTML is sanitized:
|
||||
// - <script> tags should be removed
|
||||
// - onerror attributes should be removed
|
||||
// - javascript: URLs should be removed
|
||||
const composeArea = page.locator('.ql-editor, [contenteditable], textarea').first();
|
||||
await expect(composeArea).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Use inputValue() for <textarea> (Vue v-model sets .value, not innerHTML),
|
||||
// fall back to innerHTML() for contenteditable elements
|
||||
const tagName = await composeArea.evaluate(el => el.tagName.toLowerCase());
|
||||
const content = tagName === 'textarea'
|
||||
? await composeArea.inputValue()
|
||||
: await composeArea.innerHTML();
|
||||
|
||||
// Verify content is non-empty (guard against vacuous pass)
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
// XSS vectors must be stripped
|
||||
expect(content).not.toContain('<script>');
|
||||
expect(content).not.toContain('onerror');
|
||||
expect(content).not.toContain('javascript:');
|
||||
|
||||
// No XSS dialog should have fired in the compose area
|
||||
expect(composeDialogAppeared).toBe(false);
|
||||
|
||||
// Cleanup
|
||||
await deleteAddress(api, jwt);
|
||||
await api.dispose();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user