169 lines
5.7 KiB
JavaScript
169 lines
5.7 KiB
JavaScript
"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,
|
|
};
|