Files
ai-contract-platform/frontend/scripts/member-contract-access-check.mjs
T

151 lines
6.3 KiB
JavaScript

import { chromium, request } from "playwright";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const webBase = process.env.MEMBER_WEB_URL || "http://192.168.3.90:3003";
const apiBase = process.env.MEMBER_API_URL || "http://192.168.3.90:8001/api/v1";
const artifactDir = path.resolve(__dirname, "..", "..", "project-docs", "qa-artifacts", "member-contract-access", stamp);
fs.mkdirSync(artifactDir, { recursive: true });
const result = {
webBase,
apiBase,
artifactDir,
username: process.env.MEMBER_EMAIL || null,
login: null,
contractsPage: null,
reviewPage: null,
reviewsPage: null,
errors: [],
};
function apiUrl(route) {
return `${apiBase.replace(/\/$/, "")}${route}`;
}
function assert(condition, message, details = {}) {
if (!condition) {
const error = new Error(message);
error.details = details;
throw error;
}
}
function summarizeLoginBody(body) {
const user = body?.data?.user;
return {
code: body?.code,
message: body?.message,
hasAccessToken: Boolean(body?.data?.access_token),
user: user
? {
id: user.id,
tenant_id: user.tenant_id,
username: user.username,
display_name: user.display_name,
role: user.role,
status: user.status,
}
: null,
};
}
async function loginMember(api) {
const username = process.env.MEMBER_EMAIL || `member-contract-access-${Date.now()}@example.com`;
const password = process.env.MEMBER_PASSWORD || "123456";
if (!process.env.MEMBER_EMAIL) {
const registerResp = await api.post(apiUrl("/auth/register"), {
data: { username, password, display_name: "Member Contract Access Check" },
});
const registerBody = await registerResp.json().catch(() => ({}));
assert(registerResp.ok(), "member registration failed", { status: registerResp.status(), body: registerBody });
}
const loginResp = await api.post(apiUrl("/auth/login"), {
data: { account: username, password },
});
const loginBody = await loginResp.json().catch(() => ({}));
result.login = { status: loginResp.status(), body: summarizeLoginBody(loginBody) };
assert(loginResp.ok(), "member login failed", result.login);
assert(loginBody?.data?.access_token, "member login did not return access token", loginBody);
assert(loginBody?.data?.user?.role !== "admin", "test account must be a non-admin member", loginBody?.data?.user);
result.username = username;
return loginBody.data;
}
async function main() {
const api = await request.newContext();
const browser = await chromium.launch({ headless: true });
try {
const { access_token: token, user } = await loginMember(api);
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
await context.addInitScript(({ t, u }) => {
window.localStorage.setItem("access_token", t);
window.localStorage.setItem("auth-store", JSON.stringify({ state: { accessToken: t, currentUser: u }, version: 0 }));
}, { t: token, u: user });
const page = await context.newPage();
page.on("console", (msg) => {
if (msg.type() === "error") result.errors.push(`console:${msg.text()}`);
});
page.on("pageerror", (err) => {
result.errors.push(`pageerror:${err.message}`);
});
await page.goto(`${webBase.replace(/\/$/, "")}/contracts`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1500);
await page.screenshot({ path: path.join(artifactDir, "contracts-page.png"), fullPage: true });
const contractsText = await page.locator("body").innerText();
const uploadButton = page.getByRole("button", { name: "上传已有合同审查" });
result.contractsPage = {
url: page.url(),
blockedByAdminGuard: contractsText.includes("仅管理员可管理合同"),
hasUploadReviewAction: await uploadButton.isVisible().catch(() => false),
};
assert(!result.contractsPage.blockedByAdminGuard, "member is blocked from contracts page by admin-only guard", result.contractsPage);
assert(result.contractsPage.hasUploadReviewAction, "contracts page does not expose upload review action for member", result.contractsPage);
await uploadButton.click();
await page.waitForURL(/\/review(?:\?|$)/, { timeout: 10000 });
await page.waitForTimeout(1000);
await page.screenshot({ path: path.join(artifactDir, "review-page-from-contracts.png"), fullPage: true });
const reviewText = await page.locator("body").innerText();
result.reviewPage = {
url: page.url(),
acceptsUploadedContracts: reviewText.includes("PDF / DOCX / TXT / PNG / JPG / JPEG") && reviewText.includes("提交审查"),
};
assert(result.reviewPage.acceptsUploadedContracts, "review page is not ready for uploaded contract review", result.reviewPage);
await page.goto(`${webBase.replace(/\/$/, "")}/reviews`, { waitUntil: "domcontentloaded" });
await page.waitForTimeout(1500);
await page.screenshot({ path: path.join(artifactDir, "reviews-page.png"), fullPage: true });
const reviewsText = await page.locator("body").innerText();
result.reviewsPage = {
url: page.url(),
blockedByAdminGuard: reviewsText.includes("仅管理员可查看审查记录"),
hasCreateReviewAction: await page.getByRole("button", { name: "发起新审查" }).isVisible().catch(() => false),
};
assert(!result.reviewsPage.blockedByAdminGuard, "member is blocked from reviews page by admin-only guard", result.reviewsPage);
assert(result.reviewsPage.hasCreateReviewAction, "reviews page does not expose create review action for member", result.reviewsPage);
fs.writeFileSync(path.join(artifactDir, "member-contract-access-result.json"), JSON.stringify(result, null, 2), "utf8");
console.log(JSON.stringify(result, null, 2));
} finally {
await browser.close().catch(() => undefined);
await api.dispose().catch(() => undefined);
}
}
main().catch((error) => {
result.fatal = { message: error.message, details: error.details };
fs.writeFileSync(path.join(artifactDir, "member-contract-access-result.json"), JSON.stringify(result, null, 2), "utf8");
console.error(error);
process.exit(1);
});