Skip to content

Commit 37cff10

Browse files
test: add tests for X-MCP-Proxy-Auth header implementation
- Added comprehensive test coverage for proxy authentication using X-MCP-Proxy-Auth header - Tests verify proper header separation between proxy and server authentication - Fixed transport mocks to properly capture options and headers - Updated config structure in tests to match production code - All proxy auth tests now passing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fdae89e commit 37cff10

File tree

1 file changed

+200
-6
lines changed

1 file changed

+200
-6
lines changed

client/src/lib/hooks/__tests__/useConnection.test.tsx

Lines changed: 200 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useConnection } from "../useConnection";
33
import { z } from "zod";
44
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
55
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
6+
import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
67

78
// Mock fetch
89
global.fetch = jest.fn().mockResolvedValue({
@@ -23,21 +24,46 @@ const mockClient = {
2324
setRequestHandler: jest.fn(),
2425
};
2526

27+
// Mock transport instances
28+
const mockSSETransport: {
29+
start: jest.Mock;
30+
url: URL | undefined;
31+
options: SSEClientTransportOptions | undefined;
32+
} = {
33+
start: jest.fn(),
34+
url: undefined,
35+
options: undefined,
36+
};
37+
38+
const mockStreamableHTTPTransport: {
39+
start: jest.Mock;
40+
url: URL | undefined;
41+
options: SSEClientTransportOptions | undefined;
42+
} = {
43+
start: jest.fn(),
44+
url: undefined,
45+
options: undefined,
46+
};
47+
2648
jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
2749
Client: jest.fn().mockImplementation(() => mockClient),
2850
}));
2951

3052
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
31-
SSEClientTransport: jest.fn((url) => ({
32-
toString: () => url,
33-
})),
53+
SSEClientTransport: jest.fn((url, options) => {
54+
mockSSETransport.url = url;
55+
mockSSETransport.options = options;
56+
return mockSSETransport;
57+
}),
3458
SseError: jest.fn(),
3559
}));
3660

3761
jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
38-
StreamableHTTPClientTransport: jest.fn((url) => ({
39-
toString: () => url,
40-
})),
62+
StreamableHTTPClientTransport: jest.fn((url, options) => {
63+
mockStreamableHTTPTransport.url = url;
64+
mockStreamableHTTPTransport.options = options;
65+
return mockStreamableHTTPTransport;
66+
}),
4167
}));
4268

4369
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
@@ -259,4 +285,172 @@ describe("useConnection", () => {
259285
);
260286
});
261287
});
288+
289+
describe("Proxy Authentication Headers", () => {
290+
beforeEach(() => {
291+
jest.clearAllMocks();
292+
// Reset the mock transport objects
293+
mockSSETransport.url = undefined;
294+
mockSSETransport.options = undefined;
295+
mockStreamableHTTPTransport.url = undefined;
296+
mockStreamableHTTPTransport.options = undefined;
297+
});
298+
299+
test("sends X-MCP-Proxy-Auth header when proxy auth token is configured", async () => {
300+
const propsWithProxyAuth = {
301+
...defaultProps,
302+
config: {
303+
...DEFAULT_INSPECTOR_CONFIG,
304+
MCP_PROXY_AUTH_TOKEN: {
305+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
306+
value: "test-proxy-token",
307+
},
308+
},
309+
};
310+
311+
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
312+
313+
await act(async () => {
314+
await result.current.connect();
315+
});
316+
317+
// Check that the transport was created with the correct headers
318+
expect(mockSSETransport.options).toBeDefined();
319+
expect(mockSSETransport.options?.requestInit).toBeDefined();
320+
321+
expect(mockSSETransport.options?.requestInit?.headers).toHaveProperty(
322+
"X-MCP-Proxy-Auth",
323+
"Bearer test-proxy-token",
324+
);
325+
expect(mockSSETransport?.options?.eventSourceInit?.fetch).toBeDefined();
326+
327+
// Verify the fetch function includes the proxy auth header
328+
const mockFetch = mockSSETransport.options?.eventSourceInit?.fetch;
329+
const testUrl = "http://test.com";
330+
await mockFetch?.(testUrl);
331+
332+
expect(global.fetch).toHaveBeenCalledTimes(2);
333+
expect(
334+
(global.fetch as jest.Mock).mock.calls[0][1].headers,
335+
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
336+
expect((global.fetch as jest.Mock).mock.calls[1][0]).toBe(testUrl);
337+
expect(
338+
(global.fetch as jest.Mock).mock.calls[1][1].headers,
339+
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
340+
});
341+
342+
test("does NOT send Authorization header for proxy auth", async () => {
343+
const propsWithProxyAuth = {
344+
...defaultProps,
345+
config: {
346+
...DEFAULT_INSPECTOR_CONFIG,
347+
proxyAuthToken: "test-proxy-token",
348+
},
349+
};
350+
351+
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
352+
353+
await act(async () => {
354+
await result.current.connect();
355+
});
356+
357+
// Check that Authorization header is NOT used for proxy auth
358+
expect(mockSSETransport.options?.requestInit?.headers).not.toHaveProperty(
359+
"Authorization",
360+
"Bearer test-proxy-token",
361+
);
362+
});
363+
364+
test("preserves server Authorization header when proxy auth is configured", async () => {
365+
const propsWithBothAuth = {
366+
...defaultProps,
367+
bearerToken: "server-auth-token",
368+
config: {
369+
...DEFAULT_INSPECTOR_CONFIG,
370+
MCP_PROXY_AUTH_TOKEN: {
371+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
372+
value: "test-proxy-token",
373+
},
374+
},
375+
};
376+
377+
const { result } = renderHook(() => useConnection(propsWithBothAuth));
378+
379+
await act(async () => {
380+
await result.current.connect();
381+
});
382+
383+
// Check that both headers are present and distinct
384+
const headers = mockSSETransport.options?.requestInit?.headers;
385+
expect(headers).toHaveProperty(
386+
"Authorization",
387+
"Bearer server-auth-token",
388+
);
389+
expect(headers).toHaveProperty(
390+
"X-MCP-Proxy-Auth",
391+
"Bearer test-proxy-token",
392+
);
393+
});
394+
395+
test("sends X-MCP-Proxy-Auth in health check requests", async () => {
396+
const fetchMock = global.fetch as jest.Mock;
397+
fetchMock.mockClear();
398+
399+
const propsWithProxyAuth = {
400+
...defaultProps,
401+
config: {
402+
...DEFAULT_INSPECTOR_CONFIG,
403+
MCP_PROXY_AUTH_TOKEN: {
404+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
405+
value: "test-proxy-token",
406+
},
407+
},
408+
};
409+
410+
const { result } = renderHook(() => useConnection(propsWithProxyAuth));
411+
412+
await act(async () => {
413+
await result.current.connect();
414+
});
415+
416+
// Find the health check call
417+
const healthCheckCall = fetchMock.mock.calls.find(
418+
(call) => call[0].pathname === "/health",
419+
);
420+
421+
expect(healthCheckCall).toBeDefined();
422+
expect(healthCheckCall[1].headers).toHaveProperty(
423+
"X-MCP-Proxy-Auth",
424+
"Bearer test-proxy-token",
425+
);
426+
});
427+
428+
test("works correctly with streamable-http transport", async () => {
429+
const propsWithStreamableHttp = {
430+
...defaultProps,
431+
transportType: "streamable-http" as const,
432+
config: {
433+
...DEFAULT_INSPECTOR_CONFIG,
434+
MCP_PROXY_AUTH_TOKEN: {
435+
...DEFAULT_INSPECTOR_CONFIG.MCP_PROXY_AUTH_TOKEN,
436+
value: "test-proxy-token",
437+
},
438+
},
439+
};
440+
441+
const { result } = renderHook(() =>
442+
useConnection(propsWithStreamableHttp),
443+
);
444+
445+
await act(async () => {
446+
await result.current.connect();
447+
});
448+
449+
// Check that the streamable HTTP transport was created with the correct headers
450+
expect(mockStreamableHTTPTransport.options).toBeDefined();
451+
expect(
452+
mockStreamableHTTPTransport.options?.requestInit?.headers,
453+
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
454+
});
455+
});
262456
});

0 commit comments

Comments
 (0)