Files
DadaoTask/web/e2e/register.mjs
T
Steven b5170ad1ce
CI / backend (push) Canceled after 0s
feat(auth): 邮箱验证码注册,生产重新有了自助入口
短信登录关掉后,生产没有任何注册入口——这一版补上最普通的那条路:
填邮箱、收验证码、设密码,注册完直接进首页。

先发码再注册,不是"注册时顺带发信"

  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 必需的。
2026-11-03 01:04:26 +08:00

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);