feat: 支持证书托管和节点自动安装
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
export function certificateFixture(domain = "*.p.xn--876a.net") {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "hy2-cert-fixture-"));
|
||||
try {
|
||||
writeFileSync(
|
||||
path.join(dir, "openssl.cnf"),
|
||||
`[req]\nprompt=no\ndistinguished_name=dn\nx509_extensions=ext\n[dn]\nCN=${domain}\n[ext]\nsubjectAltName=DNS:${domain}\n`,
|
||||
);
|
||||
execFileSync(
|
||||
"openssl",
|
||||
[
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-nodes",
|
||||
"-keyout",
|
||||
path.join(dir, "key"),
|
||||
"-out",
|
||||
path.join(dir, "crt"),
|
||||
"-days",
|
||||
"60",
|
||||
"-config",
|
||||
path.join(dir, "openssl.cnf"),
|
||||
],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
return {
|
||||
certificate: readFileSync(path.join(dir, "crt"), "utf8"),
|
||||
privateKey: readFileSync(path.join(dir, "key"), "utf8"),
|
||||
};
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
+20
-12
@@ -2,17 +2,25 @@ import { test as base, expect } from "@playwright/test";
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
export const test = base.extend<{ isolatedData: void }>({
|
||||
isolatedData: [async ({}, use) => {
|
||||
const file = process.env.HY2_E2E_DATABASE;
|
||||
if (!file || !/^\/tmp\/hy2-panel-ui-test-\d+\.db$/.test(file)) throw new Error("Refusing to reset a non-test database");
|
||||
const db = new Database(file, { fileMustExist: true });
|
||||
db.pragma("foreign_keys = ON");
|
||||
db.transaction(() => {
|
||||
db.exec("DELETE FROM hy2_nodes; DELETE FROM hy2_users WHERE is_admin = 0;");
|
||||
db.prepare("UPDATE app_settings SET value = '[]' WHERE key IN ('main_proxy_group_member_slugs_json','proxy_groups_json')").run();
|
||||
})();
|
||||
db.close();
|
||||
await use();
|
||||
}, { auto: true }],
|
||||
isolatedData: [
|
||||
async ({}, use) => {
|
||||
const file = process.env.HY2_E2E_DATABASE;
|
||||
if (!file || !/^\/tmp\/hy2-panel-ui-test-\d+\.db$/.test(file))
|
||||
throw new Error("Refusing to reset a non-test database");
|
||||
const db = new Database(file, { fileMustExist: true });
|
||||
db.pragma("foreign_keys = ON");
|
||||
db.transaction(() => {
|
||||
db.exec(
|
||||
"DELETE FROM node_installations; DELETE FROM managed_certificates; DELETE FROM hy2_nodes; DELETE FROM hy2_users WHERE is_admin = 0;",
|
||||
);
|
||||
db.prepare(
|
||||
"UPDATE app_settings SET value = '[]' WHERE key IN ('main_proxy_group_member_slugs_json','proxy_groups_json')",
|
||||
).run();
|
||||
})();
|
||||
db.close();
|
||||
await use();
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
});
|
||||
export { expect };
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { test, expect } from "./e2e-fixtures";
|
||||
import { certificateFixture } from "./certificate-fixture";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("upload wildcard certificate, download script and complete NAT node registration", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
const fixture = certificateFixture();
|
||||
await page.goto("/login");
|
||||
await page.getByLabel("用户名", { exact: true }).fill("ui-review");
|
||||
await page.getByLabel("密码", { exact: true }).fill("ui-review-local-2026");
|
||||
await page.getByRole("button", { name: "登录", exact: true }).click();
|
||||
await expect(page).toHaveURL(/dashboard/);
|
||||
await page.getByRole("link", { name: "证书与安装", exact: true }).click();
|
||||
const upload = page.locator("details").filter({
|
||||
has: page.getByRole("heading", { name: "上传证书", exact: true }),
|
||||
});
|
||||
await upload.getByLabel("证书名称").fill("测试通配符证书");
|
||||
await upload.getByLabel("CRT / 完整证书链").setInputFiles({
|
||||
name: "test.crt",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(fixture.certificate),
|
||||
});
|
||||
await upload.getByLabel("KEY / 私钥").setInputFiles({
|
||||
name: "test.key",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from("invalid"),
|
||||
});
|
||||
await upload.getByRole("button", { name: "上传证书", exact: true }).click();
|
||||
await expect(upload.getByRole("alert")).toContainText("无法解析");
|
||||
await upload.getByLabel("KEY / 私钥").setInputFiles({
|
||||
name: "test.key",
|
||||
mimeType: "text/plain",
|
||||
buffer: Buffer.from(fixture.privateKey),
|
||||
});
|
||||
await upload.getByRole("button", { name: "上传证书", exact: true }).click();
|
||||
await expect(
|
||||
page.getByText("测试通配符证书", { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
await page.reload();
|
||||
const create = page.locator("details").filter({
|
||||
has: page.getByRole("heading", {
|
||||
name: "生成服务器安装脚本",
|
||||
exact: true,
|
||||
}),
|
||||
});
|
||||
if ((await create.getAttribute("open")) === null)
|
||||
await create.locator("summary").click();
|
||||
await create.getByLabel("节点名称", { exact: true }).fill("NAT 浏览器测试");
|
||||
await create.getByLabel("节点 Slug", { exact: true }).fill("nat-browser");
|
||||
await create
|
||||
.getByLabel("托管证书", { exact: true })
|
||||
.selectOption({ label: "测试通配符证书" });
|
||||
await create
|
||||
.getByRole("button", { name: "生成安装脚本", exact: true })
|
||||
.click();
|
||||
const row = page.locator(".provision-row").filter({ hasText: "nat-browser" });
|
||||
await expect(row).toContainText("等待安装");
|
||||
const downloadPath = await row
|
||||
.getByRole("link", { name: "下载 SH" })
|
||||
.getAttribute("href");
|
||||
expect((await request.get(downloadPath!, { maxRedirects: 0 })).status()).toBe(
|
||||
307,
|
||||
);
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
row.getByRole("link", { name: "下载 SH" }).click(),
|
||||
]);
|
||||
const script = readFileSync((await download.path())!, "utf8");
|
||||
expect(script).not.toContain("PRIVATE KEY");
|
||||
expect(script).toContain("--domain");
|
||||
const token = script.match(/^TOKEN='([a-f0-9]+)'$/m)![1];
|
||||
const ports = {
|
||||
domain: "nat.p.xn--876a.net",
|
||||
hy2Port: 23456,
|
||||
statsPort: 23457,
|
||||
hy2ListenPort: 443,
|
||||
statsListenPort: 9999,
|
||||
};
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", { data: { action: "prepare", ports } })
|
||||
).status(),
|
||||
).toBe(400);
|
||||
expect(
|
||||
(await request.post("/api/install", { data: "x".repeat(5000) })).status(),
|
||||
).toBe(413);
|
||||
const prepare = await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "prepare", ports },
|
||||
});
|
||||
expect(prepare.status()).toBe(200);
|
||||
expect(prepare.headers()["cache-control"]).toBe("no-store");
|
||||
const bundle = await prepare.json();
|
||||
expect(bundle.privateKey).toBe(fixture.privateKey);
|
||||
const config = parse(bundle.config);
|
||||
expect(config.listen).toBe(":443");
|
||||
expect(config.trafficStats.listen).toBe(":9999");
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "complete" },
|
||||
})
|
||||
).status(),
|
||||
).toBe(200);
|
||||
await page.reload();
|
||||
await expect(row).toContainText("已安装");
|
||||
await expect(row.getByRole("link", { name: "下载 SH" })).toHaveCount(0);
|
||||
expect(
|
||||
(
|
||||
await request.post("/api/install", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
data: { action: "prepare", ports },
|
||||
})
|
||||
).status(),
|
||||
).toBe(400);
|
||||
await page.screenshot({
|
||||
path: "test-results/certificates-desktop.png",
|
||||
fullPage: true,
|
||||
});
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.screenshot({
|
||||
path: "test-results/certificates-mobile.png",
|
||||
fullPage: true,
|
||||
});
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth + 1,
|
||||
),
|
||||
).toBe(true);
|
||||
await page.getByRole("link", { name: "节点管理", exact: true }).click();
|
||||
const node = page
|
||||
.locator(".entity-table__item")
|
||||
.filter({ hasText: "nat-browser" });
|
||||
await expect(node).toContainText("nat.p.xn--876a.net");
|
||||
await node.locator(":scope > summary").click();
|
||||
await expect(node.getByLabel("对外端口", { exact: true })).toHaveValue(
|
||||
"23456",
|
||||
);
|
||||
await expect(node.getByLabel("SNI", { exact: true })).toHaveValue(
|
||||
"nat.p.xn--876a.net",
|
||||
);
|
||||
await expect(node.getByLabel("启用节点", { exact: true })).toBeChecked();
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { certificateFixture } from "../certificate-fixture";
|
||||
|
||||
// Execute the generated shell in a disposable filesystem. Only systemctl and
|
||||
// network effects are mocked; bash, jq, openssl, permissions and file writes run.
|
||||
for (const scenario of [
|
||||
"success",
|
||||
"health-failure",
|
||||
"occupied",
|
||||
"bad-domain",
|
||||
]) {
|
||||
test(`installer shell: ${scenario}`, () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "hy2-shell-test-"));
|
||||
const fake = path.join(dir, "bin");
|
||||
mkdirSync(fake);
|
||||
mkdirSync(path.join(dir, "systemd"));
|
||||
const executable = (name: string, body: string) =>
|
||||
writeFileSync(path.join(fake, name), `#!/usr/bin/env bash\n${body}\n`, {
|
||||
mode: 0o700,
|
||||
});
|
||||
const fixture = certificateFixture();
|
||||
writeFileSync(
|
||||
path.join(dir, "bundle.json"),
|
||||
JSON.stringify({
|
||||
...fixture,
|
||||
config: "listen: :443\n",
|
||||
statsSecret: "test-stats",
|
||||
}),
|
||||
);
|
||||
executable("id", "echo 0");
|
||||
executable("uname", "echo x86_64");
|
||||
executable("sleep", "exit 0");
|
||||
executable(
|
||||
"systemctl",
|
||||
`echo "$*" >> "$SANDBOX/systemctl.log"
|
||||
if [ "$1" = is-active ]; then
|
||||
[ "$3" = hy2-panel-node.service ] && [ "$SCENARIO" != health-failure ]; exit $?
|
||||
fi
|
||||
exit 0`,
|
||||
);
|
||||
executable(
|
||||
"curl",
|
||||
`url=""; output=""; request=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-o) output=$2; shift 2;;
|
||||
--data-binary) request=$2; shift 2;;
|
||||
http*) url=$1; shift;;
|
||||
*) shift;;
|
||||
esac
|
||||
done
|
||||
case "$url" in
|
||||
https://download.hysteria.network/*) printf '#!/usr/bin/env bash\\nexit 0\\n' > "$output";;
|
||||
*/api/install)
|
||||
cp "\${request#@}" "$SANDBOX/last-request.json"
|
||||
if jq -e '.action == "prepare"' "\${request#@}" >/dev/null; then
|
||||
cp "\${request#@}" "$SANDBOX/prepare.json"
|
||||
cp "$SANDBOX/bundle.json" "$output"
|
||||
else echo '{"ok":true}' > "$output"; touch "$SANDBOX/completed"; fi
|
||||
printf 200;;
|
||||
*/traffic) [ "$SCENARIO" != health-failure ];;
|
||||
*) exit 1;;
|
||||
esac`,
|
||||
);
|
||||
const template = readFileSync("lib/templates/install-node.sh", "utf8")
|
||||
.replaceAll("__PANEL__", "'https://panel.example'")
|
||||
.replaceAll("__TOKEN__", "'test-token'")
|
||||
.replaceAll("__INSTALL_ID__", "'test-job'")
|
||||
.replaceAll("__SLUG__", "'test-node'")
|
||||
.replaceAll("/run/systemd/system", path.join(dir, "systemd"))
|
||||
.replaceAll("/etc/hysteria", path.join(dir, "hysteria"))
|
||||
.replaceAll("/etc/systemd/system", path.join(dir, "systemd"))
|
||||
.replaceAll("/usr/local/lib/hy2-panel", path.join(dir, "lib"));
|
||||
const script = path.join(dir, "install.sh");
|
||||
writeFileSync(script, template);
|
||||
if (scenario === "occupied") {
|
||||
mkdirSync(path.join(dir, "hysteria"));
|
||||
writeFileSync(path.join(dir, "hysteria/config.yaml"), "original-config");
|
||||
}
|
||||
try {
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
[
|
||||
script,
|
||||
"--domain",
|
||||
scenario === "bad-domain" ? "wrong.example" : "test",
|
||||
"--hy2-port",
|
||||
"23456",
|
||||
"--stats-port",
|
||||
"23457",
|
||||
"--hy2-listen-port",
|
||||
"443",
|
||||
"--stats-listen-port",
|
||||
"9999",
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${fake}:${process.env.PATH}`,
|
||||
SANDBOX: dir,
|
||||
SCENARIO: scenario,
|
||||
},
|
||||
timeout: 20000,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
result.status,
|
||||
scenario === "success" ? 0 : 1,
|
||||
result.stderr + result.stdout,
|
||||
);
|
||||
assert.equal(
|
||||
existsSync(path.join(dir, "completed")),
|
||||
scenario === "success",
|
||||
);
|
||||
if (scenario === "success") {
|
||||
assert.equal(
|
||||
readFileSync(path.join(dir, "hysteria/server.key"), "utf8").trim(),
|
||||
fixture.privateKey.trim(),
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(readFileSync(path.join(dir, "prepare.json"), "utf8")).ports
|
||||
.domain,
|
||||
"test.p.xn--876a.net",
|
||||
);
|
||||
assert.match(
|
||||
readFileSync(path.join(dir, "systemctl.log"), "utf8"),
|
||||
/enable hy2-panel-node/,
|
||||
);
|
||||
}
|
||||
if (scenario === "health-failure")
|
||||
assert.match(
|
||||
readFileSync(path.join(dir, "systemctl.log"), "utf8"),
|
||||
/stop hy2-panel-node.service/,
|
||||
);
|
||||
if (scenario === "occupied")
|
||||
assert.equal(
|
||||
readFileSync(path.join(dir, "hysteria/config.yaml"), "utf8"),
|
||||
"original-config",
|
||||
);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -22,7 +22,12 @@ function snapshot(db: Database.Database) {
|
||||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
|
||||
)
|
||||
.all() as { name: string }[]
|
||||
).map(({ name }) => [name, db.prepare(`SELECT * FROM "${name}"`).all()]),
|
||||
)
|
||||
.filter(
|
||||
({ name }) =>
|
||||
!["managed_certificates", "node_installations"].includes(name),
|
||||
)
|
||||
.map(({ name }) => [name, db.prepare(`SELECT * FROM "${name}"`).all()]),
|
||||
);
|
||||
}
|
||||
function seed(db: Database.Database) {
|
||||
@@ -40,7 +45,7 @@ test("fresh database exactly matches the inspected remote v4 column definitions"
|
||||
const fresh = new Database(":memory:");
|
||||
const remote = remoteDb();
|
||||
migrateDatabase(fresh);
|
||||
assert.equal(fresh.pragma("user_version", { simple: true }), 4);
|
||||
assert.equal(fresh.pragma("user_version", { simple: true }), 5);
|
||||
for (const { name } of remote
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.all() as { name: string }[]) {
|
||||
@@ -93,7 +98,7 @@ for (const existing of [
|
||||
if (!existing.includes(field)) delete row[field];
|
||||
}
|
||||
assert.deepEqual(after, before);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 4);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 5);
|
||||
db.close();
|
||||
});
|
||||
}
|
||||
@@ -123,8 +128,8 @@ test("failed migrations roll back all statements and future schemas are not down
|
||||
db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(),
|
||||
[{ name: "hy2_users" }],
|
||||
);
|
||||
db.pragma("user_version = 5");
|
||||
db.pragma("user_version = 6");
|
||||
assert.throws(() => migrateDatabase(db), /版本/);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 5);
|
||||
assert.equal(db.pragma("user_version", { simple: true }), 6);
|
||||
db.close();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { parse } from "yaml";
|
||||
import { certificateFixture } from "../certificate-fixture";
|
||||
|
||||
test("certificate provisioning preserves credentials, binds domain/ports, supports retry and consumes capability", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "hy2-provision-test-"));
|
||||
process.env.DATABASE_PATH = path.join(dir, "test.db");
|
||||
process.env.APP_URL = "https://panel.example.com";
|
||||
process.env.POLLER_ENABLED = "false";
|
||||
const { ensureDb } = await import("../../lib/db");
|
||||
const { decryptText } = await import("../../lib/crypto");
|
||||
const {
|
||||
saveCertificate,
|
||||
validateCertificate,
|
||||
deleteCertificate,
|
||||
listCertificates,
|
||||
} = await import("../../lib/certificates");
|
||||
const {
|
||||
createInstallation,
|
||||
installationForDownload,
|
||||
provisionNode,
|
||||
revokeInstallation,
|
||||
} = await import("../../lib/provisioning");
|
||||
const { buildInstallScript } = await import("../../lib/install-script");
|
||||
const db = await ensureDb();
|
||||
const fixture = certificateFixture();
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCertificate(
|
||||
fixture.certificate,
|
||||
certificateFixture().privateKey,
|
||||
),
|
||||
/匹配/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateCertificate(
|
||||
fixture.certificate,
|
||||
fixture.privateKey,
|
||||
"a.b.p.xn--876a.net",
|
||||
),
|
||||
/SNI/,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateCertificate("invalid", fixture.privateKey),
|
||||
/解析/,
|
||||
);
|
||||
const certId = Number(
|
||||
await saveCertificate(
|
||||
"Wildcard",
|
||||
fixture.certificate,
|
||||
fixture.privateKey,
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(await listCertificates()).includes("PRIVATE KEY"),
|
||||
false,
|
||||
);
|
||||
const stored = db
|
||||
.prepare("SELECT private_key_encrypted FROM managed_certificates")
|
||||
.get() as { private_key_encrypted: string };
|
||||
assert.notEqual(stored.private_key_encrypted, fixture.privateKey);
|
||||
assert.equal(decryptText(stored.private_key_encrypted), fixture.privateKey);
|
||||
const id = await createInstallation({
|
||||
name: "New server",
|
||||
slug: "new-server",
|
||||
certificateId: certId,
|
||||
});
|
||||
await assert.rejects(
|
||||
() =>
|
||||
createInstallation({
|
||||
name: "Duplicate",
|
||||
slug: "new-server",
|
||||
certificateId: certId,
|
||||
}),
|
||||
/存在/,
|
||||
);
|
||||
await assert.rejects(() => deleteCertificate(certId), /关联/);
|
||||
const job = await installationForDownload(id);
|
||||
const script = buildInstallScript(job);
|
||||
assert.ok(script.includes("--domain"));
|
||||
assert.ok(!script.includes("PRIVATE KEY"));
|
||||
assert.ok(!script.includes("__TOKEN__"));
|
||||
const token = decryptText(job.token_encrypted)!;
|
||||
assert.equal(job.token_hash.includes(token), false);
|
||||
const ports = {
|
||||
domain: "hk.p.xn--876a.net",
|
||||
hy2Port: 23456,
|
||||
statsPort: 23457,
|
||||
hy2ListenPort: 443,
|
||||
statsListenPort: 9999,
|
||||
};
|
||||
await assert.rejects(
|
||||
() => provisionNode("f".repeat(64), "prepare", ports),
|
||||
/无效/,
|
||||
);
|
||||
await assert.rejects(() => provisionNode(token, "complete"), /先获取/);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
provisionNode(token, "prepare", { ...ports, domain: "evil.example" }),
|
||||
/域名/,
|
||||
);
|
||||
await assert.rejects(
|
||||
() => provisionNode(token, "prepare", { ...ports, hy2Port: 65536 }),
|
||||
/端口/,
|
||||
);
|
||||
const bundle = await provisionNode(token, "prepare", ports);
|
||||
assert.ok("config" in bundle);
|
||||
assert.equal(bundle.privateKey, fixture.privateKey);
|
||||
const config = parse(bundle.config);
|
||||
assert.equal(config.listen, ":443");
|
||||
assert.equal(config.trafficStats.listen, ":9999");
|
||||
assert.match(
|
||||
config.auth.http.url,
|
||||
/^https:\/\/panel.example.com\/api\/hy2\/nodes\/new-server\/auth\?token=/,
|
||||
);
|
||||
assert.deepEqual(await provisionNode(token, "prepare", ports), bundle);
|
||||
await assert.rejects(
|
||||
() => provisionNode(token, "prepare", { ...ports, statsPort: 12345 }),
|
||||
/绑定/,
|
||||
);
|
||||
const node = db
|
||||
.prepare("SELECT * FROM hy2_nodes WHERE slug='new-server'")
|
||||
.get() as Record<string, unknown>;
|
||||
assert.equal(node.server_port, 23456);
|
||||
assert.equal(node.traffic_stats_url, "http://hk.p.xn--876a.net:23457");
|
||||
assert.equal(node.sni, ports.domain);
|
||||
assert.equal(node.enabled, 0);
|
||||
await provisionNode(token, "complete");
|
||||
await provisionNode(token, "complete");
|
||||
assert.equal(
|
||||
(
|
||||
db.prepare("SELECT enabled FROM hy2_nodes WHERE id=?").get(node.id) as {
|
||||
enabled: number;
|
||||
}
|
||||
).enabled,
|
||||
1,
|
||||
);
|
||||
await assert.rejects(() => provisionNode(token, "prepare", ports), /完成/);
|
||||
await assert.rejects(() => installationForDownload(id), /完成/);
|
||||
assert.equal(
|
||||
(db.prepare("SELECT COUNT(*) AS n FROM hy2_nodes").get() as { n: number })
|
||||
.n,
|
||||
1,
|
||||
);
|
||||
const retryId = await createInstallation({
|
||||
name: "Retry",
|
||||
slug: "retry",
|
||||
certificateId: certId,
|
||||
});
|
||||
const retryToken = decryptText(
|
||||
(await installationForDownload(retryId)).token_encrypted,
|
||||
)!;
|
||||
await revokeInstallation(retryId);
|
||||
await assert.rejects(
|
||||
() => provisionNode(retryToken, "prepare", ports),
|
||||
/无效/,
|
||||
);
|
||||
const expiredId = await createInstallation({
|
||||
name: "Expired",
|
||||
slug: "expired",
|
||||
certificateId: certId,
|
||||
});
|
||||
const expiredToken = decryptText(
|
||||
(await installationForDownload(expiredId)).token_encrypted,
|
||||
)!;
|
||||
db.prepare(
|
||||
"UPDATE node_installations SET expires_at='2000-01-01' WHERE id=?",
|
||||
).run(expiredId);
|
||||
await assert.rejects(
|
||||
() => provisionNode(expiredToken, "prepare", ports),
|
||||
/过期/,
|
||||
);
|
||||
await revokeInstallation(id, true);
|
||||
assert.equal(
|
||||
(db.prepare("SELECT COUNT(*) AS n FROM hy2_nodes").get() as { n: number })
|
||||
.n,
|
||||
1,
|
||||
);
|
||||
await revokeInstallation(retryId, true);
|
||||
await revokeInstallation(expiredId, true);
|
||||
await deleteCertificate(certId);
|
||||
assert.equal((await listCertificates()).length, 0);
|
||||
assert.deepEqual(db.pragma("foreign_key_check"), []);
|
||||
} finally {
|
||||
db.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user