Files
mnote/scripts/task114-rust-web-gateway-entry-smoke.js
T

191 lines
5.7 KiB
JavaScript
Raw Normal View History

2026-04-29 12:24:44 +08:00
#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const { spawn } = require("node:child_process");
const net = require("node:net");
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000);
const EXTERNAL_BASE_URL = (process.env.MNOTE_UI_BASE_URL || "").replace(/\/+$/, "");
function findFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close(() => reject(new Error("无法分配测试端口")));
return;
}
const port = address.port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
async function fetchWithTimeout(url, init = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error(`请求超时: ${url}`)), REQUEST_TIMEOUT_MS);
try {
return await fetch(url, {
redirect: "manual",
cache: "no-store",
...init,
signal: controller.signal,
});
} finally {
clearTimeout(timer);
}
}
async function waitForGateway(baseUrl) {
const deadline = Date.now() + REQUEST_TIMEOUT_MS;
let lastError = null;
while (Date.now() < deadline) {
try {
const response = await fetchWithTimeout(`${baseUrl}/api/gateway/health`);
if (response.ok && response.headers.get("x-mnote-web-owner") === "mnote-web") {
return;
}
lastError = new Error(`gateway health 未就绪: ${response.status}`);
} catch (error) {
lastError = error;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
throw lastError || new Error("gateway health 未就绪");
}
function startGateway(port) {
const env = {
...process.env,
...buildDocumentFixtureEnv(),
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
MNOTE_WEB_LEGACY_NEXT_BASE_URL: "http://127.0.0.1:3100",
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
};
return spawn("cargo", ["run", "-q", "-p", "mnote-web", "--bin", "mnote-web"], {
cwd: "/mnt/Data1T/mnote/rust",
env,
stdio: ["ignore", "pipe", "pipe"],
});
}
function buildDocumentFixtureEnv() {
return {
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
MNOTE_WEB_QUERY_FIXTURES_JSON: JSON.stringify({
"documents:getMeta": {
id: "doc_1",
workspace_id: "ws_demo",
title: "服务端页面",
updated_at: "2026-04-18T09:30:00Z",
can_edit: true,
word_count: 42,
character_count: 128,
block_count: 3,
},
"documents:getContent": {
content: [{ id: "block_1", type: "paragraph", content: [] }],
revision: 7,
conflict_detection_key: "doc_1:7",
pageSubtree: { rootNodeId: "doc_1", outline: [] },
},
}),
};
}
async function validateGateway(baseUrl) {
const health = await fetchWithTimeout(`${baseUrl}/api/gateway/health`);
const healthPayload = await health.json();
assert.equal(health.status, 200, `gateway health 失败: ${health.status}`);
assert.equal(health.headers.get("x-mnote-web-owner"), "mnote-web");
assert.equal(healthPayload.owner, "mnote-web");
assert.equal(healthPayload.publicEntry, "127.0.0.1:3000");
const auth = await fetchWithTimeout(`${baseUrl}/auth`);
const authText = await auth.text();
assert.equal(auth.status, 200, `/auth 失败: ${auth.status}`);
assert.equal(auth.headers.get("x-mnote-web-owner"), "mnote-web");
assert.match(auth.headers.get("content-type") || "", /text\/html/);
2026-05-06 21:44:20 +08:00
assert.equal(auth.headers.get("x-mnote-legacy-upstream"), null, "/auth 不应再代理 3100");
assert.match(authText, /data-mnote-shell="auth"/);
2026-05-21 23:53:39 +08:00
assert.match(authText, /账号登录/);
assert.match(authText, /邮箱或用户名/);
2026-05-06 21:44:20 +08:00
assert.match(authText, /测试账号快速登录/);
assert.doesNotMatch(authText, /隐私政策|使用\s*Google|使用\s*GitHub|第三方快捷/iu);
2026-04-29 12:24:44 +08:00
const root = await fetchWithTimeout(`${baseUrl}/`);
const rootText = await root.text();
2026-05-06 21:44:20 +08:00
assert.equal(root.status, 303, `/ 未登录应跳转 /auth: ${root.status} ${rootText.slice(0, 120)}`);
2026-04-29 12:24:44 +08:00
assert.equal(root.headers.get("x-mnote-web-owner"), "mnote-web");
2026-05-06 21:44:20 +08:00
assert.equal(root.headers.get("location"), "/auth");
const authedRoot = await fetchWithTimeout(`${baseUrl}/`, {
headers: {
"x-mnote-actor-id": "user_real",
"x-mnote-actor-type": "user",
},
});
const authedRootText = await authedRoot.text();
assert.equal(authedRoot.status, 200, `/ 已登录入口失败: ${authedRoot.status}`);
assert.equal(authedRoot.headers.get("x-mnote-web-owner"), "mnote-web");
assert.match(authedRootText, /data-mnote-shell="workspace"/);
2026-04-29 12:24:44 +08:00
}
async function main() {
let gateway = null;
let baseUrl = EXTERNAL_BASE_URL;
if (!baseUrl) {
const port = await findFreePort();
baseUrl = `http://127.0.0.1:${port}`;
gateway = startGateway(port);
await waitForGateway(baseUrl);
}
try {
await validateGateway(baseUrl);
console.log(
JSON.stringify(
{
ok: true,
baseUrl,
owner: "mnote-web",
publicEntry: "3000",
legacy3104: "not-required",
},
null,
2,
),
);
} finally {
if (gateway && gateway.pid) {
gateway.kill("SIGTERM");
setTimeout(() => {
if (!gateway.killed) {
gateway.kill("SIGKILL");
}
}, 2000).unref();
}
}
}
module.exports = {
buildDocumentFixtureEnv,
fetchWithTimeout,
findFreePort,
startGateway,
validateGateway,
waitForGateway,
};
if (require.main === module) {
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});
}