import { test, expect } from '@playwright/test'; import { WORKER_GZIP_URL, TEST_DOMAIN } from '../../fixtures/test-helpers'; /** * These tests run against a worker instance with ENABLE_MAIL_GZIP=true. * They verify gzip-compressed storage and backward-compatible reading. */ // Helper: create address on the gzip worker async function createGzipAddress(ctx: any, name: string) { const uniqueName = `${name}${Date.now()}`; const res = await ctx.post(`${WORKER_GZIP_URL}/api/new_address`, { data: { name: uniqueName, domain: TEST_DOMAIN }, }); if (!res.ok()) throw new Error(`Failed to create address: ${res.status()} ${await res.text()}`); const body = await res.json(); return { jwt: body.jwt, address: body.address, address_id: body.address_id }; } // Helper: seed mail via receiveMail (goes through email() handler → gzip compression) async function receiveGzipMail( ctx: any, address: string, opts: { subject?: string; html?: string; text?: string; from?: string } ) { const from = opts.from || `sender@${TEST_DOMAIN}`; const subject = opts.subject || 'Test Email'; const boundary = `----E2E${Date.now()}`; const htmlPart = opts.html || `

${opts.text || 'Hello from E2E'}

`; const textPart = opts.text || 'Hello from E2E'; const messageId = ``; const raw = [ `From: ${from}`, `To: ${address}`, `Subject: ${subject}`, `Message-ID: ${messageId}`, `MIME-Version: 1.0`, `Content-Type: multipart/alternative; boundary="${boundary}"`, ``, `--${boundary}`, `Content-Type: text/plain; charset=utf-8`, ``, textPart, `--${boundary}`, `Content-Type: text/html; charset=utf-8`, ``, htmlPart, `--${boundary}--`, ].join('\r\n'); const res = await ctx.post(`${WORKER_GZIP_URL}/admin/test/receive_mail`, { data: { from, to: address, raw }, }); if (!res.ok()) throw new Error(`Failed to receive mail: ${res.status()} ${await res.text()}`); const body = await res.json(); if (!body.success) throw new Error(`Mail was rejected: ${body.rejected || 'unknown reason'}`); } // Helper: seed mail via seedMail (direct INSERT → plaintext raw, no gzip) async function seedPlaintextMail( ctx: any, address: string, opts: { subject?: string; text?: string; from?: string } ) { const from = opts.from || `sender@${TEST_DOMAIN}`; const subject = opts.subject || 'Plaintext Mail'; const messageId = ``; const raw = [ `From: ${from}`, `To: ${address}`, `Subject: ${subject}`, `Message-ID: ${messageId}`, `Content-Type: text/plain; charset=utf-8`, ``, opts.text || 'Hello plaintext from E2E', ].join('\r\n'); const res = await ctx.post(`${WORKER_GZIP_URL}/admin/test/seed_mail`, { data: { address, source: from, raw, message_id: messageId }, }); if (!res.ok()) throw new Error(`Failed to seed mail: ${res.status()} ${await res.text()}`); } // Helper: delete address on gzip worker async function deleteGzipAddress(ctx: any, jwt: string) { await ctx.delete(`${WORKER_GZIP_URL}/api/delete_address`, { headers: { Authorization: `Bearer ${jwt}` }, }); } test.describe('Mail Gzip Storage', () => { test.beforeEach(() => { test.skip(!WORKER_GZIP_URL, 'WORKER_GZIP_URL not set — skipping gzip tests'); }); test('gzip-compressed mail is readable in list', async ({ request }) => { const { jwt, address } = await createGzipAddress(request, 'gzip-list'); try { await receiveGzipMail(request, address, { subject: 'Gzip List Test', text: 'compressed content here', }); const res = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, { headers: { Authorization: `Bearer ${jwt}` }, }); expect(res.ok()).toBe(true); const { results } = await res.json(); expect(results).toHaveLength(1); expect(results[0].raw).toContain('Gzip List Test'); expect(results[0].raw).toContain('compressed content here'); } finally { await deleteGzipAddress(request, jwt); } }); test('gzip-compressed mail is readable in detail', async ({ request }) => { const { jwt, address } = await createGzipAddress(request, 'gzip-detail'); try { await receiveGzipMail(request, address, { subject: 'Gzip Detail Test', html: 'bold gzip', }); const listRes = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, { headers: { Authorization: `Bearer ${jwt}` }, }); const { results } = await listRes.json(); expect(results.length).toBeGreaterThanOrEqual(1); const mailId = results[0].id; const detailRes = await request.get(`${WORKER_GZIP_URL}/api/mail/${mailId}`, { headers: { Authorization: `Bearer ${jwt}` }, }); expect(detailRes.ok()).toBe(true); const mail = await detailRes.json(); expect(mail.raw).toContain('Gzip Detail Test'); expect(mail.raw).toContain('bold gzip'); } finally { await deleteGzipAddress(request, jwt); } }); test('gzip-compressed mail is readable through admin detail API', async ({ request }) => { const { jwt, address } = await createGzipAddress(request, 'gzip-admin-detail'); try { await receiveGzipMail(request, address, { subject: 'Gzip Admin Detail Test', text: 'admin compressed content', }); const listRes = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, { headers: { Authorization: `Bearer ${jwt}` }, }); const { results } = await listRes.json(); expect(results.length).toBeGreaterThanOrEqual(1); const mailId = results[0].id; const detailRes = await request.get(`${WORKER_GZIP_URL}/admin/mails/${mailId}`, { headers: { 'x-admin-auth': 'e2e-admin-pass' }, }); expect(detailRes.ok()).toBe(true); const mail = await detailRes.json(); expect(mail.raw).toContain('Gzip Admin Detail Test'); expect(mail.raw).toContain('admin compressed content'); expect(mail.raw_blob).toBeUndefined(); } finally { await deleteGzipAddress(request, jwt); } }); test('mixed: plaintext seed + gzip receive both readable in same list', async ({ request }) => { const { jwt, address } = await createGzipAddress(request, 'gzip-mixed'); try { // 1. Direct INSERT plaintext (simulates pre-gzip data) await seedPlaintextMail(request, address, { subject: 'Old Plaintext Mail', text: 'legacy plain content', }); // 2. receiveMail → goes through email() handler → gzip compressed await receiveGzipMail(request, address, { subject: 'New Gzip Mail', text: 'new compressed content', }); const res = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, { headers: { Authorization: `Bearer ${jwt}` }, }); expect(res.ok()).toBe(true); const { results } = await res.json(); expect(results).toHaveLength(2); // Both mails should have readable raw content const subjects = results.map((r: any) => r.raw); expect(subjects.some((r: string) => r.includes('Old Plaintext Mail'))).toBe(true); expect(subjects.some((r: string) => r.includes('New Gzip Mail'))).toBe(true); expect(subjects.some((r: string) => r.includes('legacy plain content'))).toBe(true); expect(subjects.some((r: string) => r.includes('new compressed content'))).toBe(true); } finally { await deleteGzipAddress(request, jwt); } }); test('admin internal mail (sendAdminInternalMail) is gzip-compressed and readable', async ({ request }) => { const { jwt, address } = await createGzipAddress(request, 'gzip-admin-mail'); try { // 1. Request send access → creates address_sender row const reqAccessRes = await request.post(`${WORKER_GZIP_URL}/api/request_send_mail_access`, { headers: { Authorization: `Bearer ${jwt}` }, }); expect(reqAccessRes.ok()).toBe(true); // 2. Get address_sender id const senderListRes = await request.get( `${WORKER_GZIP_URL}/admin/address_sender?limit=10&offset=0&address=${encodeURIComponent(address)}`, ); expect(senderListRes.ok()).toBe(true); const senderList = await senderListRes.json(); expect(senderList.results.length).toBeGreaterThanOrEqual(1); const senderId = senderList.results[0].id; // 3. Update send access via admin API → triggers sendAdminInternalMail const updateRes = await request.post(`${WORKER_GZIP_URL}/admin/address_sender`, { data: { address, address_id: senderId, balance: 99, enabled: true }, }); expect(updateRes.ok()).toBe(true); // 4. Verify the internal mail is readable const mailsRes = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, { headers: { Authorization: `Bearer ${jwt}` }, }); expect(mailsRes.ok()).toBe(true); const { results } = await mailsRes.json(); expect(results.length).toBeGreaterThanOrEqual(1); // mimetext base64-encodes the Subject header, so match on body content instead const internalMail = results.find((m: any) => m.raw?.includes('balance: 99')); expect(internalMail).toBeDefined(); expect(internalMail.raw).toContain('admin@internal'); expect(internalMail.raw).toContain('balance: 99'); expect(internalMail).not.toHaveProperty('raw_blob'); } finally { await deleteGzipAddress(request, jwt); } }); test('raw_blob field is not exposed in API response', async ({ request }) => { const { jwt, address } = await createGzipAddress(request, 'gzip-noblob'); try { await receiveGzipMail(request, address, { subject: 'No Blob Leak' }); // Check list response const listRes = await request.get(`${WORKER_GZIP_URL}/api/mails?limit=10&offset=0`, { headers: { Authorization: `Bearer ${jwt}` }, }); const { results } = await listRes.json(); expect(results.length).toBeGreaterThanOrEqual(1); expect(results[0]).not.toHaveProperty('raw_blob'); // Check detail response const detailRes = await request.get(`${WORKER_GZIP_URL}/api/mail/${results[0].id}`, { headers: { Authorization: `Bearer ${jwt}` }, }); const mail = await detailRes.json(); expect(mail).not.toHaveProperty('raw_blob'); } finally { await deleteGzipAddress(request, jwt); } }); });