244 lines
7.3 KiB
TypeScript
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");
|
|
});
|
|
});
|