/** * 测试基建:内存 D1 + 临时目录 R2 + Console app + 种子数据。 * 每个 test 文件独立建 env,互不干扰。 */ import { Database } from "bun:sqlite"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { createApp } from "../console/src/app.ts"; import { LocalD1 } from "../console/src/local/d1.ts"; import { LocalR2 } from "../console/src/local/r2.ts"; import type { Env } from "../console/src/env.ts"; import { encryptSecret } from "../console/src/db.ts"; import { hashPassword, sha256Hex, signNodeRequest } from "../shared/src/index.ts"; import { ids } from "../shared/src/index.ts"; /** # 成员变量: 迁移 SQL(按文件名顺序全部应用,一次读取) **/ export const MIGRATION_SQL = ( await Promise.all( [...new Bun.Glob("migrations/*.sql").scanSync({ cwd: new URL("..", import.meta.url).pathname })] .sort() .map(async (f) => await Bun.file(new URL(`../${f}`, import.meta.url)).text()), ) ).join("\n"); /** # 成员变量: 测试邮件样例(plain + html + 1 附件) **/ export const TEST_MAIL = `From: Alice \r To: hello@example.test\r Cc: bob@example.net\r Subject: Test Mail\r Message-ID: \r MIME-Version: 1.0\r Content-Type: multipart/mixed; boundary="BB"\r \r --BB\r Content-Type: text/plain\r \r plain body\r --BB\r Content-Type: text/html\r \r html body\r --BB\r Content-Type: text/plain; name="a.txt"\r Content-Disposition: attachment; filename="a.txt"\r \r attachment-content\r --BB--\r `; /** # 成员变量: 测试环境(内存库 + 临时 R2 目录) **/ export interface TestEnv { env: Env; app: ReturnType; db: Database; /** 默认节点 */ nodeId: string; /** 默认节点 credential secret(明文,用于签名) */ nodeSecret: string; /** catch-all 域名 example.test 对应的 mailbox id */ mailboxId: string; domainId: string; } /** # 函数 * 构造隔离测试环境并播种:admin 用户、node、credential、domain、catch-all mailbox */ export async function makeTestEnv(): Promise { const db = new Database(":memory:", { strict: true }); db.exec(MIGRATION_SQL); const env: Env = { DB: new LocalD1(db), R2: new LocalR2(mkdtempSync(path.join(tmpdir(), "wmh-r2-"))), WMAILHUB_SIM_CF: "1", WMAILHUB_ALLOW_PRIVATE_WEBHOOKS: "1", }; const app = createApp(); const now = new Date().toISOString(); await env.DB.prepare("INSERT INTO users (id, username, password_hash, created_at) VALUES (?, 'admin', ?, ?)") .bind(ids.user(), await hashPassword("admin"), now) .run(); const nodeId = "node_test1"; const nodeSecret = "wns_test_secret_for_signing"; await env.DB.prepare( `INSERT INTO nodes (id, name, cloudflare_account_id, credential_id, status, ingress_url, ingress_secret_enc, provision_steps, created_at, updated_at) VALUES (?, 'test-node', 'acct-1', 'cred1', 'online', 'http://127.0.0.1:18788/ingress/email', 'enc', '{}', ?, ?)`, ) .bind(nodeId, now, now) .run(); await env.DB.prepare( "INSERT INTO node_credentials (id, node_id, secret_hash, secret_enc, status, created_at) VALUES ('cred1', ?, ?, ?, 'active', ?)", ) .bind(nodeId, await sha256Hex(nodeSecret), await encryptSecret(env, nodeSecret), now) .run(); const domainId = "dom_test1"; await env.DB.prepare( "INSERT INTO domains (id, cloudflare_account_id, node_id, name, status, created_at, updated_at) VALUES (?, 'acct-1', ?, 'example.test', 'active', ?, ?)", ) .bind(domainId, nodeId, now, now) .run(); const mailboxId = "mbx_test1"; await env.DB.prepare( "INSERT INTO mailboxes (id, domain_id, address, status, is_catch_all, created_at, updated_at) VALUES (?, ?, '*@example.test', 'active', 1, ?, ?)", ) .bind(mailboxId, domainId, now, now) .run(); return { env, app, db, nodeId, nodeSecret, mailboxId, domainId }; } /** # 函数 * 构造登录会话 cookie(走真实登录接口) */ export async function loginCookie(test: TestEnv): Promise { const resp = await test.app.fetch( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "admin", password: "admin" }), }), test.env, ); const setCookie = resp.headers.get("Set-Cookie") ?? ""; const m = /wmh_session=([^;]+)/.exec(setCookie); if (!m) throw new Error("login failed: no session cookie"); return `wmh_session=${m[1]}`; } /** # 函数 * 构造已签名的 Node 邮件转发请求 */ export async function signedEmailRequest( test: TestEnv, body: Uint8Array, overrides: { secret?: string; nodeId?: string; tamperSignature?: boolean; tamperBody?: Uint8Array; timestamp?: string; reuseNonce?: string; } = {}, ): Promise { const url = `http://localhost/api/internal/nodes/${overrides.nodeId ?? test.nodeId}/emails`; // 签名始终基于原始 body;发送 tamperBody 即构成"签名-内容不一致"的篡改请求 const headers = await signNodeRequest( overrides.secret ?? test.nodeSecret, overrides.nodeId ?? test.nodeId, "POST", url, body, ); headers["Content-Type"] = "message/rfc822"; headers["X-WmailHub-Mail-From"] = "alice@example.org"; headers["X-WmailHub-Rcpt-To"] = "hello@example.test"; if (overrides.tamperSignature) headers["X-WmailHub-Signature"] = "0".repeat(64); if (overrides.timestamp) headers["X-WmailHub-Timestamp"] = overrides.timestamp; if (overrides.reuseNonce) headers["X-WmailHub-Nonce"] = overrides.reuseNonce; const payload = overrides.tamperBody ?? body; const ab = payload.buffer.slice( payload.byteOffset, payload.byteOffset + payload.byteLength, ) as ArrayBuffer; return new Request(url, { method: "POST", headers, body: ab }); } /** # 函数 * 会话 GET/POST 便捷封装 */ export function authedFetch(test: TestEnv, cookie: string, url: string, init?: RequestInit) { const headers = new Headers(init?.headers); headers.set("Cookie", cookie); return test.app.fetch(new Request(url, { ...init, headers }), test.env); }