Migrate to vitest from jest (#1074)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
Co-authored-by: Felix Weinberger <fweinberger@anthropic.com>
This commit is contained in:
Matt 2025-11-14 14:08:00 +00:00 • committed by GitHub
commit 98df118a4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1713 additions and 3434 deletions

View file

@ -1,14 +0,0 @@
import { createDefaultEsmPreset } from 'ts-jest';
const defaultEsmPreset = createDefaultEsmPreset();
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
...defaultEsmPreset,
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'^pkce-challenge$': '<rootDir>/src/__mocks__/pkce-challenge.ts'
},
transformIgnorePatterns: ['/node_modules/(?!eventsource)/'],
testPathIgnorePatterns: ['/node_modules/', '/dist/']
};

4249
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -71,7 +71,8 @@
"lint": "eslint src/ && prettier --check .",
"lint:fix": "eslint src/ --fix && prettier --write .",
"check": "npm run typecheck && npm run lint",
"test": "jest",
"test": "vitest run",
"test:watch": "vitest",
"start": "npm run server",
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
"client": "tsx scripts/cli.ts client"
@ -102,27 +103,24 @@
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@eslint/js": "^9.8.0",
"@jest-mock/express": "^3.0.0",
"@types/content-type": "^1.1.8",
"@types/cors": "^2.8.17",
"@types/cross-spawn": "^6.0.6",
"@types/eslint__js": "^8.42.3",
"@types/eventsource": "^1.1.15",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.12",
"@types/node": "^22.0.2",
"@types/supertest": "^6.0.2",
"@types/ws": "^8.5.12",
"@typescript/native-preview": "^7.0.0-dev.20251103.1",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.1.8",
"jest": "^29.7.0",
"prettier": "3.6.2",
"supertest": "^7.0.0",
"ts-jest": "^29.2.4",
"tsx": "^4.16.5",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0",
"vitest": "^4.0.8",
"ws": "^8.18.0"
},
"resolutions": {

View file

@ -15,9 +15,18 @@ import {
} from './auth.js';
import { ServerError } from '../server/auth/errors.js';
import { AuthorizationServerMetadata } from '../shared/auth.js';
import { expect, vi, type Mock } from 'vitest';
// Mock pkce-challenge
vi.mock('pkce-challenge', () => ({
default: () => ({
code_verifier: 'test_verifier',
code_challenge: 'test_challenge'
})
}));
// Mock fetch globally
const mockFetch = jest.fn();
const mockFetch = vi.fn();
global.fetch = mockFetch;
describe('OAuth Authorization', () => {
@ -30,7 +39,7 @@ describe('OAuth Authorization', () => {
const resourceUrl = 'https://resource.example.com/.well-known/oauth-protected-resource';
const mockResponse = {
headers: {
get: jest.fn(name => (name === 'WWW-Authenticate' ? `Bearer realm="mcp", resource_metadata="${resourceUrl}"` : null))
get: vi.fn(name => (name === 'WWW-Authenticate' ? `Bearer realm="mcp", resource_metadata="${resourceUrl}"` : null))
}
} as unknown as Response;
@ -41,7 +50,7 @@ describe('OAuth Authorization', () => {
const scope = 'read';
const mockResponse = {
headers: {
get: jest.fn(name => (name === 'WWW-Authenticate' ? `Bearer realm="mcp", scope="${scope}"` : null))
get: vi.fn(name => (name === 'WWW-Authenticate' ? `Bearer realm="mcp", scope="${scope}"` : null))
}
} as unknown as Response;
@ -53,7 +62,7 @@ describe('OAuth Authorization', () => {
const scope = 'read';
const mockResponse = {
headers: {
get: jest.fn(name =>
get: vi.fn(name =>
name === 'WWW-Authenticate' ? `Basic realm="mcp", resource_metadata="${resourceUrl}", scope="${scope}"` : null
)
}
@ -65,7 +74,7 @@ describe('OAuth Authorization', () => {
it('returns empty object if resource_metadata and scope not present', async () => {
const mockResponse = {
headers: {
get: jest.fn(name => (name === 'WWW-Authenticate' ? `Bearer realm="mcp"` : null))
get: vi.fn(name => (name === 'WWW-Authenticate' ? `Bearer realm="mcp"` : null))
}
} as unknown as Response;
@ -77,7 +86,7 @@ describe('OAuth Authorization', () => {
const scope = 'read';
const mockResponse = {
headers: {
get: jest.fn(name =>
get: vi.fn(name =>
name === 'WWW-Authenticate' ? `Bearer realm="mcp", resource_metadata="${resourceUrl}", scope="${scope}"` : null
)
}
@ -393,7 +402,7 @@ describe('OAuth Authorization', () => {
authorization_servers: ['https://auth.example.com']
};
const customFetch = jest.fn().mockResolvedValue({
const customFetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validMetadata
@ -689,7 +698,7 @@ describe('OAuth Authorization', () => {
code_challenge_methods_supported: ['S256']
};
const customFetch = jest.fn().mockResolvedValue({
const customFetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validMetadata
@ -851,7 +860,7 @@ describe('OAuth Authorization', () => {
});
it('supports custom fetch function', async () => {
const customFetch = jest.fn().mockResolvedValue({
const customFetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validOAuthMetadata
@ -1115,14 +1124,16 @@ describe('OAuth Authorization', () => {
href: 'https://auth.example.com/token'
}),
expect.objectContaining({
method: 'POST',
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded'
})
method: 'POST'
})
);
const body = mockFetch.mock.calls[0][1].body as URLSearchParams;
const options = mockFetch.mock.calls[0][1];
expect(options.headers).toBeInstanceOf(Headers);
expect(options.headers.get('Content-Type')).toBe('application/x-www-form-urlencoded');
expect(options.body).toBeInstanceOf(URLSearchParams);
const body = options.body as URLSearchParams;
expect(body.get('grant_type')).toBe('authorization_code');
expect(body.get('code')).toBe('code123');
expect(body.get('code_verifier')).toBe('verifier123');
@ -1217,7 +1228,7 @@ describe('OAuth Authorization', () => {
});
it('supports overriding the fetch function used for requests', async () => {
const customFetch = jest.fn().mockResolvedValue({
const customFetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => validTokens
@ -1506,16 +1517,16 @@ describe('OAuth Authorization', () => {
client_name: 'Test Client'
};
},
clientInformation: jest.fn(),
tokens: jest.fn(),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn()
clientInformation: vi.fn(),
tokens: vi.fn(),
saveTokens: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn()
};
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('falls back to /.well-known/oauth-authorization-server when no protected-resource-metadata', async () => {
@ -1567,9 +1578,9 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
mockProvider.saveClientInformation = jest.fn();
(mockProvider.clientInformation as Mock).mockResolvedValue(undefined);
(mockProvider.tokens as Mock).mockResolvedValue(undefined);
mockProvider.saveClientInformation = vi.fn();
// Call the auth function
const result = await auth(mockProvider, {
@ -1639,9 +1650,9 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
mockProvider.saveClientInformation = jest.fn();
(mockProvider.clientInformation as Mock).mockResolvedValue(undefined);
(mockProvider.tokens as Mock).mockResolvedValue(undefined);
mockProvider.saveClientInformation = vi.fn();
// Call the auth function with a server URL that has a path
const result = await auth(mockProvider, {
@ -1657,7 +1668,7 @@ describe('OAuth Authorization', () => {
call[0].toString().includes('/.well-known/oauth-authorization-server')
);
expect(authServerCall).toBeDefined();
expect(authServerCall[0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server');
expect(authServerCall![0].toString()).toBe('https://resource.example.com/.well-known/oauth-authorization-server');
});
it('passes resource parameter through authorization flow', async () => {
@ -1690,13 +1701,13 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods for authorization flow
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as Mock).mockResolvedValue(undefined);
// Call auth without authorization code (should trigger redirect)
const result = await auth(mockProvider, {
@ -1712,7 +1723,7 @@ describe('OAuth Authorization', () => {
})
);
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const redirectCall = (mockProvider.redirectToAuthorization as Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
expect(authUrl.searchParams.get('resource')).toBe('https://api.example.com/mcp-server');
});
@ -1760,12 +1771,12 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods for token exchange
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.codeVerifier as jest.Mock).mockResolvedValue('test-verifier');
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.codeVerifier as Mock).mockResolvedValue('test-verifier');
(mockProvider.saveTokens as Mock).mockResolvedValue(undefined);
// Call auth with authorization code
const result = await auth(mockProvider, {
@ -1826,15 +1837,15 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods for token refresh
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.tokens as jest.Mock).mockResolvedValue({
(mockProvider.tokens as Mock).mockResolvedValue({
access_token: 'old-access',
refresh_token: 'refresh123'
});
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveTokens as Mock).mockResolvedValue(undefined);
// Call auth with existing tokens (should trigger refresh)
const result = await auth(mockProvider, {
@ -1854,7 +1865,7 @@ describe('OAuth Authorization', () => {
});
it('skips default PRM resource validation when custom validateResourceURL is provided', async () => {
const mockValidateResourceURL = jest.fn().mockResolvedValue(undefined);
const mockValidateResourceURL = vi.fn().mockResolvedValue(undefined);
const providerWithCustomValidation = {
...mockProvider,
validateResourceURL: mockValidateResourceURL
@ -1892,13 +1903,13 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods
(providerWithCustomValidation.clientInformation as jest.Mock).mockResolvedValue({
(providerWithCustomValidation.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(providerWithCustomValidation.tokens as jest.Mock).mockResolvedValue(undefined);
(providerWithCustomValidation.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(providerWithCustomValidation.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
(providerWithCustomValidation.tokens as Mock).mockResolvedValue(undefined);
(providerWithCustomValidation.saveCodeVerifier as Mock).mockResolvedValue(undefined);
(providerWithCustomValidation.redirectToAuthorization as Mock).mockResolvedValue(undefined);
// Call auth - should succeed despite resource mismatch because custom validation overrides default
const result = await auth(providerWithCustomValidation, {
@ -1947,13 +1958,13 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as Mock).mockResolvedValue(undefined);
// Call auth with a URL that has the resource as prefix
const result = await auth(mockProvider, {
@ -1969,7 +1980,7 @@ describe('OAuth Authorization', () => {
})
);
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const redirectCall = (mockProvider.redirectToAuthorization as Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
// Should use the PRM's resource value, not the full requested URL
expect(authUrl.searchParams.get('resource')).toBe('https://api.example.com/');
@ -2005,13 +2016,13 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as Mock).mockResolvedValue(undefined);
// Call auth - should not include resource parameter
const result = await auth(mockProvider, {
@ -2027,7 +2038,7 @@ describe('OAuth Authorization', () => {
})
);
const redirectCall = (mockProvider.redirectToAuthorization as jest.Mock).mock.calls[0];
const redirectCall = (mockProvider.redirectToAuthorization as Mock).mock.calls[0];
const authUrl: URL = redirectCall[0];
// Resource parameter should not be present when PRM is not available
expect(authUrl.searchParams.has('resource')).toBe(false);
@ -2072,12 +2083,12 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods for token exchange
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.codeVerifier as jest.Mock).mockResolvedValue('test-verifier');
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.codeVerifier as Mock).mockResolvedValue('test-verifier');
(mockProvider.saveTokens as Mock).mockResolvedValue(undefined);
// Call auth with authorization code
const result = await auth(mockProvider, {
@ -2135,15 +2146,15 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods for token refresh
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.tokens as jest.Mock).mockResolvedValue({
(mockProvider.tokens as Mock).mockResolvedValue({
access_token: 'old-access',
refresh_token: 'refresh123'
});
(mockProvider.saveTokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveTokens as Mock).mockResolvedValue(undefined);
// Call auth with existing tokens (should trigger refresh)
const result = await auth(mockProvider, {
@ -2196,13 +2207,13 @@ describe('OAuth Authorization', () => {
});
// Mock provider methods
(mockProvider.clientInformation as jest.Mock).mockResolvedValue({
(mockProvider.clientInformation as Mock).mockResolvedValue({
client_id: 'test-client',
client_secret: 'test-secret'
});
(mockProvider.tokens as jest.Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as jest.Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as jest.Mock).mockResolvedValue(undefined);
(mockProvider.tokens as Mock).mockResolvedValue(undefined);
(mockProvider.saveCodeVerifier as Mock).mockResolvedValue(undefined);
(mockProvider.redirectToAuthorization as Mock).mockResolvedValue(undefined);
// Call auth with serverUrl that has a path
const result = await auth(mockProvider, {
@ -2222,7 +2233,7 @@ describe('OAuth Authorization', () => {
});
it('supports overriding the fetch function used for requests', async () => {
const customFetch = jest.fn();
const customFetch = vi.fn();
// Mock PRM discovery
customFetch.mockResolvedValueOnce({
@ -2258,15 +2269,15 @@ describe('OAuth Authorization', () => {
redirect_uris: ['http://localhost:3000/callback']
};
},
clientInformation: jest.fn().mockResolvedValue({
clientInformation: vi.fn().mockResolvedValue({
client_id: 'client123',
client_secret: 'secret123'
}),
tokens: jest.fn().mockResolvedValue(undefined),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn().mockResolvedValue('verifier123')
tokens: vi.fn().mockResolvedValue(undefined),
saveTokens: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn().mockResolvedValue('verifier123')
};
const result = await auth(mockProvider, {

View file

@ -2,33 +2,34 @@ import { StdioClientTransport, getDefaultEnvironment } from './stdio.js';
import spawn from 'cross-spawn';
import { JSONRPCMessage } from '../types.js';
import { ChildProcess } from 'node:child_process';
import { Mock, MockedFunction } from 'vitest';
// mock cross-spawn
jest.mock('cross-spawn');
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
vi.mock('cross-spawn');
const mockSpawn = spawn as unknown as MockedFunction<typeof spawn>;
describe('StdioClientTransport using cross-spawn', () => {
beforeEach(() => {
// mock cross-spawn's return value
mockSpawn.mockImplementation(() => {
const mockProcess: {
on: jest.Mock;
stdin?: { on: jest.Mock; write: jest.Mock };
stdout?: { on: jest.Mock };
on: Mock;
stdin?: { on: Mock; write: Mock };
stdout?: { on: Mock };
stderr?: null;
} = {
on: jest.fn((event: string, callback: () => void) => {
on: vi.fn((event: string, callback: () => void) => {
if (event === 'spawn') {
callback();
}
return mockProcess;
}),
stdin: {
on: jest.fn(),
write: jest.fn().mockReturnValue(true)
on: vi.fn(),
write: vi.fn().mockReturnValue(true)
},
stdout: {
on: jest.fn()
on: vi.fn()
},
stderr: null
};
@ -37,7 +38,7 @@ describe('StdioClientTransport using cross-spawn', () => {
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
test('should call cross-spawn correctly', async () => {
@ -105,30 +106,30 @@ describe('StdioClientTransport using cross-spawn', () => {
// get the mock process object
const mockProcess: {
on: jest.Mock;
on: Mock;
stdin: {
on: jest.Mock;
write: jest.Mock;
once: jest.Mock;
on: Mock;
write: Mock;
once: Mock;
};
stdout: {
on: jest.Mock;
on: Mock;
};
stderr: null;
} = {
on: jest.fn((event: string, callback: () => void) => {
on: vi.fn((event: string, callback: () => void) => {
if (event === 'spawn') {
callback();
}
return mockProcess;
}),
stdin: {
on: jest.fn(),
write: jest.fn().mockReturnValue(true),
once: jest.fn()
on: vi.fn(),
write: vi.fn().mockReturnValue(true),
once: vi.fn()
},
stdout: {
on: jest.fn()
on: vi.fn()
},
stderr: null
};

View file

@ -27,9 +27,9 @@ import { InMemoryTransport } from '../inMemory.js';
*/
test('should initialize with matching protocol version', async () => {
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.method === 'initialize') {
clientTransport.onmessage?.({
jsonrpc: '2.0',
@ -86,9 +86,9 @@ test('should initialize with matching protocol version', async () => {
test('should initialize with supported older protocol version', async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.method === 'initialize') {
clientTransport.onmessage?.({
jsonrpc: '2.0',
@ -136,9 +136,9 @@ test('should initialize with supported older protocol version', async () => {
*/
test('should reject unsupported protocol version', async () => {
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.method === 'initialize') {
clientTransport.onmessage?.({
jsonrpc: '2.0',

View file

@ -1,27 +1,28 @@
import { withOAuth, withLogging, applyMiddlewares, createMiddleware } from './middleware.js';
import { OAuthClientProvider } from './auth.js';
import { FetchLike } from '../shared/transport.js';
import { MockInstance, Mocked, MockedFunction } from 'vitest';
jest.mock('../client/auth.js', () => {
const actual = jest.requireActual('../client/auth.js');
vi.mock('../client/auth.js', async () => {
const actual = await vi.importActual<typeof import('../client/auth.js')>('../client/auth.js');
return {
...actual,
auth: jest.fn(),
extractWWWAuthenticateParams: jest.fn()
auth: vi.fn(),
extractWWWAuthenticateParams: vi.fn()
};
});
import { auth, extractWWWAuthenticateParams } from './auth.js';
const mockAuth = auth as jest.MockedFunction<typeof auth>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as jest.MockedFunction<typeof extractWWWAuthenticateParams>;
const mockAuth = auth as MockedFunction<typeof auth>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as MockedFunction<typeof extractWWWAuthenticateParams>;
describe('withOAuth', () => {
let mockProvider: jest.Mocked<OAuthClientProvider>;
let mockFetch: jest.MockedFunction<FetchLike>;
let mockProvider: Mocked<OAuthClientProvider>;
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockProvider = {
get redirectUrl() {
@ -30,16 +31,16 @@ describe('withOAuth', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
tokens: jest.fn(),
saveTokens: jest.fn(),
clientInformation: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
invalidateCredentials: jest.fn()
tokens: vi.fn(),
saveTokens: vi.fn(),
clientInformation: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};
mockFetch = jest.fn();
mockFetch = vi.fn();
});
it('should add Authorization header when tokens are available (with explicit baseUrl)', async () => {
@ -371,8 +372,8 @@ describe('withOAuth', () => {
});
describe('withLogging', () => {
let mockFetch: jest.MockedFunction<FetchLike>;
let mockLogger: jest.MockedFunction<
let mockFetch: MockedFunction<FetchLike>;
let mockLogger: MockedFunction<
(input: {
method: string;
url: string | URL;
@ -384,17 +385,17 @@ describe('withLogging', () => {
error?: Error;
}) => void
>;
let consoleErrorSpy: jest.SpyInstance;
let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: MockInstance;
let consoleLogSpy: MockInstance;
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
mockFetch = jest.fn();
mockLogger = jest.fn();
mockFetch = vi.fn();
mockLogger = vi.fn();
});
afterEach(() => {
@ -614,11 +615,11 @@ describe('withLogging', () => {
});
describe('applyMiddleware', () => {
let mockFetch: jest.MockedFunction<FetchLike>;
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
jest.clearAllMocks();
mockFetch = jest.fn();
vi.clearAllMocks();
mockFetch = vi.fn();
});
it('should compose no middleware correctly', () => {
@ -703,7 +704,7 @@ describe('applyMiddleware', () => {
};
// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const composedFetch = applyMiddlewares(oauthMiddleware, withLogging({ logger: mockLogger, statusLevel: 0 }))(mockFetch);
await composedFetch('https://api.example.com/data');
@ -743,11 +744,11 @@ describe('applyMiddleware', () => {
});
describe('Integration Tests', () => {
let mockProvider: jest.Mocked<OAuthClientProvider>;
let mockFetch: jest.MockedFunction<FetchLike>;
let mockProvider: Mocked<OAuthClientProvider>;
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
mockProvider = {
get redirectUrl() {
@ -756,16 +757,16 @@ describe('Integration Tests', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
tokens: jest.fn(),
saveTokens: jest.fn(),
clientInformation: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
invalidateCredentials: jest.fn()
tokens: vi.fn(),
saveTokens: vi.fn(),
clientInformation: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};
mockFetch = jest.fn();
mockFetch = vi.fn();
});
it('should work with SSE transport pattern', async () => {
@ -783,7 +784,7 @@ describe('Integration Tests', () => {
mockFetch.mockResolvedValue(response);
// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
withLogging({ logger: mockLogger, statusLevel: 400 }) // Only log errors
@ -830,7 +831,7 @@ describe('Integration Tests', () => {
mockFetch.mockResolvedValue(response);
// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://streamable-server.example.com'),
withLogging({
@ -891,7 +892,7 @@ describe('Integration Tests', () => {
mockAuth.mockResolvedValue('AUTHORIZED');
// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
withLogging({ logger: mockLogger, statusLevel: 0 })
@ -914,11 +915,11 @@ describe('Integration Tests', () => {
});
describe('createMiddleware', () => {
let mockFetch: jest.MockedFunction<FetchLike>;
let mockFetch: MockedFunction<FetchLike>;
beforeEach(() => {
jest.clearAllMocks();
mockFetch = jest.fn();
vi.clearAllMocks();
mockFetch = vi.fn();
});
it('should create middleware with cleaner syntax', async () => {

View file

@ -5,6 +5,7 @@ import { SSEClientTransport } from './sse.js';
import { OAuthClientProvider, UnauthorizedError } from './auth.js';
import { OAuthTokens } from '../shared/auth.js';
import { InvalidClientError, InvalidGrantError, UnauthorizedClientError } from '../server/auth/errors.js';
import { Mock, Mocked, MockedFunction, MockInstance } from 'vitest';
describe('SSEClientTransport', () => {
let resourceServer: Server;
@ -15,7 +16,7 @@ describe('SSEClientTransport', () => {
let lastServerRequest: IncomingMessage;
let sendServerMessage: ((message: string) => void) | null = null;
beforeEach(done => {
beforeEach(async () => {
// Reset state
lastServerRequest = null as unknown as IncomingMessage;
sendServerMessage = null;
@ -74,13 +75,15 @@ describe('SSEClientTransport', () => {
});
// Start server on random port
resourceServer.listen(0, '127.0.0.1', () => {
const addr = resourceServer.address() as AddressInfo;
resourceBaseUrl = new URL(`http://127.0.0.1:${addr.port}`);
done();
await new Promise<void>(resolve => {
resourceServer.listen(0, '127.0.0.1', () => {
const addr = resourceServer.address() as AddressInfo;
resourceBaseUrl = new URL(`http://127.0.0.1:${addr.port}`);
resolve();
});
});
jest.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(async () => {
@ -88,7 +91,7 @@ describe('SSEClientTransport', () => {
await resourceServer.close();
await authServer.close();
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('connection handling', () => {
@ -262,7 +265,7 @@ describe('SSEClientTransport', () => {
it('uses custom fetch implementation from options', async () => {
const authToken = 'Bearer custom-token';
const fetchWithAuth = jest.fn((url: string | URL, init?: RequestInit) => {
const fetchWithAuth = vi.fn((url: string | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);
headers.set('Authorization', authToken);
return fetch(url.toString(), { ...init, headers });
@ -310,7 +313,7 @@ describe('SSEClientTransport', () => {
try {
// Mock fetch for the message sending test
global.fetch = jest.fn().mockResolvedValue({
global.fetch = vi.fn().mockResolvedValue({
ok: true
});
@ -331,7 +334,7 @@ describe('SSEClientTransport', () => {
})
);
const calledHeaders = (global.fetch as jest.Mock).mock.calls[0][1].headers;
const calledHeaders = (global.fetch as Mock).mock.calls[0][1].headers;
expect(calledHeaders.get('Authorization')).toBe(customHeaders.Authorization);
expect(calledHeaders.get('X-Custom-Header')).toBe(customHeaders['X-Custom-Header']);
expect(calledHeaders.get('content-type')).toBe('application/json');
@ -345,7 +348,7 @@ describe('SSEClientTransport', () => {
describe('auth handling', () => {
const authServerMetadataUrls = ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration'];
let mockAuthProvider: jest.Mocked<OAuthClientProvider>;
let mockAuthProvider: Mocked<OAuthClientProvider>;
beforeEach(() => {
mockAuthProvider = {
@ -355,13 +358,13 @@ describe('SSEClientTransport', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
clientInformation: jest.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
tokens: jest.fn(),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
invalidateCredentials: jest.fn()
clientInformation: vi.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
tokens: vi.fn(),
saveTokens: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};
});
@ -1122,19 +1125,10 @@ describe('SSEClientTransport', () => {
});
describe('custom fetch in auth code paths', () => {
let customFetch: jest.MockedFunction<typeof fetch>;
let globalFetchSpy: jest.SpyInstance;
let mockAuthProvider: jest.Mocked<OAuthClientProvider>;
let resourceServerHandler: jest.Mock<
void,
[
IncomingMessage,
ServerResponse<IncomingMessage> & {
req: IncomingMessage;
}
],
void
>;
let customFetch: MockedFunction<typeof fetch>;
let globalFetchSpy: MockInstance;
let mockAuthProvider: Mocked<OAuthClientProvider>;
let resourceServerHandler: Mock;
/**
* Helper function to create a mock auth provider with configurable behavior
@ -1147,7 +1141,7 @@ describe('SSEClientTransport', () => {
clientRegistered?: boolean;
authorizationCode?: string;
} = {}
): jest.Mocked<OAuthClientProvider> => {
): Mocked<OAuthClientProvider> => {
const tokens = config.hasTokens
? {
access_token: config.tokensExpired ? 'expired-token' : 'valid-token',
@ -1173,13 +1167,13 @@ describe('SSEClientTransport', () => {
client_name: 'Test Client'
};
},
clientInformation: jest.fn().mockResolvedValue(clientInfo),
tokens: jest.fn().mockResolvedValue(tokens),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn().mockResolvedValue('test-verifier'),
invalidateCredentials: jest.fn()
clientInformation: vi.fn().mockResolvedValue(clientInfo),
tokens: vi.fn().mockResolvedValue(tokens),
saveTokens: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn().mockResolvedValue('test-verifier'),
invalidateCredentials: vi.fn()
};
};
@ -1279,12 +1273,12 @@ describe('SSEClientTransport', () => {
const originalFetch = fetch;
// Create custom fetch spy that delegates to real fetch
customFetch = jest.fn((url, init) => {
customFetch = vi.fn((url, init) => {
return originalFetch(url.toString(), init);
});
// Spy on global fetch to detect unauthorized usage
globalFetchSpy = jest.spyOn(global, 'fetch');
globalFetchSpy = vi.spyOn(global, 'fetch');
// Create mock auth provider with default configuration
mockAuthProvider = createMockAuthProvider({
@ -1296,7 +1290,7 @@ describe('SSEClientTransport', () => {
await createCustomFetchMockAuthServer();
// Set up resource server
resourceServerHandler = jest.fn(
resourceServerHandler = vi.fn(
(
_req: IncomingMessage,
res: ServerResponse<IncomingMessage> & {
@ -1315,7 +1309,7 @@ describe('SSEClientTransport', () => {
it('uses custom fetch during auth flow on SSE connection 401 - no global fetch fallback', async () => {
// Set up resource server that returns 401 on SSE connection and provides OAuth metadata
resourceServerHandler.mockImplementation((req, res) => {
resourceServerHandler.mockImplementation((req: IncomingMessage, res: ServerResponse) => {
if (req.url === '/') {
// Return 401 to trigger auth flow
res.writeHead(401, {
@ -1359,7 +1353,7 @@ describe('SSEClientTransport', () => {
it('uses custom fetch during auth flow on POST request 401 - no global fetch fallback', async () => {
// Set up resource server that accepts SSE connection but returns 401 on POST
resourceServerHandler.mockImplementation((req, res) => {
resourceServerHandler.mockImplementation((req: IncomingMessage, res: ServerResponse) => {
switch (req.method) {
case 'GET':
if (req.url === '/') {

View file

@ -2,10 +2,11 @@ import { StartSSEOptions, StreamableHTTPClientTransport, StreamableHTTPReconnect
import { OAuthClientProvider, UnauthorizedError } from './auth.js';
import { JSONRPCMessage, JSONRPCRequest } from '../types.js';
import { InvalidClientError, InvalidGrantError, UnauthorizedClientError } from '../server/auth/errors.js';
import { type Mock, type Mocked } from 'vitest';
describe('StreamableHTTPClientTransport', () => {
let transport: StreamableHTTPClientTransport;
let mockAuthProvider: jest.Mocked<OAuthClientProvider>;
let mockAuthProvider: Mocked<OAuthClientProvider>;
beforeEach(() => {
mockAuthProvider = {
@ -15,21 +16,21 @@ describe('StreamableHTTPClientTransport', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
clientInformation: jest.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
tokens: jest.fn(),
saveTokens: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
invalidateCredentials: jest.fn()
clientInformation: vi.fn(() => ({ client_id: 'test-client-id', client_secret: 'test-client-secret' })),
tokens: vi.fn(),
saveTokens: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), { authProvider: mockAuthProvider });
jest.spyOn(global, 'fetch');
vi.spyOn(global, 'fetch');
});
afterEach(async () => {
await transport.close().catch(() => {});
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should send JSON-RPC messages via POST', async () => {
@ -40,7 +41,7 @@ describe('StreamableHTTPClientTransport', () => {
id: 'test-id'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 202,
headers: new Headers()
@ -64,7 +65,7 @@ describe('StreamableHTTPClientTransport', () => {
{ jsonrpc: '2.0', method: 'test2', params: {}, id: 'id2' }
];
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/event-stream' }),
@ -94,7 +95,7 @@ describe('StreamableHTTPClientTransport', () => {
id: 'init-id'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/event-stream', 'mcp-session-id': 'test-session-id' })
@ -103,7 +104,7 @@ describe('StreamableHTTPClientTransport', () => {
await transport.send(message);
// Send a second message that should include the session ID
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 202,
headers: new Headers()
@ -112,7 +113,7 @@ describe('StreamableHTTPClientTransport', () => {
await transport.send({ jsonrpc: '2.0', method: 'test', params: {} } as JSONRPCMessage);
// Check that second request included session ID header
const calls = (global.fetch as jest.Mock).mock.calls;
const calls = (global.fetch as Mock).mock.calls;
const lastCall = calls[calls.length - 1];
expect(lastCall[1].headers).toBeDefined();
expect(lastCall[1].headers.get('mcp-session-id')).toBe('test-session-id');
@ -130,7 +131,7 @@ describe('StreamableHTTPClientTransport', () => {
id: 'init-id'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/event-stream', 'mcp-session-id': 'test-session-id' })
@ -140,7 +141,7 @@ describe('StreamableHTTPClientTransport', () => {
expect(transport.sessionId).toBe('test-session-id');
// Now terminate the session
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers()
@ -149,7 +150,7 @@ describe('StreamableHTTPClientTransport', () => {
await transport.terminateSession();
// Verify the DELETE request was sent with the session ID
const calls = (global.fetch as jest.Mock).mock.calls;
const calls = (global.fetch as Mock).mock.calls;
const lastCall = calls[calls.length - 1];
expect(lastCall[1].method).toBe('DELETE');
expect(lastCall[1].headers.get('mcp-session-id')).toBe('test-session-id');
@ -170,7 +171,7 @@ describe('StreamableHTTPClientTransport', () => {
id: 'init-id'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/event-stream', 'mcp-session-id': 'test-session-id' })
@ -179,7 +180,7 @@ describe('StreamableHTTPClientTransport', () => {
await transport.send(message);
// Now terminate the session, but server responds with 405
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: false,
status: 405,
statusText: 'Method Not Allowed',
@ -197,7 +198,7 @@ describe('StreamableHTTPClientTransport', () => {
id: 'test-id'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
@ -205,7 +206,7 @@ describe('StreamableHTTPClientTransport', () => {
headers: new Headers()
});
const errorSpy = jest.fn();
const errorSpy = vi.fn();
transport.onerror = errorSpy;
await expect(transport.send(message)).rejects.toThrow('Error POSTing to endpoint (HTTP 404)');
@ -226,14 +227,14 @@ describe('StreamableHTTPClientTransport', () => {
id: 'test-id'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve(responseMessage)
});
const messageSpy = jest.fn();
const messageSpy = vi.fn();
transport.onmessage = messageSpy;
await transport.send(message);
@ -243,7 +244,7 @@ describe('StreamableHTTPClientTransport', () => {
it('should attempt initial GET connection and handle 405 gracefully', async () => {
// Mock the server not supporting GET for SSE (returning 405)
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: false,
status: 405,
statusText: 'Method Not Allowed'
@ -263,7 +264,7 @@ describe('StreamableHTTPClientTransport', () => {
);
// Verify transport still works after 405
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 202,
headers: new Headers()
@ -285,14 +286,14 @@ describe('StreamableHTTPClientTransport', () => {
});
// Mock successful GET connection
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/event-stream' }),
body: stream
});
const messageSpy = jest.fn();
const messageSpy = vi.fn();
transport.onmessage = messageSpy;
await transport.start();
@ -322,7 +323,7 @@ describe('StreamableHTTPClientTransport', () => {
});
};
(global.fetch as jest.Mock)
(global.fetch as Mock)
.mockResolvedValueOnce({
ok: true,
status: 200,
@ -336,7 +337,7 @@ describe('StreamableHTTPClientTransport', () => {
body: makeStream('request2')
});
const messageSpy = jest.fn();
const messageSpy = vi.fn();
transport.onmessage = messageSpy;
// Send two concurrent requests
@ -392,7 +393,7 @@ describe('StreamableHTTPClientTransport', () => {
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));
// Mock fetch to verify headers sent
const fetchSpy = global.fetch as jest.Mock;
const fetchSpy = global.fetch as Mock;
fetchSpy.mockReset();
fetchSpy.mockResolvedValue({
ok: true,
@ -418,7 +419,7 @@ describe('StreamableHTTPClientTransport', () => {
it('should throw error when invalid content-type is received', async () => {
// Clear any previous state from other tests
jest.clearAllMocks();
vi.clearAllMocks();
// Create a fresh transport instance
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'));
@ -437,10 +438,10 @@ describe('StreamableHTTPClientTransport', () => {
}
});
const errorSpy = jest.fn();
const errorSpy = vi.fn();
transport.onerror = errorSpy;
(global.fetch as jest.Mock).mockResolvedValueOnce({
(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'text/plain' }),
@ -454,7 +455,7 @@ describe('StreamableHTTPClientTransport', () => {
it('uses custom fetch implementation if provided', async () => {
// Create custom fetch
const customFetch = jest
const customFetch = vi
.fn()
.mockResolvedValueOnce(new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } }))
.mockResolvedValueOnce(new Response(null, { status: 202 }));
@ -488,7 +489,7 @@ describe('StreamableHTTPClientTransport', () => {
let actualReqInit: RequestInit = {};
(global.fetch as jest.Mock).mockImplementation(async (_url, reqInit) => {
(global.fetch as Mock).mockImplementation(async (_url, reqInit) => {
actualReqInit = reqInit;
return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } });
});
@ -518,7 +519,7 @@ describe('StreamableHTTPClientTransport', () => {
let actualReqInit: RequestInit = {};
(global.fetch as jest.Mock).mockImplementation(async (_url, reqInit) => {
(global.fetch as Mock).mockImplementation(async (_url, reqInit) => {
actualReqInit = reqInit;
return new Response(null, { status: 200, headers: { 'content-type': 'text/event-stream' } });
});
@ -576,7 +577,7 @@ describe('StreamableHTTPClientTransport', () => {
id: 'test-id'
};
(global.fetch as jest.Mock)
(global.fetch as Mock)
.mockResolvedValueOnce({
ok: false,
status: 401,
@ -596,8 +597,8 @@ describe('StreamableHTTPClientTransport', () => {
let transport: StreamableHTTPClientTransport;
// Use fake timers to control setTimeout and make the test instant.
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('should reconnect a GET-initiated notification stream that fails', async () => {
// ARRANGE
@ -610,7 +611,7 @@ describe('StreamableHTTPClientTransport', () => {
}
});
const errorSpy = jest.fn();
const errorSpy = vi.fn();
transport.onerror = errorSpy;
const failingStream = new ReadableStream({
@ -619,7 +620,7 @@ describe('StreamableHTTPClientTransport', () => {
}
});
const fetchMock = global.fetch as jest.Mock;
const fetchMock = global.fetch as Mock;
// Mock the initial GET request, which will fail.
fetchMock.mockResolvedValueOnce({
ok: true,
@ -639,7 +640,7 @@ describe('StreamableHTTPClientTransport', () => {
await transport.start();
// Trigger the GET stream directly using the internal method for a clean test.
await transport['_startOrAuthSse']({});
await jest.advanceTimersByTimeAsync(20); // Trigger reconnection timeout
await vi.advanceTimersByTimeAsync(20); // Trigger reconnection timeout
// ASSERT
expect(errorSpy).toHaveBeenCalledWith(
@ -664,7 +665,7 @@ describe('StreamableHTTPClientTransport', () => {
}
});
const errorSpy = jest.fn();
const errorSpy = vi.fn();
transport.onerror = errorSpy;
const failingStream = new ReadableStream({
@ -673,7 +674,7 @@ describe('StreamableHTTPClientTransport', () => {
}
});
const fetchMock = global.fetch as jest.Mock;
const fetchMock = global.fetch as Mock;
// Mock the POST request. It returns a streaming content-type but a failing body.
fetchMock.mockResolvedValueOnce({
ok: true,
@ -694,7 +695,7 @@ describe('StreamableHTTPClientTransport', () => {
await transport.start();
// Use the public `send` method to initiate a POST that gets a stream response.
await transport.send(requestMessage);
await jest.advanceTimersByTimeAsync(20); // Advance time to check for reconnections
await vi.advanceTimersByTimeAsync(20); // Advance time to check for reconnections
// ASSERT
expect(errorSpy).toHaveBeenCalledWith(
@ -728,7 +729,7 @@ describe('StreamableHTTPClientTransport', () => {
statusText: 'Unauthorized',
headers: new Headers()
};
(global.fetch as jest.Mock)
(global.fetch as Mock)
// Initial connection
.mockResolvedValueOnce(unauthedResponse)
// Resource discovery, path aware
@ -781,7 +782,7 @@ describe('StreamableHTTPClientTransport', () => {
statusText: 'Unauthorized',
headers: new Headers()
};
(global.fetch as jest.Mock)
(global.fetch as Mock)
// Initial connection
.mockResolvedValueOnce(unauthedResponse)
// Resource discovery, path aware
@ -832,7 +833,7 @@ describe('StreamableHTTPClientTransport', () => {
statusText: 'Unauthorized',
headers: new Headers()
};
(global.fetch as jest.Mock)
(global.fetch as Mock)
// Initial connection
.mockResolvedValueOnce(unauthedResponse)
// Resource discovery, path aware
@ -873,7 +874,7 @@ describe('StreamableHTTPClientTransport', () => {
};
// Create custom fetch
const customFetch = jest
const customFetch = vi
.fn()
// Initial connection
.mockResolvedValueOnce(unauthedResponse)
@ -935,7 +936,7 @@ describe('StreamableHTTPClientTransport', () => {
it('uses custom fetch in finishAuth method - no global fetch fallback', async () => {
// Create custom fetch
const customFetch = jest
const customFetch = vi
.fn()
// Protected resource metadata discovery
.mockResolvedValueOnce({
@ -1032,7 +1033,7 @@ describe('StreamableHTTPClientTransport', () => {
headers: new Headers()
};
(global.fetch as jest.Mock)
(global.fetch as Mock)
// First request - 401, triggers auth flow
.mockResolvedValueOnce(unauthedResponse)
// Resource discovery, path aware

View file

@ -11,7 +11,7 @@ describe('DemoInMemoryAuthProvider', () => {
const createMockResponse = (): Response & { getRedirectUrl: () => string } => {
let capturedRedirectUrl: string | undefined;
const mockRedirect = jest.fn().mockImplementation((url: string | number, status?: number) => {
const mockRedirect = vi.fn().mockImplementation((url: string | number, status?: number) => {
if (typeof url === 'string') {
capturedRedirectUrl = url;
} else if (typeof status === 'string') {
@ -22,9 +22,9 @@ describe('DemoInMemoryAuthProvider', () => {
const mockResponse = {
redirect: mockRedirect,
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
status: vi.fn().mockReturnThis(),
json: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
getRedirectUrl: () => {
if (capturedRedirectUrl === undefined) {
throw new Error('No redirect URL was captured. Ensure redirect() was called first.');
@ -234,7 +234,7 @@ describe('DemoInMemoryAuthProvider', () => {
});
it('should validate resource when validateResource is provided', async () => {
const validateResource = jest.fn().mockReturnValue(false);
const validateResource = vi.fn().mockReturnValue(false);
const strictProvider = new DemoInMemoryAuthProvider(validateResource);
const params: AuthorizationParams = {

View file

@ -2,7 +2,7 @@ import { Server } from '../server/index.js';
import { StdioServerTransport } from '../server/stdio.js';
describe('Process cleanup', () => {
jest.setTimeout(5000); // 5 second timeout
vi.setConfig({ testTimeout: 5000 }); // 5 second timeout
it('should exit cleanly after closing transport', async () => {
const server = new Server(

View file

@ -170,7 +170,7 @@ describe('Transport resumability', () => {
expect(sessionId).toBeDefined();
// Start a long-running notification stream with tracking of lastEventId
const onLastEventIdUpdate = jest.fn((eventId: string) => {
const onLastEventIdUpdate = vi.fn((eventId: string) => {
lastEventId = eventId;
});
expect(lastEventId).toBeUndefined();

View file

@ -220,7 +220,7 @@ describe('Authorization Handler', () => {
describe('Resource parameter validation', () => {
it('propagates resource parameter', async () => {
const mockProviderWithResource = jest.spyOn(mockProvider, 'authorize');
const mockProviderWithResource = vi.spyOn(mockProvider, 'authorize');
const response = await supertest(app).get('/authorize').query({
client_id: 'valid-client',

View file

@ -3,6 +3,7 @@ import { OAuthRegisteredClientsStore } from '../clients.js';
import { OAuthClientInformationFull, OAuthClientMetadata } from '../../../shared/auth.js';
import express from 'express';
import supertest from 'supertest';
import { MockInstance } from 'vitest';
describe('Client Registration Handler', () => {
// Mock client store with registration support
@ -45,7 +46,7 @@ describe('Client Registration Handler', () => {
describe('Request handling', () => {
let app: express.Express;
let spyRegisterClient: jest.SpyInstance;
let spyRegisterClient: MockInstance;
beforeEach(() => {
// Setup express app with registration handler
@ -58,7 +59,7 @@ describe('Client Registration Handler', () => {
app.use('/register', clientRegistrationHandler(options));
// Spy on the registerClient method
spyRegisterClient = jest.spyOn(mockClientStoreWithRegistration, 'registerClient');
spyRegisterClient = vi.spyOn(mockClientStoreWithRegistration, 'registerClient');
});
afterEach(() => {

View file

@ -6,6 +6,7 @@ import express, { Response } from 'express';
import supertest from 'supertest';
import { AuthInfo } from '../types.js';
import { InvalidTokenError } from '../errors.js';
import { MockInstance } from 'vitest';
describe('Revocation Handler', () => {
// Mock client data
@ -130,7 +131,7 @@ describe('Revocation Handler', () => {
describe('Request handling', () => {
let app: express.Express;
let spyRevokeToken: jest.SpyInstance;
let spyRevokeToken: MockInstance;
beforeEach(() => {
// Setup express app with revocation handler
@ -139,7 +140,7 @@ describe('Revocation Handler', () => {
app.use('/revoke', revocationHandler(options));
// Spy on the revokeToken method
spyRevokeToken = jest.spyOn(mockProviderWithRevocation, 'revokeToken');
spyRevokeToken = vi.spyOn(mockProviderWithRevocation, 'revokeToken');
});
afterEach(() => {

View file

@ -8,10 +8,11 @@ import * as pkceChallenge from 'pkce-challenge';
import { InvalidGrantError, InvalidTokenError } from '../errors.js';
import { AuthInfo } from '../types.js';
import { ProxyOAuthServerProvider } from '../providers/proxyProvider.js';
import { type Mock } from 'vitest';
// Mock pkce-challenge
jest.mock('pkce-challenge', () => ({
verifyChallenge: jest.fn().mockImplementation(async (verifier, challenge) => {
vi.mock('pkce-challenge', () => ({
verifyChallenge: vi.fn().mockImplementation(async (verifier, challenge) => {
return verifier === 'valid_verifier' && challenge === 'mock_challenge';
})
}));
@ -111,7 +112,7 @@ describe('Token Handler', () => {
};
// Mock PKCE verification
(pkceChallenge.verifyChallenge as jest.Mock).mockImplementation(async (verifier: string, challenge: string) => {
(pkceChallenge.verifyChallenge as Mock).mockImplementation(async (verifier: string, challenge: string) => {
return verifier === 'valid_verifier' && challenge === 'mock_challenge';
});
@ -214,7 +215,7 @@ describe('Token Handler', () => {
it('verifies code_verifier against challenge', async () => {
// Setup invalid verifier
(pkceChallenge.verifyChallenge as jest.Mock).mockResolvedValueOnce(false);
(pkceChallenge.verifyChallenge as Mock).mockResolvedValueOnce(false);
const response = await supertest(app).post('/token').type('form').send({
client_id: 'valid-client',
@ -243,7 +244,7 @@ describe('Token Handler', () => {
});
it('returns tokens for valid code exchange', async () => {
const mockExchangeCode = jest.spyOn(mockProvider, 'exchangeAuthorizationCode');
const mockExchangeCode = vi.spyOn(mockProvider, 'exchangeAuthorizationCode');
const response = await supertest(app).post('/token').type('form').send({
client_id: 'valid-client',
client_secret: 'valid-secret',
@ -294,7 +295,7 @@ describe('Token Handler', () => {
const originalFetch = global.fetch;
try {
global.fetch = jest.fn().mockResolvedValue({
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockTokens)
});
@ -348,7 +349,7 @@ describe('Token Handler', () => {
const originalFetch = global.fetch;
try {
global.fetch = jest.fn().mockResolvedValue({
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockTokens)
});
@ -426,7 +427,7 @@ describe('Token Handler', () => {
});
it('returns new tokens for valid refresh token', async () => {
const mockExchangeRefresh = jest.spyOn(mockProvider, 'exchangeRefreshToken');
const mockExchangeRefresh = vi.spyOn(mockProvider, 'exchangeRefreshToken');
const response = await supertest(app).post('/token').type('form').send({
client_id: 'valid-client',
client_secret: 'valid-secret',

View file

@ -1,11 +1,12 @@
import { Request, Response } from 'express';
import { Mock } from 'vitest';
import { requireBearerAuth } from './bearerAuth.js';
import { AuthInfo } from '../types.js';
import { InsufficientScopeError, InvalidTokenError, CustomOAuthError, ServerError } from '../errors.js';
import { OAuthTokenVerifier } from '../provider.js';
// Mock verifier
const mockVerifyAccessToken = jest.fn();
const mockVerifyAccessToken = vi.fn();
const mockVerifier: OAuthTokenVerifier = {
verifyAccessToken: mockVerifyAccessToken
};
@ -13,23 +14,23 @@ const mockVerifier: OAuthTokenVerifier = {
describe('requireBearerAuth middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let nextFunction: jest.Mock;
let nextFunction: Mock;
beforeEach(() => {
mockRequest = {
headers: {}
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
set: jest.fn().mockReturnThis()
status: vi.fn().mockReturnThis(),
json: vi.fn(),
set: vi.fn().mockReturnThis()
};
nextFunction = jest.fn();
jest.spyOn(console, 'error').mockImplementation(() => {});
nextFunction = vi.fn();
vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should call next when token is valid', async () => {

View file

@ -5,6 +5,7 @@ import { OAuthClientInformationFull, OAuthTokens } from '../../../shared/auth.js
import { ServerError } from '../errors.js';
import { InvalidTokenError } from '../errors.js';
import { InsufficientScopeError } from '../errors.js';
import { type Mock } from 'vitest';
describe('Proxy OAuth Server Provider', () => {
// Mock client data
@ -16,12 +17,12 @@ describe('Proxy OAuth Server Provider', () => {
// Mock response object
const mockResponse = {
redirect: jest.fn()
redirect: vi.fn()
} as unknown as Response;
// Mock provider functions
const mockVerifyToken = jest.fn();
const mockGetClient = jest.fn();
const mockVerifyToken = vi.fn();
const mockGetClient = vi.fn();
// Base provider options
const baseOptions: ProxyOptions = {
@ -41,7 +42,7 @@ describe('Proxy OAuth Server Provider', () => {
beforeEach(() => {
provider = new ProxyOAuthServerProvider(baseOptions);
originalFetch = global.fetch;
global.fetch = jest.fn();
global.fetch = vi.fn();
// Setup mock implementations
mockVerifyToken.mockImplementation(async (token: string) => {
@ -66,7 +67,7 @@ describe('Proxy OAuth Server Provider', () => {
// Add helper function for failed responses
const mockFailedResponse = () => {
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as Mock).mockImplementation(() =>
Promise.resolve({
ok: false,
status: 400
@ -76,7 +77,7 @@ describe('Proxy OAuth Server Provider', () => {
afterEach(() => {
global.fetch = originalFetch;
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('authorization', () => {
@ -116,7 +117,7 @@ describe('Proxy OAuth Server Provider', () => {
};
beforeEach(() => {
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as Mock).mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockTokenResponse)
@ -182,7 +183,7 @@ describe('Proxy OAuth Server Provider', () => {
it('handles authorization code exchange without resource parameter', async () => {
const tokens = await provider.exchangeAuthorizationCode(validClient, 'test-code', 'test-verifier');
const fetchCall = (global.fetch as jest.Mock).mock.calls[0];
const fetchCall = (global.fetch as Mock).mock.calls[0];
const body = fetchCall[1].body as string;
expect(body).not.toContain('resource=');
expect(tokens).toEqual(mockTokenResponse);
@ -233,7 +234,7 @@ describe('Proxy OAuth Server Provider', () => {
redirect_uris: ['https://new-client.com/callback']
};
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as Mock).mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(newClient)
@ -268,7 +269,7 @@ describe('Proxy OAuth Server Provider', () => {
describe('token revocation', () => {
it('revokes token', async () => {
(global.fetch as jest.Mock).mockImplementation(() =>
(global.fetch as Mock).mockImplementation(() =>
Promise.resolve({
ok: true
})

View file

@ -279,11 +279,11 @@ describe('MCP Auth Router', () => {
issuerUrl: new URL('https://auth.example.com')
};
app.use(mcpAuthRouter(options));
jest.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('routes to authorization endpoint', async () => {
@ -301,8 +301,8 @@ describe('MCP Auth Router', () => {
it('routes to token endpoint', async () => {
// Setup verifyChallenge mock for token handler
jest.mock('pkce-challenge', () => ({
verifyChallenge: jest.fn().mockResolvedValue(true)
vi.mock('pkce-challenge', () => ({
verifyChallenge: vi.fn().mockResolvedValue(true)
}));
const response = await supertest(app).post('/token').type('form').send({

View file

@ -27,9 +27,9 @@ test('should accept latest protocol version', async () => {
});
const serverTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.id === 1 && message.result) {
expect(message.result).toEqual({
protocolVersion: LATEST_PROTOCOL_VERSION,
@ -90,9 +90,9 @@ test('should accept supported older protocol version', async () => {
});
const serverTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.id === 1 && message.result) {
expect(message.result).toEqual({
protocolVersion: OLD_VERSION,
@ -150,9 +150,9 @@ test('should handle unsupported protocol version', async () => {
});
const serverTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.id === 1 && message.result) {
expect(message.result).toEqual({
protocolVersion: LATEST_PROTOCOL_VERSION,
@ -864,7 +864,7 @@ test('should respect log level for transport without sessionId', async () => {
};
// Test the one that makes it through
clientTransport.onmessage = jest.fn().mockImplementation(message => {
clientTransport.onmessage = vi.fn().mockImplementation(message => {
expect(message).toEqual({
jsonrpc: '2.0',
method: 'notifications/message',
@ -933,7 +933,7 @@ test('should respect log level for transport with sessionId', async () => {
};
// Test the one that makes it through
clientTransport.onmessage = jest.fn().mockImplementation(message => {
clientTransport.onmessage = vi.fn().mockImplementation(message => {
expect(message).toEqual({
jsonrpc: '2.0',
method: 'notifications/message',

Some files were not shown because too many files have changed in this diff Show more