Files
wpywmail-client/tools/api-check.js
T

145 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 针对「界面实际使用的那条 API 链路」做一次验收:
* /api/send(含中文主题正文 + 回复头)→ 轮询确认到达 → /api/flags → /api/move → /api/delete
* 结束后把测试邮件从收件箱与「已发送」都清掉,不给你留垃圾。
*/
const BASE = process.env.WPYWMAIL_BASE || 'http://127.0.0.1:8788';
const token = 'WPYW-API-' + Date.now().toString().slice(-8);
const report = [];
const results = [];
function say(s = '') { report.push(s); }
function step(name, ok, detail = '') {
results.push({ name, ok, detail });
report.push(`${ok ? '[PASS]' : '[FAIL]'} ${name}${detail ? ' —— ' + detail : ''}`);
process.stdout.write(`${ok ? 'PASS' : 'FAIL'} ${name}\n`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function api(path, { method = 'GET', body } = {}) {
const res = await fetch(BASE + path, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let data = null;
try { data = text ? JSON.parse(text) : null; } catch { data = { error: text }; }
if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);
return data;
}
(async () => {
say('═'.repeat(74));
say('WpywMail 客户端 · 界面 API 链路验收');
say(`本地服务:${BASE}`);
say(`标记 :${token}`);
say(`时间 :${new Date().toLocaleString('zh-CN')}`);
say('═'.repeat(74));
say('');
const subject = `界面链路验收 ${token} · 中文主题`;
const text = [
'这封是通过客户端界面的发信链路(/api/send)发出的。',
`标记:${token}`,
'标点:你好,世界。()《》——、;:!?',
].join('\r\n');
// 1) 发送
say('── 1) /api/send 发信(中文主题 + 中文正文)──────────────────');
const sendOut = await api('/api/send', {
method: 'POST',
body: { to: '[email protected]', subject, text },
});
step('/api/send 返回成功', sendOut.ok === true,
`收件人 ${sendOut.recipients.join(',')},${sendOut.bytes} 字节`);
// 2) 等到达
say('');
say('── 2) 轮询收件箱确认到达 ────────────────────────────────────');
let hit = null;
for (let i = 0; i < 12 && !hit; i++) {
await sleep(2500);
const list = await api('/api/messages?folder=INBOX&limit=20');
hit = (list.messages || []).find((m) => (m.subject || '').includes(token)) || null;
}
step('发出的信出现在收件箱', !!hit, hit ? `UID ${hit.uid}` : '未找到');
if (hit) {
const detail = await api(`/api/messages/${hit.uid}?folder=INBOX`);
step('主题往返无损(无乱码、无编码字残留)',
detail.subject === subject && !detail.subject.includes('=?'), detail.subject);
step('正文往返无损(含全角标点)',
/你好,世界。()《》——、;:!?/.test(detail.text), `${detail.text.length} 字符`);
// 3) 旗标
say('');
say('── 3) 旗标 / 已读 ──────────────────────────────────────────');
await api('/api/flags', { method: 'POST', body: { uid: hit.uid, folder: 'INBOX', flagged: true } });
const after1 = await api('/api/messages?folder=INBOX&limit=20');
const row1 = after1.messages.find((m) => m.uid === hit.uid);
step('加旗标生效', !!(row1 && row1.flagged), `flagged=${row1 && row1.flagged}`);
await api('/api/flags', { method: 'POST', body: { uid: hit.uid, folder: 'INBOX', seen: false } });
const after2 = await api('/api/messages?folder=INBOX&limit=20');
const row2 = after2.messages.find((m) => m.uid === hit.uid);
step('标为未读生效', !!(row2 && !row2.seen), `seen=${row2 && row2.seen}`);
// 4) 移动 → 归档 → 移回
say('');
say('── 4) 移动(COPY + 删除,服务器没有 MOVE 能力)─────────────');
await api('/api/move', { method: 'POST', body: { uid: hit.uid, folder: 'INBOX', target: 'Archive' } });
const arch = await api('/api/messages?folder=Archive&limit=20');
const inArch = (arch.messages || []).some((m) => (m.subject || '').includes(token));
step('移动到 Archive 成功', inArch, `Archive 现有 ${arch.total} 封`);
const back = arch.messages.find((m) => (m.subject || '').includes(token));
if (back) {
await api('/api/move', { method: 'POST', body: { uid: back.uid, folder: 'Archive', target: 'INBOX' } });
const again = await api('/api/messages?folder=INBOX&limit=20');
step('移回 INBOX 成功', (again.messages || []).some((m) => (m.subject || '').includes(token)));
}
// 5) 删除(进废纸篓)
say('');
say('── 5) 删除(默认移入 Trash)────────────────────────────────');
const moveBack = await api('/api/messages?folder=INBOX&limit=20');
const cur = (moveBack.messages || []).find((m) => (m.subject || '').includes(token)) || hit;
const del = await api('/api/delete', { method: 'POST', body: { uid: cur.uid, folder: 'INBOX' } });
const after = await api('/api/messages?folder=INBOX&limit=20');
const gone = !(after.messages || []).some((m) => (m.subject || '').includes(token));
step('从收件箱移除', gone, del.moved ? `已移入 ${del.folder}` : '已永久删除');
}
// 6) 清掉「已发送」里的副本
say('');
say('── 6) 清理测试痕迹 ─────────────────────────────────────────');
const sentBox = await api('/api/messages?folder=Sent&limit=30');
const sentHits = (sentBox.messages || []).filter((m) => (m.subject || '').includes('WPYW-CLI-') || (m.subject || '').includes('WPYW-API-'));
let cleaned = 0;
for (const m of sentHits) {
try {
await api('/api/delete', { method: 'POST', body: { uid: m.uid, folder: 'Sent', permanent: true } });
cleaned++;
} catch { /* 忽略 */ }
}
step('清理「已发送」中的测试副本', true, `清理 ${cleaned} 封(共发现 ${sentHits.length} 封)`);
const pass = results.filter((r) => r.ok).length;
const fail = results.length - pass;
say('');
say('═'.repeat(74));
say(`汇总:通过 ${pass} / ${results.length},失败 ${fail}`);
if (fail) for (const r of results.filter((x) => !x.ok)) say(` - ${r.name} ${r.detail}`);
say('═'.repeat(74));
const fs = require('node:fs');
fs.writeFileSync('E:\\deepseek\\artifacts\\client-api-check.txt', report.join('\n'), 'utf8');
process.stdout.write(`\nREPORT E:\\deepseek\\artifacts\\client-api-check.txt\nSUMMARY pass=${pass} fail=${fail}\n`);
process.exit(fail ? 1 : 0);
})().catch((err) => {
process.stderr.write('FATAL ' + (err && err.stack ? err.stack : err) + '\n');
process.exit(2);
});