import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { ResultReferenceStore } from "../../src/results/store.js"; import { DynamicHttpExecutor } from "../../src/tools/dynamicHttpExecutor.js"; import { createTestDatabase } from "../helpers/postgres.js"; describe("DynamicHttpExecutor", () => { const originalFetch = globalThis.fetch; let payloadDir: string; let store: ResultReferenceStore; let executor: DynamicHttpExecutor; let dispose: (() => Promise) | undefined; beforeEach(async () => { payloadDir = await mkdtemp(join(tmpdir(), "tjwater-dynamic-http-")); const testDb = await createTestDatabase(); store = new ResultReferenceStore(testDb.db, payloadDir, 60_000); executor = new DynamicHttpExecutor(store); dispose = async () => { globalThis.fetch = originalFetch; await testDb.dispose(); await rm(payloadDir, { force: true, recursive: true }); }; await store.initialize(); }); afterEach(async () => { await dispose?.(); }); it("sends JSON body for POST requests while preserving query arguments", async () => { let capturedUrl = ""; let capturedInit: RequestInit | undefined; globalThis.fetch = Object.assign( async (input: URL | RequestInfo, init?: RequestInit | BunFetchRequestInit) => { capturedUrl = String(input); capturedInit = init; return new Response(JSON.stringify({ status: "ok" }), { status: 200, headers: { "content-type": "application/json" }, }); }, originalFetch, ); const result = await executor.execute( { path: "/api/v1/runproject/simulate", method: "POST", arguments: { network: "demo-network" }, body: { duration_minutes: 15 }, }, { accessToken: "token-1", actorKey: "actor-1", projectId: "project-1", projectKey: "project-key-1", sessionId: "session-1", traceId: "trace-1", }, ); const url = new URL(capturedUrl); const headers = new Headers(capturedInit?.headers); expect(url.pathname).toBe("/api/v1/runproject/simulate"); expect(url.searchParams.get("network")).toBe("demo-network"); expect(capturedInit?.method).toBe("POST"); expect(headers.get("content-type")).toBe("application/json"); expect(headers.get("authorization")).toBe("Bearer token-1"); expect(headers.get("x-project-id")).toBe("project-1"); expect(headers.get("x-trace-id")).toBe("trace-1"); expect(capturedInit?.body).toBe(JSON.stringify({ duration_minutes: 15 })); expect(result).toEqual({ ok: true, trace_id: "trace-1", upstream: { method: "POST", path: "/api/v1/runproject/simulate", status_code: 200, }, result_mode: "inline", result_size_bytes: expect.any(Number), data: { status: "ok" }, }); }); it("rejects GET requests that include a body", async () => { await expect( executor.execute( { path: "/api/v1/runproject/", method: "GET", arguments: { network: "demo-network" }, body: { duration_minutes: 15 }, }, { actorKey: "actor-1", projectId: "project-1", projectKey: "project-key-1", sessionId: "session-1", traceId: "trace-1", }, ), ).rejects.toThrow("GET requests do not support body"); }); });