inspector/client/src/lib/__tests__/proxyFetch.test.ts
2026-04-07 10:33:28 -07:00

244 lines
7.3 KiB
TypeScript

import { createProxyFetch } from "../proxyFetch";
import { DEFAULT_INSPECTOR_CONFIG } from "../constants";
import type { InspectorConfig } from "../configurationTypes";
describe("createProxyFetch", () => {
const mockFetch = jest.fn();
const proxyAddress = "http://localhost:6277";
const configWithProxy: InspectorConfig = {
...DEFAULT_INSPECTOR_CONFIG,
MCP_PROXY_FULL_ADDRESS: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_FULL_ADDRESS,
value: proxyAddress,
},
MCP_PROXY_AUTH_TOKEN: {
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
value: "test-proxy-token",
},
};
beforeEach(() => {
jest.clearAllMocks();
global.fetch = mockFetch;
});
it("returns a function", () => {
const fetchFn = createProxyFetch(configWithProxy);
expect(typeof fetchFn).toBe("function");
});
it("sends POST to proxy /fetch endpoint with correct headers", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
ok: true,
status: 200,
statusText: "OK",
headers: {},
body: "response body",
}),
});
const fetchFn = createProxyFetch(configWithProxy);
await fetchFn("https://example.com/.well-known/oauth-authorization-server");
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(`${proxyAddress}/fetch`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-MCP-Proxy-Auth": "Bearer test-proxy-token",
},
body: expect.any(String),
});
});
it("includes target url and init in request body", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
ok: true,
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
body: '{"issuer":"https://example.com"}',
}),
});
const fetchFn = createProxyFetch(configWithProxy);
await fetchFn("https://example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "grant_type=authorization_code&code=abc",
});
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(callBody).toEqual({
url: "https://example.com/oauth/token",
init: {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: "grant_type=authorization_code&code=abc",
},
});
});
it("reconstructs Response from proxy response", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
ok: true,
status: 200,
statusText: "OK",
headers: { "content-type": "application/json" },
body: '{"issuer":"https://example.com"}',
}),
});
const fetchFn = createProxyFetch(configWithProxy);
const response = await fetchFn(
"https://example.com/.well-known/oauth-authorization-server",
);
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(response.statusText).toBe("OK");
expect(response.headers.get("content-type")).toBe("application/json");
const body = await response.text();
expect(body).toBe('{"issuer":"https://example.com"}');
});
it("returns non-ok Response when upstream status is not 2xx (mirrored by proxy)", async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
json: () =>
Promise.resolve({
ok: false,
status: 404,
statusText: "Not Found",
headers: { "content-type": "application/json" },
body: '{"error":"not_found"}',
}),
});
const fetchFn = createProxyFetch(configWithProxy);
const response = await fetchFn("https://example.com/.well-known/missing");
expect(response.ok).toBe(false);
expect(response.status).toBe(404);
expect(response.statusText).toBe("Not Found");
expect(response.headers.get("content-type")).toBe("application/json");
expect(await response.text()).toBe('{"error":"not_found"}');
});
it("returns non-ok Response when upstream returns 400 with token-endpoint error JSON (RFC 6749)", async () => {
const tokenErrorBody =
'{"error":"invalid_grant","error_description":"code expired"}';
mockFetch.mockResolvedValue({
ok: false,
status: 400,
statusText: "Bad Request",
json: () =>
Promise.resolve({
ok: false,
status: 400,
statusText: "Bad Request",
headers: { "content-type": "application/json" },
body: tokenErrorBody,
}),
});
const fetchFn = createProxyFetch(configWithProxy);
const response = await fetchFn("https://example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "grant_type=authorization_code&code=x",
});
expect(response.ok).toBe(false);
expect(response.status).toBe(400);
expect(await response.json()).toEqual({
error: "invalid_grant",
error_description: "code expired",
});
});
it("throws when proxy POST returns JSON error envelope (e.g. 401 invalid session)", async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 401,
statusText: "Unauthorized",
json: () =>
Promise.resolve({
error: "Unauthorized",
message:
"Authentication required. Use the session token shown in the console when starting the server.",
}),
});
const fetchFn = createProxyFetch(configWithProxy);
await expect(fetchFn("https://example.com/")).rejects.toThrow(
"Authentication required. Use the session token shown in the console when starting the server.",
);
});
it("throws when proxy response is not valid JSON", async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 502,
statusText: "Bad Gateway",
json: () => Promise.reject(new SyntaxError("Unexpected token")),
});
const fetchFn = createProxyFetch(configWithProxy);
await expect(fetchFn("https://example.com/")).rejects.toThrow(
"Proxy fetch failed: 502 Bad Gateway",
);
});
it("uses URL object as input", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
ok: true,
status: 200,
statusText: "OK",
headers: {},
body: "",
}),
});
const fetchFn = createProxyFetch(configWithProxy);
await fetchFn(new URL("https://example.com/discovery"));
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(callBody.url).toBe("https://example.com/discovery");
});
it("uses Request.url when input is a Request (not [object Request])", async () => {
mockFetch.mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
ok: true,
status: 200,
statusText: "OK",
headers: {},
body: "",
}),
});
const fetchFn = createProxyFetch(configWithProxy);
await fetchFn(new Request("https://example.com/from-request"));
const callBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(callBody.url).toBe("https://example.com/from-request");
});
});