/** * 登录方式与系统设置的验收:短信关着、邮箱能登录、SMTP 后台可配。 * * 三个都要真跑,因为这三件事全是"接口回 200 但人用不了"的高危区: * - 短信关着,登录页就不能出现「验证码」这一档(不然用户点进去只会看到 2017); * - 邮箱 + 密码要能真的登进来,不是只把邮箱存进库; * - SMTP 存进库之后要能立刻发信,别等到真要用的时候才发现授权码是错的。 * * 授权码不进仓库:发信那一步只在给了 SMTP_PASS 环境变量时才跑。 */ import { chromium } from "playwright-core"; import { mkdirSync, readFileSync } from "node:fs"; const WEB = process.env.WEB ?? "http://127.0.0.1:5180"; const API = process.env.API ?? "http://127.0.0.1:8100/api/v1"; const PHONE = process.env.PHONE ?? "13900000001"; const PWD_FILE = new URL("../../.local/e2e-account-password.txt", import.meta.url); const OUT = "e2e/artifacts"; const SMTP_HOST = process.env.SMTP_HOST ?? "smtp.126.com"; const SMTP_USER = process.env.SMTP_USER ?? ""; const SMTP_PASS = process.env.SMTP_PASS ?? ""; let pass = 0; let fail = 0; function check(name, ok, extra = "") { if (ok) { pass += 1; console.log(`PASS ${name}`); } else { fail += 1; console.log(`FAIL ${name} ${extra}`); } } function savedPassword() { try { return readFileSync(PWD_FILE, "utf8").trim() || null; } catch { return null; } } async function post(path, body, token) { const r = await fetch(`${API}${path}`, { method: "POST", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(body ?? {}), }); return r.json(); } async function get(path, token) { const r = await fetch(`${API}${path}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); return r.json(); } async function patch(path, body, token) { const r = await fetch(`${API}${path}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify(body), }); return r.json(); } async function main() { mkdirSync(OUT, { recursive: true }); const browser = await chromium.launch({ executablePath: process.env.CHROME ?? `${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`, }); const page = await browser.newPage({ viewport: { width: 1440, height: 960 } }); const errors = []; page.on("console", (m) => m.type() === "error" && errors.push(m.text())); page.on("pageerror", (e) => errors.push(String(e))); const password = savedPassword(); // ① 服务端说清楚现在开着哪几条路 const methods = await get("/auth/methods"); check( "① /auth/methods 说短信关着、密码与邮箱密码开着", methods.code === 0 && methods.data.password === true && methods.data.email_password === true && methods.data.sms_code === false, JSON.stringify(methods.data), ); // ② 关着的那条路真的挡住了 const sms = await post("/auth/sms/send", { phone: PHONE }); check("② 发验证码的接口回 2017(不是静默成功)", sms.code === 2017, JSON.stringify(sms)); // ③ 登录页不该再出现「验证码」这一档 await page.goto(`${WEB}/login`); await page.waitForSelector("#account"); const codeTab = await page.getByRole("button", { name: "验证码", exact: true }).count(); const emailTab = await page.getByRole("tab", { name: "邮箱" }).count(); const pwdInput = await page.locator("#password").count(); check("③ 登录页没有验证码那一档、有邮箱档与密码框", codeTab === 0 && emailTab === 1 && pwdInput === 1); // ④ 手机号 + 密码,从界面登录进去 let loggedIn = false; if (password) { await page.getByLabel("手机号").fill(PHONE); await page.locator("#password").fill(password); await page.getByRole("button", { name: "登录", exact: true }).click(); loggedIn = await page .waitForURL(`${WEB}/`, { timeout: 8000 }) .then(() => true) .catch(() => false); } check("④ 手机号 + 密码能从登录页进首页", loggedIn, "(缺 .local/e2e-account-password.txt 时跳过)"); const tokens = loggedIn ? await page.evaluate(() => localStorage.getItem("dadao.access")) : (await post("/auth/login/password", { phone: PHONE, password })).data?.tokens?.access_token; // ⑤ 绑定邮箱,然后**用邮箱登录** const email = `dadao-e2e-${Date.now()}@example.com`; const bind = await patch("/auth/me", { email }, tokens); check("⑤ 账号页能绑邮箱", bind.code === 0 && bind.data.user.email === email, JSON.stringify(bind)); const byEmail = await post("/auth/login/password", { email: email.toUpperCase(), password }); const emailLogin = await post("/auth/login/password", { email, password }); check( "⑥ 邮箱 + 密码能登录(大小写不敏感)", emailLogin.code === 0 && emailLogin.data?.tokens?.access_token, JSON.stringify(emailLogin), ); check("⑥b 全大写邮箱也能登录", byEmail.code === 0, JSON.stringify(byEmail)); // ⑦ 解绑之后这条路就该关掉,而不是留个后门 const unbind = await patch("/auth/me", { email: "" }, tokens); const afterUnbind = await post("/auth/login/password", { email, password }); check( "⑦ 解绑邮箱后邮箱登录回 2013", unbind.code === 0 && unbind.data.user.email === null && afterUnbind.code === 2013, JSON.stringify(afterUnbind), ); // ⑧ 平台管理员才看得到「系统设置」 const me = await get("/auth/me", tokens); check("⑧ 当前账号被标成平台管理员", me.data?.user?.platform_admin === true, JSON.stringify(me.data?.user)); await page.goto(`${WEB}/account`); await page.waitForSelector('input[aria-label="昵称"]'); // 侧栏按 /auth/me 的 platform_admin 决定要不要渲染这一条,等它落下来再数 const settingsLink = await page .waitForFunction(() => document.querySelectorAll('aside a[href="/settings"]').length, { timeout: 5000, }) .then((handle) => handle.jsonValue()) .catch(() => 0); check("⑧b 侧栏里有「系统设置」入口", settingsLink === 1, `count=${settingsLink}`); await page.screenshot({ path: `${OUT}/settings-entry.png` }); // ⑨ 系统设置页能读、能存;授权码只写不读 await page.goto(`${WEB}/settings`); await page.waitForSelector('input[aria-label="服务器地址"]'); const before = await get("/settings/smtp", tokens); check("⑨ 系统设置页读得到当前配置", before.code === 0 && before.data.password_set !== undefined); if (SMTP_PASS) { await page.getByLabel("服务器地址").fill(SMTP_HOST); await page.getByLabel("端口").fill("465"); await page.locator('input[aria-label="账号"]').fill(SMTP_USER); await page.getByLabel("授权码").fill(SMTP_PASS); await page.getByRole("button", { name: "保存", exact: true }).click(); await page.waitForTimeout(1200); const saved = await get("/settings/smtp", tokens); check( "⑩ 授权码存进库了,但接口不回显", saved.data?.password_set === true && saved.data?.configured === true && !JSON.stringify(saved.data).includes(SMTP_PASS), JSON.stringify(saved.data), ); // ⑪ 真发一封,确认这条通道是通的(不是"存了个假配置") const sent = await post("/settings/smtp/test", { to: SMTP_USER }, tokens); check("⑪ 测试邮件发送成功", sent.code === 0, JSON.stringify(sent)); if (sent.code !== 0) { console.log(` 发信失败原文:${sent.message}`); } await page.screenshot({ path: `${OUT}/settings-smtp.png` }); } else { console.log("SKIP ⑩⑪ 没给 SMTP_PASS,跳过保存与发信"); } // ⑫ 账号页上的邮箱框要能回填刚绑的值(说明前端读的是接口,不是本地缓存) const rebind = await patch("/auth/me", { email }, tokens); await page.goto(`${WEB}/account`); await page.waitForSelector('input[aria-label="邮箱"]'); const emailInput = await page.waitForFunction( () => { const el = document.querySelector('input[aria-label="邮箱"]'); return el && el.value.includes("@") ? el.value : null; }, { timeout: 5000 }, ); const shownEmail = await emailInput.jsonValue(); check("⑫ 账号页回填刚绑的邮箱", rebind.code === 0 && shownEmail === email, shownEmail); check("⑬ 控制台零报错", errors.length === 0, errors.slice(0, 2).join(" | ")); await browser.close(); console.log(`\n${fail === 0 ? "ALL PASS" : "SOME FAILED"} — pass ${pass}, fail ${fail}`); process.exit(fail === 0 ? 0 : 1); } main().catch((err) => { console.error("脚本异常", err); process.exit(1); });