Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions __tests__/lib/j0di3/j0di3-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ vi.mock("axios", () => ({
},
}));

import j0di3 from "@/lib/j0di3-client";
import axios from "axios";
import j0di3 from "@/lib/j0di3-client";
import { j0di3Proxy } from "@/lib/j0di3-proxy";

const mockJ0di3 = j0di3 as unknown as Mock;
const mockIsAxiosError = axios.isAxiosError as Mock;

function createMockReqRes(
overrides: Partial<NextApiRequest> = {}
): { req: NextApiRequest; res: NextApiResponse } {
function createMockReqRes(overrides: Partial<NextApiRequest> = {}): {
req: NextApiRequest;
res: NextApiResponse;
} {
const req = {
method: "POST",
body: { question: "What is React?" },
Expand All @@ -43,6 +44,7 @@ function createMockReqRes(
const res = {
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
setHeader: vi.fn().mockReturnThis(),
} as unknown as NextApiResponse;

return { req, res };
Expand Down Expand Up @@ -100,6 +102,44 @@ describe("j0di3Proxy", () => {
);
});

it("forwards Idempotency-Key and X-Request-ID headers when supplied", async () => {
mockJ0di3.mockResolvedValue({ data: { ok: true } });

const handler = j0di3Proxy("POST", "/api/v1/challenges/c-1/submit");
const { req, res } = createMockReqRes({
headers: {
"idempotency-key": "idem-key-789",
"x-request-id": "req-id-456",
} as unknown as NextApiRequest["headers"],
});

await handler(req, res);

expect(mockJ0di3).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
"X-Troop-Token": "troop-token-abc",
"Idempotency-Key": "idem-key-789",
"X-Request-ID": "req-id-456",
},
})
);
});

it("echoes the J0dI3 X-Request-ID back on the response", async () => {
mockJ0di3.mockResolvedValue({
data: { ok: true },
headers: { "x-request-id": "echoed-req-id" },
});

const handler = j0di3Proxy("POST", "/api/v1/challenges/c-1/submit");
const { req, res } = createMockReqRes();

await handler(req, res);

expect(res.setHeader).toHaveBeenCalledWith("X-Request-ID", "echoed-req-id");
});

it("returns 400 when user has no troopId", async () => {
const handler = j0di3Proxy("POST", "/api/v1/learning/explain");
const { req, res } = createMockReqRes();
Expand Down
25 changes: 22 additions & 3 deletions src/lib/j0di3-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,29 @@ async function dispatch(
: req.query
: undefined;

const headers = injectTroopId && troopToken ? { "X-Troop-Token": troopToken } : undefined;
const headers: Record<string, string> = {};
if (injectTroopId && troopToken) {
headers["X-Troop-Token"] = troopToken;
}
// Forward idempotency + correlation headers when the caller supplies them.
const idempotencyKey = req.headers?.["idempotency-key"];
if (typeof idempotencyKey === "string") {
headers["Idempotency-Key"] = idempotencyKey;
}
const requestId = req.headers?.["x-request-id"];
if (typeof requestId === "string") {
headers["X-Request-ID"] = requestId;
}

const response = await j0di3({ method, url, data: body, params, headers });

// J0dI3 echoes X-Request-ID back; surface it for support correlation.
const echoedRequestId = response.headers?.["x-request-id"];
if (typeof echoedRequestId === "string") {
res.setHeader("X-Request-ID", echoedRequestId);
}

const { data } = await j0di3({ method, url, data: body, params, headers });
res.json(data);
res.json(response.data);
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response) {
const status = error.response.status;
Expand Down
Loading