Files
mnote/scripts/lib/browser-auth-login.js
T

169 lines
5.7 KiB
JavaScript
Raw Normal View History

"use strict";
/**
* 7-76 P0:浏览器 smoke 标准登录(无「测试账号快速登录」按钮)。
* 优先填表提交 /auth;失败时可回退 API signIn/signUp(由调用方决定)。
*/
const DEFAULT_EMAIL = process.env.MNOTE_E2E_EMAIL || "mnote.e2e@example.com";
const DEFAULT_PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!";
const DEFAULT_USERNAME = process.env.MNOTE_E2E_USERNAME || "mnote-e2e";
/** 方案 A:独立 ops admin(非 AI 主体、非个人笔记主账号) */
const DEFAULT_ADMIN_EMAIL = process.env.MNOTE_ADMIN_EMAIL || "mnote.admin@example.com";
const DEFAULT_ADMIN_PASSWORD = process.env.MNOTE_ADMIN_PASSWORD || "MnoteAdmin123!";
const DEFAULT_ADMIN_USERNAME = process.env.MNOTE_ADMIN_USERNAME || "mnote-admin";
const DEFAULT_UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
function resolveBaseUrl(baseUrl) {
return String(baseUrl || process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(
/\/+$/,
"",
);
}
/**
* 在已打开的 /auth 页(或任意页)用标准表单登录。
* @param {import('playwright').Page} page
* @param {{ baseUrl?: string, email?: string, password?: string, username?: string, timeoutMs?: number, gotoAuth?: boolean }} [options]
*/
async function loginViaAuthForm(page, options = {}) {
const baseUrl = resolveBaseUrl(options.baseUrl);
const email = options.email || DEFAULT_EMAIL;
const password = options.password || DEFAULT_PASSWORD;
const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS;
const gotoAuth = options.gotoAuth !== false;
if (gotoAuth || !String(page.url() || "").includes("/auth")) {
await page.goto(`${baseUrl}/auth`, { waitUntil: "commit", timeout: timeoutMs });
}
const account = page.locator("#account, input[name='account']").first();
const passwordInput = page.locator("#password, input[name='password']").first();
const submit = page.locator("[data-auth-submit], button[type='submit']").first();
await account.waitFor({ state: "visible", timeout: timeoutMs });
await account.fill(email, { timeout: timeoutMs });
await passwordInput.fill(password, { timeout: timeoutMs });
await submit.click({ timeout: timeoutMs });
}
/**
* API 层 signIn;不存在时可 signUp 再 signIn。
* @param {{ fetch: Function }} requestContext Playwright request 或兼容对象
* @param {{ baseUrl?: string, email?: string, password?: string, username?: string, timeoutMs?: number }} [options]
*/
async function loginViaAuthApi(requestContext, options = {}) {
const baseUrl = resolveBaseUrl(options.baseUrl);
const email = options.email || DEFAULT_EMAIL;
const password = options.password || DEFAULT_PASSWORD;
const username = options.username || DEFAULT_USERNAME;
const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS;
async function post(params) {
const response = await requestContext.fetch(`${baseUrl}/api/auth`, {
method: "POST",
headers: { "content-type": "application/json" },
data: {
action: "auth:signIn",
args: {
provider: "password",
params,
},
},
timeout: timeoutMs,
});
const text = await response.text();
let payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch {
payload = text;
}
return { ok: response.ok(), status: response.status(), payload };
}
let result = await post({
account: email,
email,
password,
flow: "signIn",
});
if (result.ok) {
return result.payload;
}
result = await post({
email,
name: username,
password,
flow: "signUp",
});
if (result.ok) {
return result.payload;
}
const details =
typeof result.payload === "string" ? result.payload : JSON.stringify(result.payload);
throw new Error(`标准 API 登录失败: ${result.status} ${details}`);
}
/**
* 以 ops admin 登录(/admin/*、代签、access-policy、ai-admin settings)。
* AI 主体 mnote-e2e 与个人账号 liaibo 默认无 admin 能力。
*/
async function loginAsAdminViaAuthForm(page, options = {}) {
return loginViaAuthForm(page, {
...options,
email: options.email || DEFAULT_ADMIN_EMAIL,
password: options.password || DEFAULT_ADMIN_PASSWORD,
username: options.username || DEFAULT_ADMIN_USERNAME,
});
}
async function loginAsAdminViaAuthApi(requestContext, options = {}) {
return loginViaAuthApi(requestContext, {
...options,
email: options.email || DEFAULT_ADMIN_EMAIL,
password: options.password || DEFAULT_ADMIN_PASSWORD,
username: options.username || DEFAULT_ADMIN_USERNAME,
});
}
/**
* 在独立 browser context 里以 admin 会话执行 fn(page),不污染主测用户 cookie。
* 用于:access-policy grants、/api/ai-admin/* 等必须 admin 的 seed。
* @param {import('playwright').Browser} browser
* @param {(page: import('playwright').Page) => Promise<T>} fn
* @param {{ baseUrl?: string, timeoutMs?: number }} [options]
* @returns {Promise<T>}
*/
async function withAdminBrowserSession(browser, fn, options = {}) {
const baseUrl = resolveBaseUrl(options.baseUrl);
const timeoutMs = options.timeoutMs || DEFAULT_UI_TIMEOUT_MS;
const context = await browser.newContext();
const page = await context.newPage();
try {
await loginAsAdminViaAuthForm(page, { baseUrl, timeoutMs });
await page
.waitForURL((url) => !String(url).includes("/auth"), { timeout: timeoutMs })
.catch(() => {});
return await fn(page);
} finally {
await context.close().catch(() => {});
}
}
module.exports = {
DEFAULT_EMAIL,
DEFAULT_PASSWORD,
DEFAULT_USERNAME,
DEFAULT_ADMIN_EMAIL,
DEFAULT_ADMIN_PASSWORD,
DEFAULT_ADMIN_USERNAME,
loginViaAuthForm,
loginViaAuthApi,
loginAsAdminViaAuthForm,
loginAsAdminViaAuthApi,
withAdminBrowserSession,
};