test(api): integration tests for traffic-inspector routes (F6)

This commit is contained in:
diegosouzapw
2026-05-28 01:24:33 -03:00
parent 668010bcf2
commit 1d9cb7eb03
8 changed files with 1462 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
/**
* Integration tests: Traffic Inspector capture-modes endpoints
*
* Tests:
* - GET /capture-modes — status overview
* - POST /capture-modes/http-proxy — start/stop (ephemeral port to avoid 8080 conflict)
* - POST /capture-modes/system-proxy — apply/revert (mocked OS commands)
* - POST /capture-modes/tls-intercept — toggle
*/
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 net from "node:net";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-capture-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.INSPECTOR_HTTP_PROXY_PORT = "0"; // ephemeral port
const captureModesRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/route.ts"
);
const httpProxyRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/http-proxy/route.ts"
);
const systemProxyRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/system-proxy/route.ts"
);
const tlsInterceptRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/tls-intercept/route.ts"
);
const { setHttpProxyHandle, getHttpProxyHandle, clearSystemProxy } = await import(
"../../src/lib/inspector/captureState.ts"
);
const { __setExec } = await import(
"../../src/mitm/inspector/systemProxyConfig.ts"
);
test.beforeEach(() => {
// Ensure no running proxy handle leaks between tests
const handle = getHttpProxyHandle();
if (handle) {
handle.stop().catch(() => {/* ignore */});
setHttpProxyHandle(null);
}
clearSystemProxy();
});
test.after(() => {
// Clean up any running proxy
const handle = getHttpProxyHandle();
if (handle) {
handle.stop().catch(() => {/* ignore */});
setHttpProxyHandle(null);
}
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ── GET /capture-modes ──────────────────────────────────────────────────────
test("GET /capture-modes: returns status of all modes", async () => {
const res = await captureModesRoute.GET();
assert.equal(res.status, 200);
const body = await res.json() as {
agentBridge: boolean;
httpProxy: { running: boolean; port: number | null };
systemProxy: { applied: boolean };
tlsIntercept: { enabled: boolean };
};
assert.equal(body.agentBridge, true);
assert.equal(body.httpProxy.running, false);
assert.equal(body.systemProxy.applied, false);
assert.ok("enabled" in body.tlsIntercept);
});
// ── POST /capture-modes/http-proxy ─────────────────────────────────────────
test("http-proxy: start binds an ephemeral port", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/http-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "start" }),
}
);
const res = await httpProxyRoute.POST(req);
assert.equal(res.status, 201);
const body = await res.json() as { ok: boolean; running: boolean; port: number };
assert.equal(body.ok, true);
assert.equal(body.running, true);
assert.ok(body.port > 0, "should have a bound port");
// Clean up
const handle = getHttpProxyHandle();
if (handle) {
await handle.stop();
setHttpProxyHandle(null);
}
});
test("http-proxy: stop when not running returns ok", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/http-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "stop" }),
}
);
const res = await httpProxyRoute.POST(req);
assert.equal(res.status, 200);
const body = await res.json() as { ok: boolean; running: boolean };
assert.equal(body.ok, true);
assert.equal(body.running, false);
});
test("http-proxy: start then stop lifecycle", async () => {
const startReq = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/http-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "start" }),
}
);
const startRes = await httpProxyRoute.POST(startReq);
assert.equal(startRes.status, 201);
const stopReq = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/http-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "stop" }),
}
);
const stopRes = await httpProxyRoute.POST(stopReq);
assert.equal(stopRes.status, 200);
const body = await stopRes.json() as { running: boolean };
assert.equal(body.running, false);
});
test("http-proxy: EADDRINUSE returns 409 with structured error", async () => {
// Import startHttpProxyServer directly so we can test the low-level error path
// without depending on the module-cached DEFAULT_PORT.
const { startHttpProxyServer } = await import(
"../../src/mitm/inspector/httpProxyServer.ts"
);
// Occupy a random port
const blocker = net.createServer();
await new Promise<void>((resolve) => blocker.listen(0, "127.0.0.1", resolve));
const blockedPort = (blocker.address() as net.AddressInfo).port;
try {
// startHttpProxyServer should reject with code === EADDRINUSE
let caught: NodeJS.ErrnoException | null = null;
try {
await startHttpProxyServer(blockedPort);
} catch (err) {
caught = err as NodeJS.ErrnoException;
}
assert.ok(caught !== null, "should have thrown");
assert.equal(caught?.code, "EADDRINUSE");
} finally {
blocker.close();
}
});
// ── POST /capture-modes/system-proxy ───────────────────────────────────────
test("system-proxy: apply with mocked OS commands", async () => {
const restore = __setExec(async (_file, _args) => ({ stdout: "Enabled: No\nServer: \nPort: 0", stderr: "" }));
try {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/system-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "apply", port: 8080, guardMinutes: 1 }),
}
);
const res = await systemProxyRoute.POST(req);
assert.equal(res.status, 200);
const body = await res.json() as { ok: boolean; applied: boolean };
assert.equal(body.ok, true);
assert.equal(body.applied, true);
} finally {
restore();
clearSystemProxy();
}
});
test("system-proxy: revert without prior apply is a no-op", async () => {
const restore = __setExec(async (_file, _args) => ({ stdout: "", stderr: "" }));
try {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/system-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "revert" }),
}
);
const res = await systemProxyRoute.POST(req);
assert.equal(res.status, 200);
const body = await res.json() as { applied: boolean };
assert.equal(body.applied, false);
} finally {
restore();
}
});
test("system-proxy: rejects invalid action", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/system-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "invalid" }),
}
);
const res = await systemProxyRoute.POST(req);
assert.equal(res.status, 400);
});
// ── POST /capture-modes/tls-intercept ──────────────────────────────────────
test("tls-intercept: toggle on/off", async () => {
const enableReq = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/tls-intercept",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: true }),
}
);
const enableRes = await tlsInterceptRoute.POST(enableReq);
assert.equal(enableRes.status, 200);
const enableBody = await enableRes.json() as { tlsIntercept: { enabled: boolean } };
assert.equal(enableBody.tlsIntercept.enabled, true);
const disableReq = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/tls-intercept",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: false }),
}
);
const disableRes = await tlsInterceptRoute.POST(disableReq);
assert.equal(disableRes.status, 200);
const disableBody = await disableRes.json() as { tlsIntercept: { enabled: boolean } };
assert.equal(disableBody.tlsIntercept.enabled, false);
});

View File

@@ -0,0 +1,216 @@
/**
* Integration tests: Traffic Inspector error sanitization
*
* Verifies that all error responses do NOT include stack traces or raw
* file paths (Hard Rule #12).
*/
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 { randomUUID } from "node:crypto";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-errsanitize-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { globalTrafficBuffer } = await import("../../src/mitm/inspector/buffer.ts");
const requestsRoute = await import(
"../../src/app/api/tools/traffic-inspector/requests/route.ts"
);
const requestDetailRoute = await import(
"../../src/app/api/tools/traffic-inspector/requests/[id]/route.ts"
);
const annotationRoute = await import(
"../../src/app/api/tools/traffic-inspector/requests/[id]/annotation/route.ts"
);
const hostsRoute = await import(
"../../src/app/api/tools/traffic-inspector/hosts/route.ts"
);
const hostDetailRoute = await import(
"../../src/app/api/tools/traffic-inspector/hosts/[host]/route.ts"
);
const sessionsRoute = await import(
"../../src/app/api/tools/traffic-inspector/sessions/route.ts"
);
const sessionDetailRoute = await import(
"../../src/app/api/tools/traffic-inspector/sessions/[id]/route.ts"
);
const ingestRoute = await import(
"../../src/app/api/tools/traffic-inspector/internal/ingest/route.ts"
);
const httpProxyRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/http-proxy/route.ts"
);
const systemProxyRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/system-proxy/route.ts"
);
const tlsInterceptRoute = await import(
"../../src/app/api/tools/traffic-inspector/capture-modes/tls-intercept/route.ts"
);
function noStackTrace(msg: string, label: string): void {
assert.ok(
!msg.includes("at /"),
`${label}: error message must not contain stack trace (found "at /")`
);
assert.ok(
!msg.includes(".ts:"),
`${label}: error message must not include TS file paths`
);
}
async function getErrorMessage(res: Response): Promise<string> {
const body = await res.json() as { error: { message: string } };
return body.error?.message ?? "";
}
test.beforeEach(() => {
globalTrafficBuffer.clear();
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("requests: invalid profile param does not leak stack", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/requests?profile=BAD"
);
const res = await requestsRoute.GET(req);
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "GET /requests");
});
test("requests/[id]: unknown id does not leak stack", async () => {
const res = await requestDetailRoute.GET(
new Request("http://localhost/"),
{ params: Promise.resolve({ id: randomUUID() }) }
);
assert.equal(res.status, 404);
noStackTrace(await getErrorMessage(res), "GET /requests/[id]");
});
test("annotation: invalid body does not leak stack", async () => {
const entry = {
id: randomUUID(),
source: "agent-bridge" as const,
timestamp: new Date().toISOString(),
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 200 as const,
};
globalTrafficBuffer.push(entry);
const req = new Request("http://localhost/", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ annotation: 12345 }), // wrong type
});
const res = await annotationRoute.PUT(req, {
params: Promise.resolve({ id: entry.id }),
});
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "PUT annotation");
});
test("hosts: invalid body does not leak stack", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/hosts", {
method: "POST",
headers: { "content-type": "application/json" },
body: "bad json!}",
});
const res = await hostsRoute.POST(req);
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "POST /hosts");
});
test("hosts/[host] PATCH: invalid body does not leak stack", async () => {
const res = await hostDetailRoute.PATCH(
new Request("http://localhost/", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: "bad json!",
}),
{ params: Promise.resolve({ host: "foo.com" }) }
);
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "PATCH /hosts/[host]");
});
test("sessions: 404 does not leak stack", async () => {
const res = await sessionDetailRoute.GET(
new Request("http://localhost/"),
{ params: Promise.resolve({ id: randomUUID() }) }
);
assert.equal(res.status, 404);
noStackTrace(await getErrorMessage(res), "GET /sessions/[id]");
});
test("ingest: 403 does not leak stack", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/internal/ingest",
{
method: "POST",
headers: {
"content-type": "application/json",
authorization: "Bearer wrong-token",
},
body: JSON.stringify({}),
}
);
const res = await ingestRoute.POST(req);
assert.equal(res.status, 403);
noStackTrace(await getErrorMessage(res), "POST /internal/ingest (403)");
});
test("http-proxy: invalid action does not leak stack", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/http-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "invalid" }),
}
);
const res = await httpProxyRoute.POST(req);
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "POST /capture-modes/http-proxy");
});
test("system-proxy: invalid body does not leak stack", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/system-proxy",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "bad-action" }),
}
);
const res = await systemProxyRoute.POST(req);
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "POST /capture-modes/system-proxy");
});
test("tls-intercept: missing enabled field does not leak stack", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/capture-modes/tls-intercept",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: "not-a-boolean" }),
}
);
const res = await tlsInterceptRoute.POST(req);
assert.equal(res.status, 400);
noStackTrace(await getErrorMessage(res), "POST /capture-modes/tls-intercept");
});

View File

@@ -0,0 +1,142 @@
/**
* Integration tests: Traffic Inspector custom hosts CRUD
*
* Tests GET /hosts, POST /hosts, DELETE /hosts/[host], PATCH /hosts/[host].
* DB is isolated per test via a temp DATA_DIR.
*/
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";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-hosts-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// Boot DB so migrations run
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const hostsRoute = await import(
"../../src/app/api/tools/traffic-inspector/hosts/route.ts"
);
const hostDetailRoute = await import(
"../../src/app/api/tools/traffic-inspector/hosts/[host]/route.ts"
);
test.beforeEach(async () => {
resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
// Re-init DB with fresh migrations
await import("../../src/lib/db/core.ts").then((m) => m.getDbInstance());
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("GET /hosts: returns empty list initially", async () => {
const res = await hostsRoute.GET();
assert.equal(res.status, 200);
const body = await res.json() as { hosts: unknown[] };
assert.deepEqual(body.hosts, []);
});
test("POST /hosts: adds a host", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/hosts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ host: "api.openai.com", kind: "llm" }),
});
const res = await hostsRoute.POST(req);
assert.equal(res.status, 201);
const body = await res.json() as { ok: boolean; host: string };
assert.equal(body.ok, true);
assert.equal(body.host, "api.openai.com");
// Verify it appears in list
const listRes = await hostsRoute.GET();
const list = await listRes.json() as { hosts: Array<{ host: string }> };
assert.ok(list.hosts.some((h) => h.host === "api.openai.com"));
});
test("POST /hosts: rejects empty host string", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/hosts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ host: "" }),
});
const res = await hostsRoute.POST(req);
assert.equal(res.status, 400);
const body = await res.json() as { error: { message: string } };
assert.ok(!body.error.message.includes("at /"), "must not leak stack trace");
});
test("POST /hosts: rejects invalid JSON", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/hosts", {
method: "POST",
headers: { "content-type": "application/json" },
body: "not json",
});
const res = await hostsRoute.POST(req);
assert.equal(res.status, 400);
});
test("DELETE /hosts/[host]: removes existing host", async () => {
// Add host first
const addReq = new Request("http://localhost/api/tools/traffic-inspector/hosts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ host: "remove-me.example.com", kind: "custom" }),
});
await hostsRoute.POST(addReq);
// Now delete it
const delRes = await hostDetailRoute.DELETE(
new Request("http://localhost/"),
{ params: Promise.resolve({ host: "remove-me.example.com" }) }
);
assert.equal(delRes.status, 204);
// Verify gone
const listRes = await hostsRoute.GET();
const list = await listRes.json() as { hosts: Array<{ host: string }> };
assert.ok(!list.hosts.some((h) => h.host === "remove-me.example.com"));
});
test("PATCH /hosts/[host]: toggles enabled flag", async () => {
// Add host
const addReq = new Request("http://localhost/api/tools/traffic-inspector/hosts", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ host: "toggle-me.example.com", kind: "app", enabled: true }),
});
await hostsRoute.POST(addReq);
// Disable it
const patchRes = await hostDetailRoute.PATCH(
new Request("http://localhost/", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: false }),
}),
{ params: Promise.resolve({ host: "toggle-me.example.com" }) }
);
assert.equal(patchRes.status, 200);
const body = await patchRes.json() as { enabled: boolean };
assert.equal(body.enabled, false);
});
test("PATCH /hosts/[host]: returns 404 for non-existent host", async () => {
const res = await hostDetailRoute.PATCH(
new Request("http://localhost/", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ enabled: true }),
}),
{ params: Promise.resolve({ host: "nonexistent.example.com" }) }
);
assert.equal(res.status, 404);
});

View File

@@ -0,0 +1,150 @@
/**
* Integration tests: Traffic Inspector internal ingest endpoint
*
* Tests:
* - POST without token → 403
* - POST with wrong token → 403
* - POST with valid token + valid body → 200 + buffer push
* - POST with valid token + invalid body → 400 (no stack trace)
*/
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 { randomUUID } from "node:crypto";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-ingest-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// Set a known token BEFORE importing the route so the module picks it up
const VALID_TOKEN = "test-ingest-token-abc123xyz789-longer-than-16";
process.env.INSPECTOR_INTERNAL_INGEST_TOKEN = VALID_TOKEN;
const { globalTrafficBuffer } = await import("../../src/mitm/inspector/buffer.ts");
const ingestRoute = await import(
"../../src/app/api/tools/traffic-inspector/internal/ingest/route.ts"
);
function makeIngestRequest(token: string | null, body: unknown): Request {
const headers: Record<string, string> = {
"content-type": "application/json",
};
if (token !== null) {
headers["authorization"] = `Bearer ${token}`;
}
return new Request(
"http://localhost/api/tools/traffic-inspector/internal/ingest",
{
method: "POST",
headers,
body: JSON.stringify(body),
}
);
}
function minimalEntry(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
id: randomUUID(),
timestamp: new Date().toISOString(),
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
source: "agent-bridge",
requestHeaders: {},
requestSize: 0,
responseHeaders: {},
responseSize: 0,
status: 200,
...overrides,
};
}
test.beforeEach(() => {
globalTrafficBuffer.clear();
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("ingest: POST without Authorization header → 403", async () => {
const req = makeIngestRequest(null, minimalEntry());
const res = await ingestRoute.POST(req);
assert.equal(res.status, 403);
const body = await res.json() as { error: { message: string } };
assert.ok(!body.error.message.includes("at /"), "must not leak stack trace");
});
test("ingest: POST with wrong token → 403", async () => {
const req = makeIngestRequest("wrong-token", minimalEntry());
const res = await ingestRoute.POST(req);
assert.equal(res.status, 403);
});
test("ingest: POST with empty string token → 403", async () => {
const req = makeIngestRequest("", minimalEntry());
const res = await ingestRoute.POST(req);
assert.equal(res.status, 403);
});
test("ingest: POST with valid token + valid body → 200 + buffer push", async () => {
const id = randomUUID();
const req = makeIngestRequest(VALID_TOKEN, minimalEntry({ id }));
const res = await ingestRoute.POST(req);
assert.equal(res.status, 200);
const body = await res.json() as { ok: boolean; id: string };
assert.equal(body.ok, true);
assert.equal(body.id, id);
// Verify the entry was added to the buffer
const entry = globalTrafficBuffer.get(id);
assert.ok(entry, "entry should be in the buffer");
assert.equal(entry?.host, "api.openai.com");
});
test("ingest: valid token + missing required field → 400", async () => {
const req = makeIngestRequest(VALID_TOKEN, {
// missing 'host', 'path', 'source', etc.
id: randomUUID(),
timestamp: new Date().toISOString(),
method: "GET",
});
const res = await ingestRoute.POST(req);
assert.equal(res.status, 400);
const body = await res.json() as { error: { message: string } };
assert.ok(!body.error.message.includes("at /"), "must not leak stack trace");
});
test("ingest: valid token + invalid JSON → 400", async () => {
const headers: Record<string, string> = {
"content-type": "application/json",
"authorization": `Bearer ${VALID_TOKEN}`,
};
const req = new Request(
"http://localhost/api/tools/traffic-inspector/internal/ingest",
{
method: "POST",
headers,
body: "not valid json",
}
);
const res = await ingestRoute.POST(req);
assert.equal(res.status, 400);
});
test("ingest: getIngestTokenForBootstrap returns a non-empty token", () => {
const token = ingestRoute.getIngestTokenForBootstrap();
assert.ok(typeof token === "string" && token.length >= 16, "token should be ≥16 chars");
});
test("ingest: multiple pushes accumulate in buffer", async () => {
const ids = [randomUUID(), randomUUID(), randomUUID()];
for (const id of ids) {
const req = makeIngestRequest(VALID_TOKEN, minimalEntry({ id }));
const res = await ingestRoute.POST(req);
assert.equal(res.status, 200);
}
assert.equal(globalTrafficBuffer.size(), 3);
});

View File

@@ -0,0 +1,114 @@
/**
* Integration tests: Traffic Inspector LOCAL_ONLY enforcement
*
* Verifies that `isLocalOnlyPath` returns true for all traffic-inspector prefixes
* and that a simulated non-loopback request to the management policy returns 403.
*/
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";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-local-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { isLocalOnlyPath, isLoopbackHost } = await import(
"../../src/server/authz/routeGuard.ts"
);
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ── isLocalOnlyPath assertions ──────────────────────────────────────────────
test("isLocalOnlyPath: traffic-inspector prefix is LOCAL_ONLY", () => {
assert.equal(
isLocalOnlyPath("/api/tools/traffic-inspector/"),
true,
"root prefix should be LOCAL_ONLY"
);
});
test("isLocalOnlyPath: ws sub-path is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/ws"), true);
});
test("isLocalOnlyPath: requests sub-path is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/requests"), true);
});
test("isLocalOnlyPath: capture-modes sub-path is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/capture-modes/http-proxy"), true);
});
test("isLocalOnlyPath: sessions sub-path is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/sessions"), true);
});
test("isLocalOnlyPath: internal/ingest is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/internal/ingest"), true);
});
test("isLocalOnlyPath: export.har is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/export.har"), true);
});
test("isLocalOnlyPath: hosts sub-path is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/tools/traffic-inspector/hosts"), true);
});
// ── isLoopbackHost assertions ───────────────────────────────────────────────
test("isLoopbackHost: localhost returns true", () => {
assert.equal(isLoopbackHost("localhost"), true);
});
test("isLoopbackHost: 127.0.0.1 returns true", () => {
assert.equal(isLoopbackHost("127.0.0.1"), true);
});
test("isLoopbackHost: example.com returns false", () => {
assert.equal(isLoopbackHost("example.com"), false);
});
test("isLoopbackHost: external IP returns false", () => {
assert.equal(isLoopbackHost("192.168.1.100"), false);
});
test("isLoopbackHost: ::1 IPv6 returns true", () => {
assert.equal(isLoopbackHost("[::1]"), true);
});
// ── Management policy simulation ────────────────────────────────────────────
test("management policy: non-loopback request to LOCAL_ONLY path would be blocked", () => {
// Simulate the guard check that happens in management.ts
const path2 = "/api/tools/traffic-inspector/requests";
const hostHeader = "example.com"; // non-loopback
const isLocalOnly = isLocalOnlyPath(path2);
const isLoopback = isLoopbackHost(hostHeader);
// The policy blocks when: isLocalOnly && !isLoopback
assert.equal(isLocalOnly, true, "path should be LOCAL_ONLY");
assert.equal(isLoopback, false, "example.com should not be loopback");
// Therefore this request would be blocked (403 LOCAL_ONLY)
const wouldBeBlocked = isLocalOnly && !isLoopback;
assert.equal(wouldBeBlocked, true, "non-loopback request to LOCAL_ONLY path should be blocked");
});
test("management policy: loopback request to LOCAL_ONLY path passes IP check", () => {
const path2 = "/api/tools/traffic-inspector/ws";
const hostHeader = "localhost";
const isLocalOnly = isLocalOnlyPath(path2);
const isLoopback = isLoopbackHost(hostHeader);
assert.equal(isLocalOnly, true);
assert.equal(isLoopback, true);
const passesIpCheck = !(isLocalOnly && !isLoopback);
assert.equal(passesIpCheck, true, "loopback to LOCAL_ONLY path passes IP gate");
});

View File

@@ -0,0 +1,193 @@
/**
* Integration tests: Traffic Inspector requests endpoints
*
* Tests GET /requests (with filters), DELETE /requests, GET /requests/[id],
* and PUT /requests/[id]/annotation.
*/
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 { randomUUID } from "node:crypto";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-reqs-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { globalTrafficBuffer } = await import("../../src/mitm/inspector/buffer.ts");
const requestsRoute = await import(
"../../src/app/api/tools/traffic-inspector/requests/route.ts"
);
const requestDetailRoute = await import(
"../../src/app/api/tools/traffic-inspector/requests/[id]/route.ts"
);
const annotationRoute = await import(
"../../src/app/api/tools/traffic-inspector/requests/[id]/annotation/route.ts"
);
function makeEntry(overrides: Partial<{
id: string;
host: string;
detectedKind: "llm" | "app" | "unknown";
status: number | "in-flight" | "error";
source: "agent-bridge" | "custom-host" | "http-proxy" | "system-proxy";
}> = {}) {
return {
id: randomUUID(),
source: "agent-bridge" as const,
timestamp: new Date().toISOString(),
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 200 as const,
detectedKind: "llm" as const,
...overrides,
};
}
test.beforeEach(() => {
globalTrafficBuffer.clear();
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("GET /requests: returns empty list when buffer is empty", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/requests");
const res = await requestsRoute.GET(req);
assert.equal(res.status, 200);
const body = await res.json() as { requests: unknown[]; total: number };
assert.deepEqual(body.requests, []);
assert.equal(body.total, 0);
});
test("GET /requests: returns all entries without filter", async () => {
globalTrafficBuffer.push(makeEntry({ id: randomUUID(), host: "a.com" }));
globalTrafficBuffer.push(makeEntry({ id: randomUUID(), host: "b.com" }));
const req = new Request("http://localhost/api/tools/traffic-inspector/requests");
const res = await requestsRoute.GET(req);
assert.equal(res.status, 200);
const body = await res.json() as { requests: unknown[]; total: number };
assert.equal(body.total, 2);
});
test("GET /requests: filters by profile=llm", async () => {
globalTrafficBuffer.push(makeEntry({ id: randomUUID(), detectedKind: "llm" }));
globalTrafficBuffer.push(makeEntry({ id: randomUUID(), detectedKind: "app" }));
const req = new Request(
"http://localhost/api/tools/traffic-inspector/requests?profile=llm"
);
const res = await requestsRoute.GET(req);
assert.equal(res.status, 200);
const body = await res.json() as { requests: unknown[]; total: number };
assert.equal(body.total, 1);
});
test("GET /requests: filters by host", async () => {
globalTrafficBuffer.push(makeEntry({ id: randomUUID(), host: "target.com" }));
globalTrafficBuffer.push(makeEntry({ id: randomUUID(), host: "other.com" }));
const req = new Request(
"http://localhost/api/tools/traffic-inspector/requests?host=target.com"
);
const res = await requestsRoute.GET(req);
assert.equal(res.status, 200);
const body = await res.json() as { requests: Array<{ host: string }>; total: number };
assert.equal(body.total, 1);
assert.equal(body.requests[0]?.host, "target.com");
});
test("GET /requests: rejects invalid profile param with 400", async () => {
const req = new Request(
"http://localhost/api/tools/traffic-inspector/requests?profile=invalid"
);
const res = await requestsRoute.GET(req);
assert.equal(res.status, 400);
const body = await res.json() as { error: { message: string } };
assert.ok(!body.error.message.includes("at /"), "must not leak stack trace");
});
test("DELETE /requests: clears the buffer", async () => {
globalTrafficBuffer.push(makeEntry());
const res = await requestsRoute.DELETE();
assert.equal(res.status, 204);
assert.equal(globalTrafficBuffer.size(), 0);
});
test("GET /requests/[id]: returns entry by id", async () => {
const entry = makeEntry();
globalTrafficBuffer.push(entry);
const req = new Request(`http://localhost/api/tools/traffic-inspector/requests/${entry.id}`);
const res = await requestDetailRoute.GET(req, {
params: Promise.resolve({ id: entry.id }),
});
assert.equal(res.status, 200);
const body = await res.json() as { id: string };
assert.equal(body.id, entry.id);
});
test("GET /requests/[id]: returns 404 for unknown id", async () => {
const req = new Request(
`http://localhost/api/tools/traffic-inspector/requests/${randomUUID()}`
);
const res = await requestDetailRoute.GET(req, {
params: Promise.resolve({ id: randomUUID() }),
});
assert.equal(res.status, 404);
const body = await res.json() as { error: { message: string } };
assert.ok(!body.error.message.includes("at /"), "must not leak stack trace");
});
test("PUT /requests/[id]/annotation: attaches annotation", async () => {
const entry = makeEntry();
globalTrafficBuffer.push(entry);
const req = new Request(
`http://localhost/api/tools/traffic-inspector/requests/${entry.id}/annotation`,
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ annotation: "my note" }),
}
);
const res = await annotationRoute.PUT(req, {
params: Promise.resolve({ id: entry.id }),
});
assert.equal(res.status, 200);
const body = await res.json() as { annotation: string };
assert.equal(body.annotation, "my note");
// Confirm buffer was updated
const updated = globalTrafficBuffer.get(entry.id);
assert.equal(updated?.annotation, "my note");
});
test("PUT /requests/[id]/annotation: rejects annotation > 10000 chars", async () => {
const entry = makeEntry();
globalTrafficBuffer.push(entry);
const req = new Request(
`http://localhost/api/tools/traffic-inspector/requests/${entry.id}/annotation`,
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ annotation: "x".repeat(10_001) }),
}
);
const res = await annotationRoute.PUT(req, {
params: Promise.resolve({ id: entry.id }),
});
assert.equal(res.status, 400);
});

View File

@@ -0,0 +1,241 @@
/**
* Integration tests: Traffic Inspector sessions CRUD
*
* Tests the full lifecycle: POST start → PATCH stop → GET snapshot → DELETE cascade.
*/
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 { randomUUID } from "node:crypto";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-sessions-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { resetDbInstance, getDbInstance } = await import("../../src/lib/db/core.ts");
async function resetStorage() {
resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
// Re-initialize db
getDbInstance();
}
const sessionsRoute = await import(
"../../src/app/api/tools/traffic-inspector/sessions/route.ts"
);
const sessionDetailRoute = await import(
"../../src/app/api/tools/traffic-inspector/sessions/[id]/route.ts"
);
const sessionHarRoute = await import(
"../../src/app/api/tools/traffic-inspector/sessions/[id]/export.har/route.ts"
);
const { appendSessionRequest } = await import("../../src/lib/db/inspectorSessions.ts");
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("POST /sessions: creates a session", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "Test Session" }),
});
const res = await sessionsRoute.POST(req);
assert.equal(res.status, 201);
const body = await res.json() as { id: string; started_at: string };
assert.ok(body.id, "should have an id");
assert.ok(body.started_at, "should have started_at");
});
test("POST /sessions: name is optional", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{}",
});
const res = await sessionsRoute.POST(req);
assert.equal(res.status, 201);
});
test("GET /sessions: lists all sessions", async () => {
// Create two sessions
await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "s1" }),
})
);
await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "s2" }),
})
);
const res = await sessionsRoute.GET();
assert.equal(res.status, 200);
const body = await res.json() as { sessions: unknown[] };
assert.equal(body.sessions.length, 2);
});
test("PATCH /sessions/[id]: stop adds ended_at", async () => {
const createRes = await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
})
);
const session = await createRes.json() as { id: string };
const patchReq = new Request("http://localhost/", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "stop" }),
});
const patchRes = await sessionDetailRoute.PATCH(patchReq, {
params: Promise.resolve({ id: session.id }),
});
assert.equal(patchRes.status, 200);
const body = await patchRes.json() as { ended_at: string | null };
assert.ok(body.ended_at !== null, "ended_at should be set after stop");
});
test("PATCH /sessions/[id]: rename updates name", async () => {
const createRes = await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "old-name" }),
})
);
const session = await createRes.json() as { id: string };
const patchRes = await sessionDetailRoute.PATCH(
new Request("http://localhost/", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action: "rename", name: "new-name" }),
}),
{ params: Promise.resolve({ id: session.id }) }
);
assert.equal(patchRes.status, 200);
const body = await patchRes.json() as { name: string };
assert.equal(body.name, "new-name");
});
test("GET /sessions/[id]: returns session with requests", async () => {
const createRes = await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "with-reqs" }),
})
);
const session = await createRes.json() as { id: string };
// Append a fake request
const payload = JSON.stringify({
id: randomUUID(),
source: "agent-bridge",
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
status: 200,
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
timestamp: new Date().toISOString(),
});
appendSessionRequest(session.id, payload);
const getRes = await sessionDetailRoute.GET(
new Request("http://localhost/"),
{ params: Promise.resolve({ id: session.id }) }
);
assert.equal(getRes.status, 200);
const body = await getRes.json() as { session: { id: string }; requests: unknown[] };
assert.equal(body.session.id, session.id);
assert.equal(body.requests.length, 1);
});
test("DELETE /sessions/[id]: cascades requests", async () => {
const createRes = await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({}),
})
);
const session = await createRes.json() as { id: string };
appendSessionRequest(session.id, JSON.stringify({ note: "test" }));
const delRes = await sessionDetailRoute.DELETE(
new Request("http://localhost/"),
{ params: Promise.resolve({ id: session.id }) }
);
assert.equal(delRes.status, 204);
// Session should be gone
const getRes = await sessionDetailRoute.GET(
new Request("http://localhost/"),
{ params: Promise.resolve({ id: session.id }) }
);
assert.equal(getRes.status, 404);
});
test("GET /sessions/[id]/export.har: returns HAR file", async () => {
const createRes = await sessionsRoute.POST(
new Request("http://localhost/api/tools/traffic-inspector/sessions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "har-test" }),
})
);
const session = await createRes.json() as { id: string };
const reqPayload = {
id: randomUUID(),
source: "agent-bridge",
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
status: 200,
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
timestamp: new Date().toISOString(),
};
appendSessionRequest(session.id, JSON.stringify(reqPayload));
const harRes = await sessionHarRoute.GET(
new Request("http://localhost/"),
{ params: Promise.resolve({ id: session.id }) }
);
assert.equal(harRes.status, 200);
assert.ok(
harRes.headers.get("content-disposition")?.includes(".har"),
"should have .har filename"
);
const har = await harRes.json() as { log: { entries: unknown[] } };
assert.ok(har.log, "should be a HAR object");
assert.equal(har.log.entries.length, 1);
});

View File

@@ -0,0 +1,148 @@
/**
* Integration tests: Traffic Inspector WebSocket endpoint
*
* Tests WS upgrade, initial snapshot delivery, and live buffer events.
* We do not spin up a full HTTP server — we test the buffer subscribe
* mechanism directly since the WS handler is a thin wrapper around it.
*/
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 { randomUUID } from "node:crypto";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ti-ws-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.INSPECTOR_BUFFER_SIZE = "100";
const { TrafficBuffer } = await import("../../src/mitm/inspector/buffer.ts");
const wsRoute = await import(
"../../src/app/api/tools/traffic-inspector/ws/route.ts"
);
function makeRequest(upgrade = "websocket", clientKey = "dGhlIHNhbXBsZSBub25jZQ=="): Request {
return new Request("http://localhost/api/tools/traffic-inspector/ws", {
headers: {
upgrade,
"sec-websocket-key": clientKey,
connection: "Upgrade",
},
});
}
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("ws/route: rejects non-WebSocket GET with 426", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/ws");
const res = await wsRoute.GET(req);
assert.equal(res.status, 426);
const body = await res.json() as { error: { message: string } };
assert.ok(body.error.message.includes("Upgrade"), "should mention upgrade");
});
test("ws/route: rejects missing Sec-WebSocket-Key with 400", async () => {
const req = new Request("http://localhost/api/tools/traffic-inspector/ws", {
headers: { upgrade: "websocket", connection: "Upgrade" },
});
const res = await wsRoute.GET(req);
assert.equal(res.status, 400);
});
test("ws/route: rejects when no raw socket available with 500", async () => {
const req = makeRequest();
// No `.socket` property injected — Next.js standalone would attach it
const res = await wsRoute.GET(req);
assert.equal(res.status, 500);
const body = await res.json() as { error: { message: string } };
assert.ok(!body.error.message.includes("at /"), "must not leak stack trace");
});
test("TrafficBuffer: subscribe receives snapshot immediately", () => {
const buf = new TrafficBuffer(10, 1024 * 1024);
const entry = {
id: randomUUID(),
source: "agent-bridge" as const,
timestamp: new Date().toISOString(),
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 200 as const,
};
buf.push(entry);
const events: unknown[] = [];
const unsub = buf.subscribe((ev) => events.push(ev));
assert.equal(events.length, 1, "should receive snapshot immediately");
const snapshot = events[0] as { type: string; data: unknown[] };
assert.equal(snapshot.type, "snapshot");
assert.ok(Array.isArray(snapshot.data));
assert.equal(snapshot.data.length, 1);
unsub();
});
test("TrafficBuffer: push broadcasts new event to subscribers", () => {
const buf = new TrafficBuffer(10, 1024 * 1024);
const events: unknown[] = [];
const unsub = buf.subscribe((ev) => events.push(ev));
// snapshot is at index 0
buf.push({
id: randomUUID(),
source: "http-proxy" as const,
timestamp: new Date().toISOString(),
method: "GET",
host: "example.com",
path: "/",
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 200 as const,
});
assert.equal(events.length, 2, "should have snapshot + new event");
const newEv = events[1] as { type: string; data: { host: string } };
assert.equal(newEv.type, "new");
assert.equal(newEv.data.host, "example.com");
unsub();
});
test("TrafficBuffer: unsubscribe stops receiving events", () => {
const buf = new TrafficBuffer(10, 1024 * 1024);
const events: unknown[] = [];
const unsub = buf.subscribe((ev) => events.push(ev));
unsub();
buf.push({
id: randomUUID(),
source: "system-proxy" as const,
timestamp: new Date().toISOString(),
method: "POST",
host: "test.com",
path: "/api",
requestHeaders: {},
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 204 as const,
});
assert.equal(events.length, 1, "should only have the initial snapshot");
});