174 lines
6.6 KiB
JavaScript
174 lines
6.6 KiB
JavaScript
#!/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;
|
||
|
|
});
|