263 lines
8.1 KiB
JavaScript
263 lines
8.1 KiB
JavaScript
/*
|
|
* routes/secrets.js + routes/internal.js · 路由烟测
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
|
*
|
|
* 跑 Express app 实例 + supertest-like 风格 (用 http.request 直连本机端口).
|
|
* 不依赖 supertest, 只用 Node 内置.
|
|
*/
|
|
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const http = require("http");
|
|
|
|
// 测试用 manifest (不污染主清单)
|
|
function makeTempManifest() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vault-mft-"));
|
|
const p = path.join(dir, "secrets-manifest.json");
|
|
fs.writeFileSync(
|
|
p,
|
|
JSON.stringify({
|
|
version: "test",
|
|
workflows: {
|
|
"test-flow": {
|
|
_describe: "测试流",
|
|
secrets: [
|
|
{ name: "ZY_TEST_SIMPLE", level: "required", 用途: "测试用" },
|
|
{
|
|
name: "ZY_TEST_STRONG",
|
|
level: "required",
|
|
用途: "强校验测试",
|
|
强校验: "length>=12"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
})
|
|
);
|
|
return { dir, manifestPath: p };
|
|
}
|
|
|
|
function startApp() {
|
|
const tmpVault = fs.mkdtempSync(path.join(os.tmpdir(), "vault-routes-"));
|
|
const tmpEndpoint = path.join(tmpVault, "inference-endpoint.json");
|
|
const { manifestPath } = makeTempManifest();
|
|
|
|
process.env.VAULT_DIR = tmpVault;
|
|
process.env.INFERENCE_ENDPOINT_PATH = tmpEndpoint;
|
|
process.env.VAULT_MANIFEST_PATH = manifestPath;
|
|
process.env.VAULT_SKIP_PM_RELOAD = "1";
|
|
|
|
// require lazily 让 env 先生效
|
|
delete require.cache[require.resolve("../server")];
|
|
const { app } = require("../server");
|
|
return { app, tmpVault, tmpEndpoint, manifestPath };
|
|
}
|
|
|
|
function listen(app) {
|
|
return new Promise((resolve) => {
|
|
const server = app.listen(0, "127.0.0.1", () => {
|
|
const port = server.address().port;
|
|
resolve({ server, port });
|
|
});
|
|
});
|
|
}
|
|
|
|
function req(port, method, path_, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const data = body == null ? null : JSON.stringify(body);
|
|
const r = http.request(
|
|
{
|
|
host: "127.0.0.1",
|
|
port,
|
|
method,
|
|
path: path_,
|
|
headers: data
|
|
? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) }
|
|
: {}
|
|
},
|
|
(res) => {
|
|
let buf = "";
|
|
res.on("data", (c) => (buf += c));
|
|
res.on("end", () => {
|
|
let json = null;
|
|
try {
|
|
json = JSON.parse(buf);
|
|
} catch (_) {
|
|
/* ignore */
|
|
}
|
|
resolve({ status: res.statusCode, body: json, raw: buf });
|
|
});
|
|
}
|
|
);
|
|
r.on("error", reject);
|
|
if (data) r.write(data);
|
|
r.end();
|
|
});
|
|
}
|
|
|
|
test("GET /admin/__healthz 返回 vault 状态", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "GET", "/admin/__healthz");
|
|
assert.strictEqual(r.status, 200);
|
|
assert.strictEqual(r.body.ok, true);
|
|
assert.strictEqual(r.body.role, "secrets-vault");
|
|
assert.strictEqual(r.body.has_master, true);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("GET /admin/secrets/manifest 按 workflow 分组", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "GET", "/admin/secrets/manifest");
|
|
assert.strictEqual(r.status, 200);
|
|
assert.ok(Array.isArray(r.body.groups));
|
|
const tf = r.body.groups.find((g) => g.workflow === "test-flow");
|
|
assert.ok(tf, "应包含 test-flow 组");
|
|
assert.strictEqual(tf.secrets.length, 2);
|
|
assert.strictEqual(tf.secrets[0].用途, "测试用");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("POST /admin/secrets/:key — 白名单外拒绝", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "POST", "/admin/secrets/RANDOM_KEY_NAME", { value: "v" });
|
|
assert.strictEqual(r.status, 400);
|
|
assert.strictEqual(r.body.code, "unknown_key");
|
|
assert.ok(/中文|不在.*白名单/.test(r.body.message), "应有中文回执");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("POST /admin/secrets/:key — key 名格式错拒绝", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "POST", "/admin/secrets/lowercase_bad", { value: "v" });
|
|
assert.strictEqual(r.status, 400);
|
|
assert.strictEqual(r.body.code, "invalid_key");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("POST /admin/secrets/:key — 强校验长度不够拒绝", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "POST", "/admin/secrets/ZY_TEST_STRONG", { value: "short" });
|
|
assert.strictEqual(r.status, 400);
|
|
assert.strictEqual(r.body.code, "weak_value");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("POST + GET + DELETE 完整往返 (值不返回明文, 只遮罩)", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const w = await req(port, "POST", "/admin/secrets/ZY_TEST_SIMPLE", {
|
|
value: "abcdef-this-is-secret"
|
|
});
|
|
assert.strictEqual(w.status, 200);
|
|
assert.strictEqual(w.body.ok, true);
|
|
assert.ok(w.body.masked.includes("•"), "应遮罩");
|
|
|
|
const list = await req(port, "GET", "/admin/secrets/");
|
|
assert.strictEqual(list.status, 200);
|
|
assert.deepStrictEqual(list.body.configured_keys, ["ZY_TEST_SIMPLE"]);
|
|
assert.ok(list.body.values.ZY_TEST_SIMPLE.masked.includes("•"));
|
|
assert.ok(
|
|
!JSON.stringify(list.body).includes("abcdef-this-is-secret"),
|
|
"明文绝对不能出现在列表响应里"
|
|
);
|
|
|
|
const del = await req(port, "DELETE", "/admin/secrets/ZY_TEST_SIMPLE");
|
|
assert.strictEqual(del.status, 200);
|
|
|
|
const list2 = await req(port, "GET", "/admin/secrets/");
|
|
assert.deepStrictEqual(list2.body.configured_keys, []);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("/internal/fetch/:key 只允许本机 (loopback 通)", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
// 先存
|
|
await req(port, "POST", "/admin/secrets/ZY_TEST_SIMPLE", { value: "internal-val" });
|
|
|
|
// 本机 loopback 应该通
|
|
const r = await req(port, "GET", "/internal/fetch/ZY_TEST_SIMPLE");
|
|
assert.strictEqual(r.status, 200);
|
|
assert.strictEqual(r.body.value, "internal-val");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("/internal/fetch/:key — 未配置 key 返回 404", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "GET", "/internal/fetch/ZY_TEST_SIMPLE");
|
|
assert.strictEqual(r.status, 404);
|
|
assert.strictEqual(r.body.code, "not_configured");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("autodl/save_and_refresh — host/port 缺失 → 400", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
const r = await req(port, "POST", "/admin/secrets/autodl/save_and_refresh", { host: "" });
|
|
assert.strictEqual(r.status, 400);
|
|
assert.strictEqual(r.body.code, "host_required");
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
test("autodl/save_and_refresh — 探活失败仍然把 vault 落了 (vault_saved=true)", async () => {
|
|
const ctx = startApp();
|
|
const { server, port } = await listen(ctx.app);
|
|
try {
|
|
// 用一个保证不可达的端口
|
|
const r = await req(port, "POST", "/admin/secrets/autodl/save_and_refresh", {
|
|
host: "127.0.0.1",
|
|
port: "1",
|
|
scheme: "http"
|
|
});
|
|
assert.strictEqual(r.status, 502);
|
|
assert.strictEqual(r.body.error, true);
|
|
assert.strictEqual(r.body.vault_saved, true);
|
|
assert.strictEqual(r.body.endpoint_refreshed, false);
|
|
assert.strictEqual(r.body.code, "health_failed");
|
|
|
|
// 确认 vault 真的写了 host/port
|
|
const list = await req(port, "GET", "/admin/secrets/");
|
|
assert.ok(list.body.configured_keys.includes("ZY_AUTODL_HOST"));
|
|
assert.ok(list.body.configured_keys.includes("ZY_AUTODL_PORT"));
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|