Spaces:
Runtime error
Runtime error
| /** | |
| * 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"); | |
| }); | |