CI / backend (push) Canceled after 0s
短信登录关掉后,生产没有任何注册入口——这一版补上最普通的那条路:
填邮箱、收验证码、设密码,注册完直接进首页。
先发码再注册,不是"注册时顺带发信"
POST /auth/email/code 只做三件事:查重、限流、发码。
填完密码才被告知"这个邮箱注册过了"最气人,所以占用(2019)在发码前就回。
邮箱既是账号名也是唯一联系方式,所以必须真验一次码
验证通过时间落 app_user.email_verified_at,/auth/me 回 email_verified。
"随手填一个"会让账号变成没人能找回的孤岛。
app_user.phone 改为可空(迁移 0011)
邮箱注册的账号本来就没有手机号。user.phone 对这类账号回空串,
成员列表同理,不再伪装成一串星号。
防刷
同邮箱 60 秒一次(2022)、验证码 10 分钟有效、错 5 次即作废(2021)。
6 位数可以被刮,不设上限等于把注册口子交给脚本。
本机联调开关 EMAIL_CODE_DEV_ECHO
为 true 时把码回给调用方且不发真信——否则每跑一次 e2e 就往真实收件箱塞一封。
生产打开会让 validate_runtime() 拒绝启动。
前端
新增 /register(邮箱、验证码、密码、确认、昵称;60 秒倒计时;
服务端关着注册时整页只说"联系管理员开通"),登录页按 /auth/methods
决定要不要显示「还没有账号?用邮箱注册」。
验证
pytest 139 passed、ruff check / format 全绿
tsc --noEmit 0、vite build 通过
e2e register.mjs 7 项:登录页入口 → 拿码 → 注册落进首页 → 退出用邮箱+密码登回来
→ 同一邮箱再发码回 2019 → 控制台零报错
e2e login-email-smtp.mjs 15 项回归通过(含真发一封测试信)
e2e account.mjs 14 项、knowledge.mjs 全绿
顺带修的两处测试
test_account 的字段白名单补 email_verified(接口确实多了这个字段);
test_config_prod 用 Settings(_env_file=None, ...) 构造,别被本机 .env 里的
EMAIL_CODE_DEV_ECHO=true 带偏——那个开关在生产是硬错误,本机却是跑 e2e 必需的。
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
/**
|
|
* 注册的验收:从登录页走到注册页,把账号建出来,再用它登回来。
|
|
*
|
|
* 这里验的是"一个人真能自己进来",所以每一步都从界面走:
|
|
* ① 登录页有「用邮箱注册」(服务端开着才会出现)
|
|
* ② 注册页拿到验证码(本机 EMAIL_CODE_DEV_ECHO 会把码回给页面,不发真信)
|
|
* ③ 提交后直接进首页,说明注册即登录
|
|
* ④ 退出、用「邮箱 + 密码」再登一次,证明邮箱真的是账号名
|
|
* ⑤ 同一个邮箱再发码,必须被挡(2019),而不是又建一个号
|
|
*/
|
|
import { chromium } from "playwright-core";
|
|
|
|
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 CHROME =
|
|
process.env.CHROME ??
|
|
`${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`;
|
|
|
|
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 ? ` — ${extra}` : ""}`);
|
|
}
|
|
}
|
|
|
|
const email = `reg${Date.now()}@example.com`;
|
|
const password = `dadao${Math.floor(Math.random() * 900000 + 1000)}`;
|
|
|
|
const browser = await chromium.launch({ executablePath: CHROME });
|
|
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)));
|
|
|
|
try {
|
|
await page.goto(`${WEB}/login`);
|
|
await page.waitForSelector("#account");
|
|
|
|
const registerLink = page.getByRole("link", { name: "用邮箱注册" });
|
|
check("登录页有「用邮箱注册」入口", (await registerLink.count()) > 0);
|
|
|
|
await registerLink.click();
|
|
await page.waitForURL("**/register");
|
|
await page.waitForSelector("#email");
|
|
check("能进注册页", true);
|
|
|
|
await page.locator("#email").fill(email);
|
|
await page.getByRole("button", { name: "获取验证码" }).click();
|
|
await page.waitForSelector("text=开发环境验证码", { timeout: 15000 });
|
|
const notice = await page.locator("text=开发环境验证码").first().innerText();
|
|
const code = notice.match(/\d{6}/)?.[0] ?? "";
|
|
check("本机拿得到验证码(生产只发到邮箱)", code.length === 6, notice);
|
|
|
|
await page.locator("#code").fill(code);
|
|
await page.locator("#password").fill(password);
|
|
await page.locator("#confirm").fill(password);
|
|
await page.locator("#nickname").fill("注册验收");
|
|
await page.getByRole("button", { name: "注册并登录" }).click();
|
|
|
|
const landed = await page
|
|
.waitForURL(`${WEB}/`, { timeout: 15000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
check("注册完直接进首页(注册即登录)", landed, page.url());
|
|
|
|
// 退出后用「邮箱 + 密码」登回来:邮箱真是账号名
|
|
await page.getByRole("button", { name: "退出登录" }).last().click();
|
|
await page.waitForURL("**/login");
|
|
await page.getByRole("tab", { name: "邮箱" }).click();
|
|
await page.locator("#account").fill(email);
|
|
await page.locator("#password").fill(password);
|
|
await page.getByRole("button", { name: "登录", exact: true }).click();
|
|
const back = await page
|
|
.waitForURL(`${WEB}/`, { timeout: 15000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
check("邮箱 + 密码能登回来", back);
|
|
|
|
// 同一个邮箱再发码:必须拦住,别再建一个号
|
|
const again = await page.request.post(`${API}/auth/email/code`, { data: { email } });
|
|
const body = await again.json();
|
|
check("已注册的邮箱再发码回 2019", body.code === 2019, JSON.stringify(body));
|
|
|
|
check("控制台无报错", errors.length === 0, errors.slice(0, 2).join(" | "));
|
|
} catch (err) {
|
|
fail += 1;
|
|
console.log(`FAIL 脚本异常 — ${err}`);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
console.log(`\n${fail === 0 ? "ALL PASS" : "FAILED"} — pass ${pass}, fail ${fail}`);
|
|
process.exit(fail === 0 ? 0 : 1);
|