typescript-sdk/test/client/middleware.test.ts
2025-12-05 23:56:12 +00:00

1118 lines
41 KiB
TypeScript

import { withOAuth, withLogging, applyMiddlewares, createMiddleware } from '../../src/client/middleware.js';
import { OAuthClientProvider } from '../../src/client/auth.js';
import { FetchLike } from '../../src/shared/transport.js';
import { MockInstance, Mocked, MockedFunction } from 'vitest';
vi.mock('../../src/client/auth.js', async () => {
const actual = await vi.importActual<typeof import('../../src/client/auth.js')>('../../src/client/auth.js');
return {
...actual,
auth: vi.fn(),
extractWWWAuthenticateParams: vi.fn()
};
});
import { auth, extractWWWAuthenticateParams } from '../../src/client/auth.js';
const mockAuth = auth as MockedFunction<typeof auth>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as MockedFunction<typeof extractWWWAuthenticateParams>;
describe('withOAuth', () => {
let mockProvider: Mocked<OAuthClientProvider>;
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
vi.clearAllMocks();
mockProvider = {
get redirectUrl() {
return 'http://localhost/callback';
},
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
tokens: vi.fn(),
saveTokens: vi.fn(),
clientInformation: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};
mockFetch = vi.fn();
});
it('should add Authorization header when tokens are available (with explicit baseUrl)', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
mockFetch.mockResolvedValue(new Response('success', { status: 200 }));
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
await enhancedFetch('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: expect.any(Headers)
})
);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBe('Bearer test-token');
});
it('should add Authorization header when tokens are available (without baseUrl)', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
mockFetch.mockResolvedValue(new Response('success', { status: 200 }));
// Test without baseUrl - should extract from request URL
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
await enhancedFetch('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: expect.any(Headers)
})
);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBe('Bearer test-token');
});
it('should handle requests without tokens (without baseUrl)', async () => {
mockProvider.tokens.mockResolvedValue(undefined);
mockFetch.mockResolvedValue(new Response('success', { status: 200 }));
// Test without baseUrl
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
await enhancedFetch('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledTimes(1);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBeNull();
});
it('should retry request after successful auth on 401 response (with explicit baseUrl)', async () => {
mockProvider.tokens
.mockResolvedValueOnce({
access_token: 'old-token',
token_type: 'Bearer',
expires_in: 3600
})
.mockResolvedValueOnce({
access_token: 'new-token',
token_type: 'Bearer',
expires_in: 3600
});
const unauthorizedResponse = new Response('Unauthorized', {
status: 401,
headers: { 'www-authenticate': 'Bearer realm="oauth"' }
});
const successResponse = new Response('success', { status: 200 });
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
const mockWWWAuthenticateParams = {
resourceMetadataUrl: new URL('https://oauth.example.com/.well-known/oauth-protected-resource'),
scope: 'read'
};
mockExtractWWWAuthenticateParams.mockReturnValue(mockWWWAuthenticateParams);
mockAuth.mockResolvedValue('AUTHORIZED');
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
const result = await enhancedFetch('https://api.example.com/data');
expect(result).toBe(successResponse);
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://api.example.com',
resourceMetadataUrl: mockWWWAuthenticateParams.resourceMetadataUrl,
scope: mockWWWAuthenticateParams.scope,
fetchFn: mockFetch
});
// Verify the retry used the new token
const retryCallArgs = mockFetch.mock.calls[1];
const retryHeaders = retryCallArgs[1]?.headers as Headers;
expect(retryHeaders.get('Authorization')).toBe('Bearer new-token');
});
it('should retry request after successful auth on 401 response (without baseUrl)', async () => {
mockProvider.tokens
.mockResolvedValueOnce({
access_token: 'old-token',
token_type: 'Bearer',
expires_in: 3600
})
.mockResolvedValueOnce({
access_token: 'new-token',
token_type: 'Bearer',
expires_in: 3600
});
const unauthorizedResponse = new Response('Unauthorized', {
status: 401,
headers: { 'www-authenticate': 'Bearer realm="oauth"' }
});
const successResponse = new Response('success', { status: 200 });
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
const mockWWWAuthenticateParams = {
resourceMetadataUrl: new URL('https://oauth.example.com/.well-known/oauth-protected-resource'),
scope: 'read'
};
mockExtractWWWAuthenticateParams.mockReturnValue(mockWWWAuthenticateParams);
mockAuth.mockResolvedValue('AUTHORIZED');
// Test without baseUrl - should extract from request URL
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
const result = await enhancedFetch('https://api.example.com/data');
expect(result).toBe(successResponse);
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://api.example.com', // Should be extracted from request URL
resourceMetadataUrl: mockWWWAuthenticateParams.resourceMetadataUrl,
scope: mockWWWAuthenticateParams.scope,
fetchFn: mockFetch
});
// Verify the retry used the new token
const retryCallArgs = mockFetch.mock.calls[1];
const retryHeaders = retryCallArgs[1]?.headers as Headers;
expect(retryHeaders.get('Authorization')).toBe('Bearer new-token');
});
it('should throw UnauthorizedError when auth returns REDIRECT (without baseUrl)', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockResolvedValue('REDIRECT');
// Test without baseUrl
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
await expect(enhancedFetch('https://api.example.com/data')).rejects.toThrow(
'Authentication requires user authorization - redirect initiated'
);
});
it('should throw UnauthorizedError when auth fails', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockRejectedValue(new Error('Network error'));
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
await expect(enhancedFetch('https://api.example.com/data')).rejects.toThrow('Failed to re-authenticate: Network error');
});
it('should handle persistent 401 responses after auth', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
// Always return 401
mockFetch.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockResolvedValue('AUTHORIZED');
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
await expect(enhancedFetch('https://api.example.com/data')).rejects.toThrow(
'Authentication failed for https://api.example.com/data'
);
// Should have made initial request + 1 retry after auth = 2 total
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledTimes(1);
});
it('should preserve original request method and body', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
mockFetch.mockResolvedValue(new Response('success', { status: 200 }));
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
const requestBody = JSON.stringify({ data: 'test' });
await enhancedFetch('https://api.example.com/data', {
method: 'POST',
body: requestBody,
headers: { 'Content-Type': 'application/json' }
});
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
method: 'POST',
body: requestBody,
headers: expect.any(Headers)
})
);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Content-Type')).toBe('application/json');
expect(headers.get('Authorization')).toBe('Bearer test-token');
});
it('should handle non-401 errors normally', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
const serverErrorResponse = new Response('Server Error', { status: 500 });
mockFetch.mockResolvedValue(serverErrorResponse);
const enhancedFetch = withOAuth(mockProvider, 'https://api.example.com')(mockFetch);
const result = await enhancedFetch('https://api.example.com/data');
expect(result).toBe(serverErrorResponse);
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockAuth).not.toHaveBeenCalled();
});
it('should handle URL object as input (without baseUrl)', async () => {
mockProvider.tokens.mockResolvedValue({
access_token: 'test-token',
token_type: 'Bearer',
expires_in: 3600
});
mockFetch.mockResolvedValue(new Response('success', { status: 200 }));
// Test URL object without baseUrl - should extract origin from URL object
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
await enhancedFetch(new URL('https://api.example.com/data'));
expect(mockFetch).toHaveBeenCalledWith(
expect.any(URL),
expect.objectContaining({
headers: expect.any(Headers)
})
);
});
it('should handle URL object in auth retry (without baseUrl)', async () => {
mockProvider.tokens
.mockResolvedValueOnce({
access_token: 'old-token',
token_type: 'Bearer',
expires_in: 3600
})
.mockResolvedValueOnce({
access_token: 'new-token',
token_type: 'Bearer',
expires_in: 3600
});
const unauthorizedResponse = new Response('Unauthorized', { status: 401 });
const successResponse = new Response('success', { status: 200 });
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
mockExtractWWWAuthenticateParams.mockReturnValue({});
mockAuth.mockResolvedValue('AUTHORIZED');
const enhancedFetch = withOAuth(mockProvider)(mockFetch);
const result = await enhancedFetch(new URL('https://api.example.com/data'));
expect(result).toBe(successResponse);
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://api.example.com', // Should extract origin from URL object
resourceMetadataUrl: undefined,
fetchFn: mockFetch
});
});
});
describe('withLogging', () => {
let mockFetch: MockedFunction<FetchLike>;
let mockLogger: MockedFunction<
(input: {
method: string;
url: string | URL;
status: number;
statusText: string;
duration: number;
requestHeaders?: Headers;
responseHeaders?: Headers;
error?: Error;
}) => void
>;
let consoleErrorSpy: MockInstance;
let consoleLogSpy: MockInstance;
beforeEach(() => {
vi.clearAllMocks();
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
mockFetch = vi.fn();
mockLogger = vi.fn();
});
afterEach(() => {
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
});
it('should log successful requests with default logger', async () => {
const response = new Response('success', { status: 200, statusText: 'OK' });
mockFetch.mockResolvedValue(response);
const enhancedFetch = withLogging()(mockFetch);
await enhancedFetch('https://api.example.com/data');
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringMatching(/HTTP GET https:\/\/api\.example\.com\/data 200 OK \(\d+\.\d+ms\)/)
);
});
it('should log error responses with default logger', async () => {
const response = new Response('Not Found', {
status: 404,
statusText: 'Not Found'
});
mockFetch.mockResolvedValue(response);
const enhancedFetch = withLogging()(mockFetch);
await enhancedFetch('https://api.example.com/data');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringMatching(/HTTP GET https:\/\/api\.example\.com\/data 404 Not Found \(\d+\.\d+ms\)/)
);
});
it('should log network errors with default logger', async () => {
const networkError = new Error('Network connection failed');
mockFetch.mockRejectedValue(networkError);
const enhancedFetch = withLogging()(mockFetch);
await expect(enhancedFetch('https://api.example.com/data')).rejects.toThrow('Network connection failed');
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringMatching(/HTTP GET https:\/\/api\.example\.com\/data failed: Network connection failed \(\d+\.\d+ms\)/)
);
});
it('should use custom logger when provided', async () => {
const response = new Response('success', { status: 200, statusText: 'OK' });
mockFetch.mockResolvedValue(response);
const enhancedFetch = withLogging({ logger: mockLogger })(mockFetch);
await enhancedFetch('https://api.example.com/data', { method: 'POST' });
expect(mockLogger).toHaveBeenCalledWith({
method: 'POST',
url: 'https://api.example.com/data',
status: 200,
statusText: 'OK',
duration: expect.any(Number),
requestHeaders: undefined,
responseHeaders: undefined
});
expect(consoleLogSpy).not.toHaveBeenCalled();
});
it('should include request headers when configured', async () => {
const response = new Response('success', { status: 200, statusText: 'OK' });
mockFetch.mockResolvedValue(response);
const enhancedFetch = withLogging({
logger: mockLogger,
includeRequestHeaders: true
})(mockFetch);
await enhancedFetch('https://api.example.com/data', {
headers: {
Authorization: 'Bearer token',
'Content-Type': 'application/json'
}
});
expect(mockLogger).toHaveBeenCalledWith({
method: 'GET',
url: 'https://api.example.com/data',
status: 200,
statusText: 'OK',
duration: expect.any(Number),
requestHeaders: expect.any(Headers),
responseHeaders: undefined
});
const logCall = mockLogger.mock.calls[0][0];
expect(logCall.requestHeaders?.get('Authorization')).toBe('Bearer token');
expect(logCall.requestHeaders?.get('Content-Type')).toBe('application/json');
});
it('should include response headers when configured', async () => {
const response = new Response('success', {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
mockFetch.mockResolvedValue(response);
const enhancedFetch = withLogging({
logger: mockLogger,
includeResponseHeaders: true
})(mockFetch);
await enhancedFetch('https://api.example.com/data');
const logCall = mockLogger.mock.calls[0][0];
expect(logCall.responseHeaders?.get('Content-Type')).toBe('application/json');
expect(logCall.responseHeaders?.get('Cache-Control')).toBe('no-cache');
});
it('should respect statusLevel option', async () => {
const successResponse = new Response('success', {
status: 200,
statusText: 'OK'
});
const errorResponse = new Response('Server Error', {
status: 500,
statusText: 'Internal Server Error'
});
mockFetch.mockResolvedValueOnce(successResponse).mockResolvedValueOnce(errorResponse);
const enhancedFetch = withLogging({
logger: mockLogger,
statusLevel: 400
})(mockFetch);
// 200 response should not be logged (below statusLevel 400)
await enhancedFetch('https://api.example.com/success');
expect(mockLogger).not.toHaveBeenCalled();
// 500 response should be logged (above statusLevel 400)
await enhancedFetch('https://api.example.com/error');
expect(mockLogger).toHaveBeenCalledWith({
method: 'GET',
url: 'https://api.example.com/error',
status: 500,
statusText: 'Internal Server Error',
duration: expect.any(Number),
requestHeaders: undefined,
responseHeaders: undefined
});
});
it('should always log network errors regardless of statusLevel', async () => {
const networkError = new Error('Connection timeout');
mockFetch.mockRejectedValue(networkError);
const enhancedFetch = withLogging({
logger: mockLogger,
statusLevel: 500 // Very high log level
})(mockFetch);
await expect(enhancedFetch('https://api.example.com/data')).rejects.toThrow('Connection timeout');
expect(mockLogger).toHaveBeenCalledWith({
method: 'GET',
url: 'https://api.example.com/data',
status: 0,
statusText: 'Network Error',
duration: expect.any(Number),
requestHeaders: undefined,
error: networkError
});
});
it('should include headers in default logger message when configured', async () => {
const response = new Response('success', {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'application/json' }
});
mockFetch.mockResolvedValue(response);
const enhancedFetch = withLogging({
includeRequestHeaders: true,
includeResponseHeaders: true
})(mockFetch);
await enhancedFetch('https://api.example.com/data', {
headers: { Authorization: 'Bearer token' }
});
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Request Headers: {authorization: Bearer token}'));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Response Headers: {content-type: application/json}'));
});
it('should measure request duration accurately', async () => {
// Mock a slow response
const response = new Response('success', { status: 200 });
mockFetch.mockImplementation(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
return response;
});
const enhancedFetch = withLogging({ logger: mockLogger })(mockFetch);
await enhancedFetch('https://api.example.com/data');
const logCall = mockLogger.mock.calls[0][0];
expect(logCall.duration).toBeGreaterThanOrEqual(90); // Allow some margin for timing
});
});
describe('applyMiddleware', () => {
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
vi.clearAllMocks();
mockFetch = vi.fn();
});
it('should compose no middleware correctly', () => {
const response = new Response('success', { status: 200 });
mockFetch.mockResolvedValue(response);
const composedFetch = applyMiddlewares()(mockFetch);
expect(composedFetch).toBe(mockFetch);
});
it('should compose single middleware correctly', async () => {
const response = new Response('success', { status: 200 });
mockFetch.mockResolvedValue(response);
// Create a middleware that adds a header
const middleware1 = (next: FetchLike) => async (input: string | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
headers.set('X-Middleware-1', 'applied');
return next(input, { ...init, headers });
};
const composedFetch = applyMiddlewares(middleware1)(mockFetch);
await composedFetch('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: expect.any(Headers)
})
);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('X-Middleware-1')).toBe('applied');
});
it('should compose multiple middleware in order', async () => {
const response = new Response('success', { status: 200 });
mockFetch.mockResolvedValue(response);
// Create middleware that add identifying headers
const middleware1 = (next: FetchLike) => async (input: string | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
headers.set('X-Middleware-1', 'applied');
return next(input, { ...init, headers });
};
const middleware2 = (next: FetchLike) => async (input: string | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
headers.set('X-Middleware-2', 'applied');
return next(input, { ...init, headers });
};
const middleware3 = (next: FetchLike) => async (input: string | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
headers.set('X-Middleware-3', 'applied');
return next(input, { ...init, headers });
};
const composedFetch = applyMiddlewares(middleware1, middleware2, middleware3)(mockFetch);
await composedFetch('https://api.example.com/data');
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('X-Middleware-1')).toBe('applied');
expect(headers.get('X-Middleware-2')).toBe('applied');
expect(headers.get('X-Middleware-3')).toBe('applied');
});
it('should work with real fetch middleware functions', async () => {
const response = new Response('success', { status: 200, statusText: 'OK' });
mockFetch.mockResolvedValue(response);
// Create middleware that add identifying headers
const oauthMiddleware = (next: FetchLike) => async (input: string | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
headers.set('Authorization', 'Bearer test-token');
return next(input, { ...init, headers });
};
// Use custom logger to avoid console output
const mockLogger = vi.fn();
const composedFetch = applyMiddlewares(oauthMiddleware, withLogging({ logger: mockLogger, statusLevel: 0 }))(mockFetch);
await composedFetch('https://api.example.com/data');
// Should have both Authorization header and logging
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBe('Bearer test-token');
expect(mockLogger).toHaveBeenCalledWith({
method: 'GET',
url: 'https://api.example.com/data',
status: 200,
statusText: 'OK',
duration: expect.any(Number),
requestHeaders: undefined,
responseHeaders: undefined
});
});
it('should preserve error propagation through middleware', async () => {
const errorMiddleware = (next: FetchLike) => async (input: string | URL, init?: RequestInit) => {
try {
return await next(input, init);
} catch (error) {
// Add context to the error
throw new Error(`Middleware error: ${error instanceof Error ? error.message : String(error)}`);
}
};
const originalError = new Error('Network failure');
mockFetch.mockRejectedValue(originalError);
const composedFetch = applyMiddlewares(errorMiddleware)(mockFetch);
await expect(composedFetch('https://api.example.com/data')).rejects.toThrow('Middleware error: Network failure');
});
});
describe('Integration Tests', () => {
let mockProvider: Mocked<OAuthClientProvider>;
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
vi.clearAllMocks();
mockProvider = {
get redirectUrl() {
return 'http://localhost/callback';
},
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
tokens: vi.fn(),
saveTokens: vi.fn(),
clientInformation: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};
mockFetch = vi.fn();
});
it('should work with SSE transport pattern', async () => {
// Simulate how SSE transport might use the middleware
mockProvider.tokens.mockResolvedValue({
access_token: 'sse-token',
token_type: 'Bearer',
expires_in: 3600
});
const response = new Response('{"jsonrpc":"2.0","id":1,"result":{}}', {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
mockFetch.mockResolvedValue(response);
// Use custom logger to avoid console output
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
withLogging({ logger: mockLogger, statusLevel: 400 }) // Only log errors
)(mockFetch);
// Simulate SSE POST request
await enhancedFetch('https://mcp-server.example.com/endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'tools/list',
id: 1
})
});
expect(mockFetch).toHaveBeenCalledWith(
'https://mcp-server.example.com/endpoint',
expect.objectContaining({
method: 'POST',
headers: expect.any(Headers),
body: expect.any(String)
})
);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBe('Bearer sse-token');
expect(headers.get('Content-Type')).toBe('application/json');
});
it('should work with StreamableHTTP transport pattern', async () => {
// Simulate how StreamableHTTP transport might use the middleware
mockProvider.tokens.mockResolvedValue({
access_token: 'streamable-token',
token_type: 'Bearer',
expires_in: 3600
});
const response = new Response(null, {
status: 202,
headers: { 'mcp-session-id': 'session-123' }
});
mockFetch.mockResolvedValue(response);
// Use custom logger to avoid console output
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://streamable-server.example.com'),
withLogging({
logger: mockLogger,
includeResponseHeaders: true,
statusLevel: 0
})
)(mockFetch);
// Simulate StreamableHTTP initialization request
await enhancedFetch('https://streamable-server.example.com/mcp', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream'
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'initialize',
params: { protocolVersion: '2025-03-26', clientInfo: { name: 'test' } },
id: 1
})
});
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBe('Bearer streamable-token');
expect(headers.get('Accept')).toBe('application/json, text/event-stream');
});
it('should handle auth retry in transport-like scenario', async () => {
mockProvider.tokens
.mockResolvedValueOnce({
access_token: 'expired-token',
token_type: 'Bearer',
expires_in: 3600
})
.mockResolvedValueOnce({
access_token: 'fresh-token',
token_type: 'Bearer',
expires_in: 3600
});
const unauthorizedResponse = new Response('{"error":"invalid_token"}', {
status: 401,
headers: { 'www-authenticate': 'Bearer realm="mcp"' }
});
const successResponse = new Response('{"jsonrpc":"2.0","id":1,"result":{}}', {
status: 200
});
mockFetch.mockResolvedValueOnce(unauthorizedResponse).mockResolvedValueOnce(successResponse);
mockExtractWWWAuthenticateParams.mockReturnValue({
resourceMetadataUrl: new URL('https://auth.example.com/.well-known/oauth-protected-resource'),
scope: 'read'
});
mockAuth.mockResolvedValue('AUTHORIZED');
// Use custom logger to avoid console output
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
withLogging({ logger: mockLogger, statusLevel: 0 })
)(mockFetch);
const result = await enhancedFetch('https://mcp-server.example.com/endpoint', {
method: 'POST',
body: JSON.stringify({ jsonrpc: '2.0', method: 'test', id: 1 })
});
expect(result).toBe(successResponse);
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockAuth).toHaveBeenCalledWith(mockProvider, {
serverUrl: 'https://mcp-server.example.com',
resourceMetadataUrl: new URL('https://auth.example.com/.well-known/oauth-protected-resource'),
scope: 'read',
fetchFn: mockFetch
});
});
});
describe('createMiddleware', () => {
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
vi.clearAllMocks();
mockFetch = vi.fn();
});
it('should create middleware with cleaner syntax', async () => {
const response = new Response('success', { status: 200 });
mockFetch.mockResolvedValue(response);
const customMiddleware = createMiddleware(async (next, input, init) => {
const headers = new Headers(init?.headers);
headers.set('X-Custom-Header', 'custom-value');
return next(input, { ...init, headers });
});
const enhancedFetch = customMiddleware(mockFetch);
await enhancedFetch('https://api.example.com/data');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
headers: expect.any(Headers)
})
);
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('X-Custom-Header')).toBe('custom-value');
});
it('should support conditional middleware logic', async () => {
const apiResponse = new Response('api response', { status: 200 });
const publicResponse = new Response('public response', { status: 200 });
mockFetch.mockResolvedValueOnce(apiResponse).mockResolvedValueOnce(publicResponse);
const conditionalMiddleware = createMiddleware(async (next, input, init) => {
const url = typeof input === 'string' ? input : input.toString();
if (url.includes('/api/')) {
const headers = new Headers(init?.headers);
headers.set('X-API-Version', 'v2');
return next(input, { ...init, headers });
}
return next(input, init);
});
const enhancedFetch = conditionalMiddleware(mockFetch);
// Test API route
await enhancedFetch('https://example.com/api/users');
let callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('X-API-Version')).toBe('v2');
// Test non-API route
await enhancedFetch('https://example.com/public/page');
callArgs = mockFetch.mock.calls[1];
const maybeHeaders = callArgs[1]?.headers as Headers | undefined;
expect(maybeHeaders?.get('X-API-Version')).toBeUndefined();
});
it('should support short-circuit responses', async () => {
const customMiddleware = createMiddleware(async (next, input, init) => {
const url = typeof input === 'string' ? input : input.toString();
// Short-circuit for specific URL
if (url.includes('/cached')) {
return new Response('cached data', { status: 200 });
}
return next(input, init);
});
const enhancedFetch = customMiddleware(mockFetch);
// Test cached route (should not call mockFetch)
const cachedResponse = await enhancedFetch('https://example.com/cached/data');
expect(await cachedResponse.text()).toBe('cached data');
expect(mockFetch).not.toHaveBeenCalled();
// Test normal route
mockFetch.mockResolvedValue(new Response('fresh data', { status: 200 }));
const normalResponse = await enhancedFetch('https://example.com/normal/data');
expect(await normalResponse.text()).toBe('fresh data');
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('should handle response transformation', async () => {
const originalResponse = new Response('{"data": "original"}', {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
mockFetch.mockResolvedValue(originalResponse);
const transformMiddleware = createMiddleware(async (next, input, init) => {
const response = await next(input, init);
if (response.headers.get('content-type')?.includes('application/json')) {
const data = await response.json();
const transformed = { ...data, timestamp: 123456789 };
return new Response(JSON.stringify(transformed), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
return response;
});
const enhancedFetch = transformMiddleware(mockFetch);
const response = await enhancedFetch('https://api.example.com/data');
const result = await response.json();
expect(result).toEqual({
data: 'original',
timestamp: 123456789
});
});
it('should support error handling and recovery', async () => {
let attemptCount = 0;
mockFetch.mockImplementation(async () => {
attemptCount++;
if (attemptCount === 1) {
throw new Error('Network error');
}
return new Response('success', { status: 200 });
});
const retryMiddleware = createMiddleware(async (next, input, init) => {
try {
return await next(input, init);
} catch (error) {
// Retry once on network error
console.log('Retrying request after error:', error);
return await next(input, init);
}
});
const enhancedFetch = retryMiddleware(mockFetch);
const response = await enhancedFetch('https://api.example.com/data');
expect(await response.text()).toBe('success');
expect(mockFetch).toHaveBeenCalledTimes(2);
});
it('should compose well with other middleware', async () => {
const response = new Response('success', { status: 200 });
mockFetch.mockResolvedValue(response);
// Create custom middleware using createMiddleware
const customAuth = createMiddleware(async (next, input, init) => {
const headers = new Headers(init?.headers);
headers.set('Authorization', 'Custom token');
return next(input, { ...init, headers });
});
const customLogging = createMiddleware(async (next, input, init) => {
const url = typeof input === 'string' ? input : input.toString();
console.log(`Request to: ${url}`);
const response = await next(input, init);
console.log(`Response status: ${response.status}`);
return response;
});
// Compose with existing middleware
const enhancedFetch = applyMiddlewares(customAuth, customLogging, withLogging({ statusLevel: 400 }))(mockFetch);
await enhancedFetch('https://api.example.com/data');
const callArgs = mockFetch.mock.calls[0];
const headers = callArgs[1]?.headers as Headers;
expect(headers.get('Authorization')).toBe('Custom token');
});
it('should have access to both input types (string and URL)', async () => {
const response = new Response('success', { status: 200 });
mockFetch.mockResolvedValue(response);
let capturedInputType: string | undefined;
const inspectMiddleware = createMiddleware(async (next, input, init) => {
capturedInputType = typeof input === 'string' ? 'string' : 'URL';
return next(input, init);
});
const enhancedFetch = inspectMiddleware(mockFetch);
// Test with string input
await enhancedFetch('https://api.example.com/data');
expect(capturedInputType).toBe('string');
// Test with URL input
await enhancedFetch(new URL('https://api.example.com/data'));
expect(capturedInputType).toBe('URL');
});
});