Files
LinkPort/web/tests/startup-settings.spec.ts
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
2026-10-13 19:43:23 +08:00

89 lines
4.3 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { spawn, type ChildProcess } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { once } from "node:events";
import { createServer } from "node:net";
test("保存端口后实际重启保持地址,环境变量可覆盖", async () => {
const directory = await mkdtemp(join(tmpdir(), "linkport-startup-"));
let child: ChildProcess | undefined;
const blocker = createServer();
async function stop() {
if (child && child.exitCode === null && child.signalCode === null) {
const exited = once(child, "exit");
child.kill();
await exited;
}
}
async function start(override = "") {
child = spawn(resolve(process.env.LINKPORT_E2E_BINARY!), [], {
windowsHide: true,
env: { ...process.env, LINKPORT_DATA: join(directory, "test.sqlite"), LINKPORT_NO_BROWSER: "1",
LINKPORT_AUTH_ENABLED: "false", LINKPORT_HTTP_ADDR: override, LINKPORT_SECURE_COOKIE: "false" },
stdio: ["ignore", "pipe", "pipe"],
});
const processHandle = child;
return await new Promise<string>((resolveAddress, reject) => {
let output = "";
const timeout = setTimeout(() => reject(Error("startup timed out: " + output)), 10000);
processHandle.on("error", error => { clearTimeout(timeout); reject(error); });
processHandle.on("exit", () => { clearTimeout(timeout); reject(Error(output)); });
const capture = (data: Buffer) => {
output += data.toString();
const match = output.match(/linkport management listening on (127\.0\.0\.1:\d+)/);
if (match) { clearTimeout(timeout); resolveAddress("http://" + match[1]); }
};
processHandle.stdout!.on("data", capture);
processHandle.stderr!.on("data", capture);
});
}
try {
const address = await start();
const port = Number(new URL(address).port);
const session = await (await fetch(address + "/api/v1/session")).json();
const saved = await fetch(address + "/api/v1/configuration/startup", { method: "PUT",
headers: { "Content-Type": "application/json", "X-CSRF-Token": session.csrfToken, Origin: address },
body: JSON.stringify({ httpPort: port }) });
expect(saved.ok, await saved.text()).toBeTruthy();
await stop();
expect(await start()).toBe(address);
await stop();
blocker.listen(port, "127.0.0.1");
await once(blocker, "listening");
await expect(start()).rejects.toThrow("cannot listen");
await stop();
expect(await start("127.0.0.1:0")).not.toBe(address);
} finally {
await stop();
if (blocker.listening) await new Promise<void>(done => blocker.close(() => done()));
await rm(directory, { recursive: true, force: true });
}
});
test("启动端口校验、保存与重新加载", async ({ page }) => {
await page.goto("/");
const session = await (await page.request.get("/api/v1/session")).json();
const headers = { "X-CSRF-Token": session.csrfToken };
const original = await (await page.request.get("/api/v1/configuration/startup")).json();
try {
await page.getByRole("navigation").getByRole("button", { name: "配置管理", exact: true }).click();
const field = page.getByRole("spinbutton", { name: "管理端口", exact: true });
await expect(field).toBeEnabled();
await expect(page.getByRole("button", { name: "保存启动端口", exact: true })).toBeVisible();
await field.fill("65536");
await page.getByRole("button", { name: "保存启动端口", exact: true }).click();
await expect(field).toHaveAttribute("aria-invalid", "true");
await field.fill("25989");
await page.getByRole("button", { name: "保存启动端口", exact: true }).click();
await expect(page.getByText("启动端口已保存", { exact: true })).toBeVisible();
expect((await (await page.request.get("/api/v1/configuration/startup")).json()).httpPort).toBe(25989);
await page.reload();
await page.getByRole("navigation").getByRole("button", { name: "配置管理", exact: true }).click();
await expect(field).toHaveValue("25989");
} finally {
if (!page.isClosed()) await page.request.put("/api/v1/configuration/startup", { headers, data: { httpPort: original.httpPort }, timeout: 5000 }).catch(() => {});
}
});