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:
parent
324d471158
commit
98df118a4c
30 changed files with 1713 additions and 3434 deletions
|
|
@ -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
4249
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 === '/') {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue