// server/__tests__/run.test.js /** * @file run.test.js * @description Route tests for the terminal-run feature: same-origin guard, * laneId/cwd validation, spawn/kill/list against a mocked tmux backend. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after, beforeEach } = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const http = require("node:http"); const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "run-route-test-")); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); const { createApp } = require("../index"); const tmux = require("../lib/tmux"); let server; let BASE; function fetchJson(p, opts = {}) { return new Promise((resolve, reject) => { const url = new URL(p, BASE); const headers = { ...(opts.headers || {}) }; let body; if (opts.body !== undefined) { body = Buffer.from(JSON.stringify(opts.body)); headers["Content-Type"] = "application/json"; headers["Content-Length"] = body.length; } const req = http.request( { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: opts.method || "GET", headers, }, (res) => { const chunks = []; res.on("data", (c) => chunks.push(c)); res.on("end", () => { const raw = Buffer.concat(chunks).toString("utf8"); let parsed; try { parsed = JSON.parse(raw); } catch { parsed = raw; } resolve({ status: res.statusCode, body: parsed }); }); } ); req.on("error", reject); if (body) req.write(body); req.end(); }); } describe("/api/run", () => { before(async () => { const app = createApp(); server = http.createServer(app); await new Promise((r) => server.listen(0, r)); BASE = `http://127.0.0.1:${server.address().port}`; }); after(async () => { await new Promise((r) => server.close(r)); try { fs.rmSync(TMP, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } catch { /* best-effort */ } }); beforeEach(() => { tmux.__reset(); }); it("rejects cross-origin browser requests", async () => { const { status, body } = await fetchJson("/api/run", { headers: { Origin: "http://evil.example.com" }, }); assert.equal(status, 403); assert.equal(body.error.code, "EBADORIGIN"); }); it("allows requests with no Origin (CLI/curl)", async () => { tmux.__setExecImpl((args) => { if (args[0] === "list-sessions") { const e = new Error("no server running"); e.status = 1; throw e; } return ""; }); const { status, body } = await fetchJson("/api/run"); assert.equal(status, 200); assert.deepEqual(body.items, []); }); it("POST / rejects a missing laneId", async () => { const { status, body } = await fetchJson("/api/run", { method: "POST", body: { cwd: TMP } }); assert.equal(status, 400); assert.equal(body.error.code, "EBADLANE"); }); it("POST / rejects a non-existent cwd", async () => { const { status, body } = await fetchJson("/api/run", { method: "POST", body: { laneId: 1, cwd: "/definitely/not/a/real/path" }, }); assert.equal(status, 400); assert.equal(body.error.code, "EBADCWD"); }); it("POST / spawns a tmux session and GET /:id finds it", async () => { let hasSessionCalls = 0; tmux.__setExecImpl((args) => { if (args[0] === "has-session") { hasSessionCalls++; // First call (inside spawnRun): not yet running. Every call after // (GET /:id) sees it as running. if (hasSessionCalls === 1) { const e = new Error("gone"); e.status = 1; throw e; } return ""; } return ""; }); const spawned = await fetchJson("/api/run", { method: "POST", body: { laneId: 9, cwd: TMP } }); assert.equal(spawned.status, 201); assert.equal(spawned.body.id, "ccam-lane-9"); const fetched = await fetchJson(`/api/run/${spawned.body.id}`); assert.equal(fetched.status, 200); assert.equal(fetched.body.status, "running"); }); it("DELETE /:id kills a live tmux session", async () => { tmux.__setExecImpl(() => ""); // has-session succeeds; kill-session succeeds const { status, body } = await fetchJson("/api/run/ccam-lane-9", { method: "DELETE" }); assert.equal(status, 200); assert.deepEqual(body, { ok: true }); }); it("DELETE /:id returns 404 for a session that doesn't exist", async () => { tmux.__setExecImpl((args) => { if (args[0] === "has-session") { const e = new Error("gone"); e.status = 1; throw e; } return ""; }); const { status } = await fetchJson("/api/run/ccam-lane-999", { method: "DELETE" }); assert.equal(status, 404); }); it("GET /tmux reports availability from the tmux wrapper", async () => { tmux.__setExecImpl(() => "tmux 3.4"); const { body } = await fetchJson("/api/run/tmux"); assert.equal(body.available, true); }); it("GET /cwds still returns suggested directories (unchanged behavior)", async () => { const { status, body } = await fetchJson("/api/run/cwds"); assert.equal(status, 200); assert.ok(Array.isArray(body.items)); }); });