refactor: E2E tests use real email() handler (#870)

* refactor: add receive_mail E2E endpoint using real email() handler

Add /admin/test/receive_mail that constructs a mock ForwardableEmailMessage
and calls the real email() handler, so E2E tests exercise the full mail
processing pipeline. Extract both test endpoints into e2e_test_api.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trigger CI

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2026-03-06 10:15:02 +08:00
committed by GitHub
parent f98bbce234
commit e38015a5b6
3 changed files with 77 additions and 25 deletions

View File

@@ -0,0 +1,63 @@
import { Context } from 'hono'
import { getBooleanValue } from '../utils'
// Direct DB insert — bypasses the email() handler.
const seedMail = async (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.E2E_TEST_MODE)) {
return c.text("Not available", 404);
}
const { address, source, raw, message_id } = await c.req.json();
if (!address || !raw) {
return c.text("address and raw are required", 400);
}
if (raw.length > 1_000_000) {
return c.text("raw content too large", 400);
}
if (message_id && message_id.length > 255) {
return c.text("message_id too long", 400);
}
const msgId = message_id || `<e2e-${Date.now()}@test>`;
const { success } = await c.env.DB.prepare(
`INSERT INTO raw_mails (message_id, source, address, raw, created_at)`
+ ` VALUES (?, ?, ?, ?, datetime('now'))`
).bind(msgId, source || address, address, raw).run();
return c.json({ success });
};
// Exercises the real email() handler with a mock ForwardableEmailMessage.
const receiveMail = async (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.E2E_TEST_MODE)) {
return c.text("Not available", 404);
}
const { from, to, raw } = await c.req.json();
if (!from || !to || !raw) {
return c.text("from, to and raw are required", 400);
}
// Parse MIME headers (unfold continuation lines, extract key:value pairs)
const headerSection = raw.substring(0, Math.max(0, raw.indexOf('\r\n\r\n')));
const headers = new Headers();
for (const line of headerSection.replace(/\r\n(?=[ \t])/g, ' ').split('\r\n')) {
const idx = line.indexOf(':');
if (idx > 0) headers.append(line.substring(0, idx).trim(), line.substring(idx + 1).trim());
}
if (!headers.has('Message-ID')) headers.set('Message-ID', `<e2e-${Date.now()}@test>`);
const rawBytes = new TextEncoder().encode(raw);
let rejected: string | undefined;
const mockMessage: ForwardableEmailMessage = {
from, to, headers,
rawSize: rawBytes.byteLength,
raw: new ReadableStream({ start(ctrl) { ctrl.enqueue(rawBytes); ctrl.close(); } }),
setReject(reason: string) { rejected = reason; },
forward: async () => ({ messageId: '' }),
reply: async () => ({ messageId: '' }),
};
const { email: emailHandler } = await import('../email');
await emailHandler(mockMessage, c.env, { waitUntil: () => {}, passThroughOnException: () => {} });
return c.json({ success: !rejected, ...(rejected ? { rejected } : {}) });
};
export default { seedMail, receiveMail };

View File

@@ -17,6 +17,7 @@ import db_api from './db_api'
import ip_blacklist_settings from './ip_blacklist_settings'
import ai_extract_settings from './ai_extract_settings'
import { EmailRuleSettings } from '../models'
import e2e_test_api from './e2e_test_api'
export const api = new Hono<HonoCustomType>()
@@ -389,25 +390,6 @@ api.post("/admin/ip_blacklist/settings", ip_blacklist_settings.saveIpBlacklistSe
api.get("/admin/ai_extract/settings", ai_extract_settings.getAiExtractSettings);
api.post("/admin/ai_extract/settings", ai_extract_settings.saveAiExtractSettings);
// Test-only endpoint for seeding emails. MUST NOT be enabled in production.
api.post('/admin/test/seed_mail', async (c) => {
if (!getBooleanValue(c.env.E2E_TEST_MODE)) {
return c.text("Not available", 404);
}
const { address, source, raw, message_id } = await c.req.json();
if (!address || !raw) {
return c.text("address and raw are required", 400);
}
if (raw.length > 1_000_000) {
return c.text("raw content too large", 400);
}
if (message_id && message_id.length > 255) {
return c.text("message_id too long", 400);
}
const msgId = message_id || `<e2e-${Date.now()}@test>`;
const { success } = await c.env.DB.prepare(
`INSERT INTO raw_mails (message_id, source, address, raw, created_at)`
+ ` VALUES (?, ?, ?, ?, datetime('now'))`
).bind(msgId, source || address, address, raw).run();
return c.json({ success });
});
// E2E test endpoints
api.post('/admin/test/seed_mail', e2e_test_api.seedMail);
api.post('/admin/test/receive_mail', e2e_test_api.receiveMail);