Spaces:
Runtime error
Runtime error
File size: 7,837 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | /**
* T-04 lifecycle endpoint tests.
*
* Tests HTTP handlers directly (bypassing Next.js routing) with a real
* ServiceSupervisor whose start/stop methods are patched via mock.fn().
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { mock } from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-lifecycle-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Bootstrap DB
const core = await import("../../../src/lib/db/core.ts");
const db = core.getDbInstance();
// Seed service rows
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, getVersionManagerTool } =
await import("../../../src/lib/db/versionManager.ts");
// Set version so installer functions have something to read
await updateVersionManagerTool("9router", { installedVersion: "0.4.59", status: "stopped" });
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
const { registerSupervisor, getSupervisor } = await import("../../../src/lib/services/registry.ts");
/** Creates a minimal ServiceSupervisor with fake spawnArgs (no real process). */
function makeFakeSup(tool: string) {
return new ServiceSupervisor({
tool,
port: 20130,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 60000)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => `http://127.0.0.1:20130/api/health`,
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
}
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// βββ status endpoint ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test("GET /status returns not_installed when no installer version", async () => {
await updateVersionManagerTool("9router", { installedVersion: null, status: "not_installed" });
const { GET } = await import("../../../src/app/api/services/9router/status/route.ts?t=status-1");
const resp = await GET();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok(["not_installed", "unknown", "stopped"].includes(body.state as string));
// restore
await updateVersionManagerTool("9router", { installedVersion: "0.4.59", status: "stopped" });
});
test("GET /status returns enriched shape", async () => {
const { GET } = await import("../../../src/app/api/services/9router/status/route.ts?t=status-2");
const resp = await GET();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok("state" in body, "should have state");
assert.ok("installedVersion" in body, "should have installedVersion");
assert.ok("updateAvailable" in body, "should have updateAvailable");
assert.ok("apiKeyMasked" in body, "should have apiKeyMasked");
});
// βββ start endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test("POST /start returns 409 when not_installed", async () => {
await updateVersionManagerTool("9router", { status: "not_installed", installedVersion: null });
const { POST } = await import("../../../src/app/api/services/9router/start/route.ts?t=start-1");
const resp = await POST();
assert.equal(resp.status, 409);
const body = await resp.json();
assert.ok(body.error?.message?.includes("instalado"), "should explain not installed");
// restore
await updateVersionManagerTool("9router", { status: "stopped", installedVersion: "0.4.59" });
});
test("POST /start returns 200 when already running", async () => {
const sup = makeFakeSup("9router");
// Patch start() to return immediately with running state
const origStart = sup.start.bind(sup);
void origStart;
mock.method(sup, "start", async () => ({
tool: "9router",
state: "running" as const,
pid: 12345,
port: 20130,
health: "healthy" as const,
startedAt: new Date().toISOString(),
lastError: null,
}));
registerSupervisor(sup);
await updateVersionManagerTool("9router", { status: "stopped", installedVersion: "0.4.59" });
const { POST } = await import("../../../src/app/api/services/9router/start/route.ts?t=start-2");
const resp = await POST();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.equal(body.state, "running");
mock.restoreAll();
});
// βββ stop endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
test("POST /stop returns stopped even if supervisor absent", async () => {
// Remove from registry by re-importing fresh
const { POST } = await import("../../../src/app/api/services/9router/stop/route.ts?t=stop-1");
const resp = await POST();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok(["stopped", "running", "error"].includes(body.state as string));
});
// βββ auto-start endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββ
test("POST /auto-start toggles DB field", async () => {
const { POST } = await import("../../../src/app/api/services/9router/auto-start/route.ts");
const reqEnable = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ enabled: true }),
});
const resp1 = await POST(reqEnable);
assert.equal(resp1.status, 204);
const row1 = await getVersionManagerTool("9router");
assert.ok(row1?.autoStart === true, "autoStart should be true after enable");
const reqDisable = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ enabled: false }),
});
const resp2 = await POST(reqDisable);
assert.equal(resp2.status, 204);
const row2 = await getVersionManagerTool("9router");
assert.ok(row2?.autoStart === false, "autoStart should be false after disable");
});
test("POST /auto-start returns 400 for invalid body", async () => {
const { POST } =
await import("../../../src/app/api/services/9router/auto-start/route.ts?t=as-bad");
const req = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ enabled: "yes" }), // should be boolean
});
const resp = await POST(req);
assert.equal(resp.status, 400);
});
// βββ rotate-key endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββ
test("POST /rotate-key does not leak plain key in response", async () => {
const { POST } = await import("../../../src/app/api/services/9router/rotate-key/route.ts");
const resp = await POST();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok(body.keyRotated === true, "keyRotated should be true");
assert.ok(!("key" in body), "response must not contain 'key' field");
assert.ok(!("apiKey" in body), "response must not contain 'apiKey' field");
assert.ok(!("plainKey" in body), "response must not contain 'plainKey' field");
// Ensure no string in the response looks like our key prefix
const bodyStr = JSON.stringify(body);
assert.ok(!bodyStr.includes("nr_"), "response must not contain plain key value");
});
|