2026-04-29 12:24:44 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
"use strict";
|
|
|
|
|
|
2026-07-28 17:04:27 +08:00
|
|
|
const { loginViaAuthForm } = require('./lib/browser-auth-login');
|
2026-04-29 12:24:44 +08:00
|
|
|
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",
|
|
|
|
|
};
|
|
|
|
|
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-07-28 17:04:27 +08:00
|
|
|
assert.doesNotMatch(authText, /测试账号快速登录/);
|
|
|
|
|
assert.doesNotMatch(authText, /data-auth-test-login/);
|
|
|
|
|
assert.doesNotMatch(authText, /data-test-password/);
|
|
|
|
|
assert.match(authText, /data-auth-submit/);
|
2026-05-06 21:44:20 +08:00
|
|
|
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-28 22:01:44 +08:00
|
|
|
assert.equal(root.headers.get("location"), "/auth?next=%2F");
|
2026-05-06 21:44:20 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|