#!/usr/bin/env node const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); const { chromium } = require("playwright"); const BASE_URL = process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000"; const UI_TIMEOUT_MS = Number(process.env.UI_TIMEOUT_MS || 10_000); function resolveChromiumExecutablePath() { const explicit = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || ""; if (explicit && fs.existsSync(explicit)) return explicit; return [ "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium", "/usr/bin/chromium", "/usr/bin/chromium-browser", ].find((candidate) => fs.existsSync(candidate)) || ""; } function fileUrl(filePath) { return `file://${filePath.split(path.sep).map((part, index) => ( index === 0 ? "" : encodeURIComponent(part) )).join("/")}`; } async function waitForText(page, selector, expected) { await page.waitForFunction( ({ selector: targetSelector, expectedText }) => { const node = document.querySelector(targetSelector); return Boolean(node && node.textContent && node.textContent.includes(expectedText)); }, { selector, expectedText: expected }, { timeout: UI_TIMEOUT_MS }, ); return page.locator(selector).innerText({ timeout: UI_TIMEOUT_MS }); } async function main() { const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-admin-access-policy-")); fs.mkdirSync(path.join(root, ".mnote"), { recursive: true }); fs.writeFileSync(path.join(root, "README.md"), "# Admin Smoke\n", "utf8"); const executablePath = resolveChromiumExecutablePath(); const browser = await chromium.launch({ headless: true, ...(executablePath ? { executablePath } : {}), }); const adminContext = await browser.newContext({ extraHTTPHeaders: { "x-mnote-actor-id": "admin_smoke", "x-mnote-actor-type": "admin", }, }); const adminPage = await adminContext.newPage(); const readerContext = await browser.newContext({ extraHTTPHeaders: { "x-mnote-actor-id": "reader_smoke", "x-mnote-actor-type": "user", }, }); const readerPage = await readerContext.newPage(); const grantId = `grant_${Date.now()}`; const rootUri = fileUrl(root); try { await adminPage.goto(`${BASE_URL}/admin/access-policy`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS, }); await adminPage.locator('[data-testid="mnote-admin-access-policy-page"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS, }); assert( await adminPage.locator('[data-testid="mnote-admin-policy-path"]').innerText(), "管理页应显示策略路径", ); await adminPage.locator('[data-testid="mnote-admin-root-uri"]').fill(rootUri); await adminPage.locator('[data-testid="mnote-admin-validate-root-submit"]').click({ timeout: UI_TIMEOUT_MS, }); const validateResult = await waitForText( adminPage, '[data-testid="mnote-admin-validate-result"]', root, ); assert(validateResult.includes(root), "验证目录结果应包含 canonical root"); await adminPage.locator('[data-testid="mnote-admin-grant-id"]').fill(grantId); await adminPage.locator('[data-testid="mnote-admin-grant-user-id"]').fill("reader_smoke"); await adminPage.locator('[data-testid="mnote-admin-grant-root-uri"]').fill(rootUri); await adminPage.locator('[data-testid="mnote-admin-grant-permission"]').selectOption("read"); await adminPage.locator('[data-testid="mnote-admin-grant-recursive"]').check(); await adminPage.locator('[data-testid="mnote-admin-grant-capabilities"]').fill("ai,share"); await adminPage.locator('[data-testid="mnote-admin-create-grant-submit"]').click({ timeout: UI_TIMEOUT_MS, }); const createResult = await waitForText( adminPage, '[data-testid="mnote-admin-create-result"]', grantId, ); assert(createResult.includes(grantId), "创建结果应返回 grantId"); await readerPage.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); const openResponse = await readerPage.evaluate(async ({ rootUriValue }) => { const url = new URL("/api/local-folder/files/open", window.location.origin); url.searchParams.set("rootUri", rootUriValue); url.searchParams.set("path", "README.md"); const response = await fetch(url.toString(), { credentials: "include" }); return { status: response.status, text: await response.text() }; }, { rootUriValue: rootUri }); assert.equal(openResponse.status, 200, "read grant 应可打开本地文件"); assert(openResponse.text.includes("Admin Smoke"), "打开的文件内容应正确"); const writeResponse = await readerPage.evaluate(async ({ rootUriValue }) => { const response = await fetch("/api/page-body/write", { method: "POST", headers: { "content-type": "application/json" }, credentials: "include", body: JSON.stringify({ documentId: "local-md:README.md", workspaceId: "local-ws-admin-smoke", sourceKind: "local_folder", rootUri: rootUriValue, contentFormat: "editorBlocks", content: [], }), }); return { status: response.status, text: await response.text() }; }, { rootUriValue: rootUri }); assert.equal(writeResponse.status, 403, "read grant 不应允许写入"); await adminPage.locator('[data-testid="mnote-admin-delete-grant-id"]').fill(grantId); await adminPage.locator('[data-testid="mnote-admin-delete-grant-submit"]').click({ timeout: UI_TIMEOUT_MS, }); const deleteResult = await waitForText( adminPage, '[data-testid="mnote-admin-delete-result"]', grantId, ); assert(deleteResult.includes(grantId), "删除结果应返回 grantId"); await adminPage.waitForFunction( ({ selector, removedGrantId }) => { const node = document.querySelector(selector); return Boolean(node && node.textContent && !node.textContent.includes(removedGrantId)); }, { selector: '[data-testid="mnote-admin-policy-json"]', removedGrantId: grantId }, { timeout: UI_TIMEOUT_MS }, ); const policyJson = await adminPage.locator('[data-testid="mnote-admin-policy-json"]').innerText(); assert(!policyJson.includes(grantId), "删除后策略面板不应再包含已删授权"); } finally { await adminContext.close().catch(() => {}); await readerContext.close().catch(() => {}); await browser.close().catch(() => {}); fs.rmSync(root, { recursive: true, force: true }); } } main().catch((error) => { console.error(error); process.exitCode = 1; });