Files
2026-07-13 13:39:12 +08:00

128 lines
4.7 KiB
TypeScript

/**
* Tests for GET /api/services/9router/status?reveal=key (R-01)
*
* - Without ?reveal: returns only masked key
* - With ?reveal=key but missing X-Reveal-Confirm: returns 403
* - With ?reveal=key and X-Reveal-Confirm: yes: returns apiKeyPlain + logs audit
*/
import { describe, it, before, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-9router-reveal-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Bootstrap DB and seed service row
const core = await import("../../../../src/lib/db/core.ts");
const db = core.getDbInstance();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('9router', 'stopped', 20130, 0, 1, 1)`
).run();
const { updateVersionManagerTool } = await import("../../../../src/lib/db/versionManager.ts");
await updateVersionManagerTool("9router", {
installedVersion: "0.4.59",
status: "stopped",
});
// Ensure audit_log table exists (compliance init)
const { initAuditLog, countAuditLog } = await import("../../../../src/lib/compliance/index.ts");
initAuditLog();
// Import GET after env is set
const { GET } =
await import("../../../../src/app/api/services/9router/status/route.ts?t=reveal-suite");
function makeRequest(url: string, headers?: Record<string, string>): Request {
return new Request(url, { headers });
}
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
describe("GET /api/services/9router/status", () => {
it("without ?reveal returns only masked key (no plain)", async () => {
const req = makeRequest("http://localhost/api/services/9router/status");
const res = await GET(req);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok("apiKeyMasked" in body, "should have apiKeyMasked");
assert.ok(!("apiKeyPlain" in body), "should NOT have apiKeyPlain");
});
it("?reveal=key without X-Reveal-Confirm returns 403", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key");
const res = await GET(req);
assert.equal(res.status, 403);
const body = await res.json();
assert.ok(
body.error?.message?.toLowerCase().includes("confirm") ||
body.error?.message?.toLowerCase().includes("header") ||
body.message?.toLowerCase().includes("confirm") ||
String(body).toLowerCase().includes("confirm"),
`body should mention confirmation: ${JSON.stringify(body)}`
);
});
it("?reveal=key with wrong X-Reveal-Confirm value returns 403", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "no",
});
const res = await GET(req);
assert.equal(res.status, 403);
});
it("?reveal=key with X-Reveal-Confirm: yes returns apiKeyPlain", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "yes",
});
const res = await GET(req);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(typeof body.apiKeyPlain === "string", "should have apiKeyPlain string");
assert.ok(body.apiKeyPlain.startsWith("nr_"), "plain key should start with nr_");
// masked should also be present
assert.ok("apiKeyMasked" in body, "should still have apiKeyMasked");
});
it("reveal logs an audit entry with action=service.reveal_api_key", async () => {
const countBefore = countAuditLog({ action: "service.reveal_api_key" });
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "yes",
});
const res = await GET(req);
assert.equal(res.status, 200);
const countAfter = countAuditLog({ action: "service.reveal_api_key" });
assert.equal(
countAfter,
countBefore + 1,
`audit entries should increase by 1 (was ${countBefore}, now ${countAfter})`
);
});
it("plain key is different from masked key (not trivially masked)", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "yes",
});
const res = await GET(req);
const body = await res.json();
assert.notEqual(body.apiKeyPlain, body.apiKeyMasked, "plain and masked should differ");
assert.ok(
body.apiKeyPlain.length > body.apiKeyMasked.length ||
!body.apiKeyMasked.includes(body.apiKeyPlain),
"masked should not contain the full plain key"
);
});
});