Relocate tests under /test (#1220)

This commit is contained in:
Konstantin Konstantinov 2025-12-06 01:56:12 +02:00 • committed by GitHub
commit 5a9de1c638
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 630 additions and 524 deletions

View file

@ -1,373 +0,0 @@
import { describe, it, expect } from 'vitest';
import { auth } from './auth.js';
import {
ClientCredentialsProvider,
PrivateKeyJwtProvider,
StaticPrivateKeyJwtProvider,
createPrivateKeyJwtAuth
} from './auth-extensions.js';
import type { FetchLike } from '../shared/transport.js';
const RESOURCE_SERVER_URL = 'https://resource.example.com/';
const AUTH_SERVER_URL = 'https://auth.example.com';
function createMockFetch(onTokenRequest?: (url: URL, init: RequestInit | undefined) => void | Promise<void>): FetchLike {
return async (input: string | URL, init?: RequestInit): Promise<Response> => {
const url = input instanceof URL ? input : new URL(input);
// Protected resource metadata discovery
if (url.origin === RESOURCE_SERVER_URL.slice(0, -1) && url.pathname === '/.well-known/oauth-protected-resource') {
return new Response(
JSON.stringify({
resource: RESOURCE_SERVER_URL,
authorization_servers: [AUTH_SERVER_URL]
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Authorization server metadata discovery
if (url.origin === AUTH_SERVER_URL && url.pathname === '/.well-known/oauth-authorization-server') {
return new Response(
JSON.stringify({
issuer: AUTH_SERVER_URL,
authorization_endpoint: `${AUTH_SERVER_URL}/authorize`,
token_endpoint: `${AUTH_SERVER_URL}/token`,
response_types_supported: ['code'],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'private_key_jwt']
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Token endpoint
if (url.origin === AUTH_SERVER_URL && url.pathname === '/token') {
if (onTokenRequest) {
await onTokenRequest(url, init);
}
return new Response(
JSON.stringify({
access_token: 'test-access-token',
token_type: 'Bearer'
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
}
throw new Error(`Unexpected URL in mock fetch: ${url.toString()}`);
};
}
describe('auth-extensions providers (end-to-end with auth())', () => {
it('authenticates using ClientCredentialsProvider with client_secret_basic', async () => {
const provider = new ClientCredentialsProvider({
clientId: 'my-client',
clientSecret: 'my-secret',
clientName: 'test-client'
});
const fetchMock = createMockFetch(async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
expect(params.get('client_assertion')).toBeNull();
const headers = new Headers(init?.headers);
const authHeader = headers.get('Authorization');
expect(authHeader).toBeTruthy();
const expectedCredentials = Buffer.from('my-client:my-secret').toString('base64');
expect(authHeader).toBe(`Basic ${expectedCredentials}`);
});
const result = await auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
});
expect(result).toBe('AUTHORIZED');
const tokens = provider.tokens();
expect(tokens).toBeTruthy();
expect(tokens?.access_token).toBe('test-access-token');
});
it('authenticates using PrivateKeyJwtProvider with private_key_jwt', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
algorithm: 'HS256',
clientName: 'private-key-jwt-client'
});
let assertionFromRequest: string | null = null;
const fetchMock = createMockFetch(async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
assertionFromRequest = params.get('client_assertion');
expect(assertionFromRequest).toBeTruthy();
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
const parts = assertionFromRequest!.split('.');
expect(parts).toHaveLength(3);
const headers = new Headers(init?.headers);
expect(headers.get('Authorization')).toBeNull();
});
const result = await auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
});
expect(result).toBe('AUTHORIZED');
const tokens = provider.tokens();
expect(tokens).toBeTruthy();
expect(tokens?.access_token).toBe('test-access-token');
expect(assertionFromRequest).toBeTruthy();
});
it('fails when PrivateKeyJwtProvider is configured with an unsupported algorithm', async () => {
const provider = new PrivateKeyJwtProvider({
clientId: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
algorithm: 'none',
clientName: 'private-key-jwt-client'
});
const fetchMock = createMockFetch();
await expect(
auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
})
).rejects.toThrow('Unsupported algorithm none');
});
it('authenticates using StaticPrivateKeyJwtProvider with static client assertion', async () => {
const staticAssertion = 'header.payload.signature';
const provider = new StaticPrivateKeyJwtProvider({
clientId: 'static-client',
jwtBearerAssertion: staticAssertion,
clientName: 'static-private-key-jwt-client'
});
const fetchMock = createMockFetch(async (_url, init) => {
const params = init?.body as URLSearchParams;
expect(params).toBeInstanceOf(URLSearchParams);
expect(params.get('grant_type')).toBe('client_credentials');
expect(params.get('resource')).toBe(RESOURCE_SERVER_URL);
expect(params.get('client_assertion')).toBe(staticAssertion);
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
const headers = new Headers(init?.headers);
expect(headers.get('Authorization')).toBeNull();
});
const result = await auth(provider, {
serverUrl: RESOURCE_SERVER_URL,
fetchFn: fetchMock
});
expect(result).toBe('AUTHORIZED');
const tokens = provider.tokens();
expect(tokens).toBeTruthy();
expect(tokens?.access_token).toBe('test-access-token');
});
});
describe('createPrivateKeyJwtAuth', () => {
const baseOptions = {
issuer: 'client-id',
subject: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
alg: 'HS256'
};
it('creates an addClientAuthentication function that sets JWT assertion params', async () => {
const addClientAuth = createPrivateKeyJwtAuth(baseOptions);
const headers = new Headers();
const params = new URLSearchParams();
await addClientAuth(headers, params, 'https://auth.example.com/token', undefined);
expect(params.get('client_assertion')).toBeTruthy();
expect(params.get('client_assertion_type')).toBe('urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
// Verify JWT structure (three dot-separated segments)
const assertion = params.get('client_assertion')!;
const parts = assertion.split('.');
expect(parts).toHaveLength(3);
});
it('throws when globalThis.crypto is not available', async () => {
// Temporarily remove globalThis.crypto to simulate older Node.js runtimes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const globalAny = globalThis as any;
const originalCrypto = globalAny.crypto;
// Use delete so that typeof globalThis.crypto === 'undefined'
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete globalAny.crypto;
try {
const addClientAuth = createPrivateKeyJwtAuth(baseOptions);
const params = new URLSearchParams();
await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
'crypto is not available, please ensure you add have Web Crypto API support for older Node.js versions'
);
} finally {
// Restore original crypto to avoid affecting other tests
globalAny.crypto = originalCrypto;
}
});
it('creates a signed JWT when using a Uint8Array HMAC key', async () => {
const secret = new TextEncoder().encode('a-string-secret-at-least-256-bits-long');
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: secret,
alg: 'HS256'
});
const params = new URLSearchParams();
await addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined);
const assertion = params.get('client_assertion')!;
const parts = assertion.split('.');
expect(parts).toHaveLength(3);
});
it('creates a signed JWT when using a symmetric JWK key', async () => {
const jwk: Record<string, unknown> = {
kty: 'oct',
// "a-string-secret-at-least-256-bits-long" base64url-encoded
k: 'YS1zdHJpbmctc2VjcmV0LWF0LWxlYXN0LTI1Ni1iaXRzLWxvbmc',
alg: 'HS256'
};
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: jwk,
alg: 'HS256'
});
const params = new URLSearchParams();
await addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined);
const assertion = params.get('client_assertion')!;
const parts = assertion.split('.');
expect(parts).toHaveLength(3);
});
it('creates a signed JWT when using an RSA PEM private key', async () => {
// Generate an RSA key pair on the fly
const jose = await import('jose');
const { privateKey } = await jose.generateKeyPair('RS256', { extractable: true });
const pem = await jose.exportPKCS8(privateKey);
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: pem,
alg: 'RS256'
});
const params = new URLSearchParams();
await addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined);
const assertion = params.get('client_assertion')!;
const parts = assertion.split('.');
expect(parts).toHaveLength(3);
});
it('uses metadata.issuer as audience when available', async () => {
const addClientAuth = createPrivateKeyJwtAuth(baseOptions);
const params = new URLSearchParams();
await addClientAuth(new Headers(), params, 'https://auth.example.com/token', {
issuer: 'https://issuer.example.com',
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
response_types_supported: ['code']
});
const assertion = params.get('client_assertion')!;
// Decode the payload to verify audience
const [, payloadB64] = assertion.split('.');
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString());
expect(payload.aud).toBe('https://issuer.example.com');
});
it('throws when using an unsupported algorithm', async () => {
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: 'a-string-secret-at-least-256-bits-long',
alg: 'none'
});
const params = new URLSearchParams();
await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
'Unsupported algorithm none'
);
});
it('throws when jose cannot import an invalid RSA PEM key', async () => {
const badPem = '-----BEGIN PRIVATE KEY-----\nnot-a-valid-key\n-----END PRIVATE KEY-----';
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: badPem,
alg: 'RS256'
});
const params = new URLSearchParams();
await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
/Invalid character/
);
});
it('throws when jose cannot import a mismatched JWK key', async () => {
const jwk: Record<string, unknown> = {
kty: 'oct',
k: 'c2VjcmV0LWtleQ', // "secret-key" base64url
alg: 'HS256'
};
const addClientAuth = createPrivateKeyJwtAuth({
issuer: 'client-id',
subject: 'client-id',
privateKey: jwk,
// Ask for an RSA algorithm with an octet key, which should cause jose.importJWK to fail
alg: 'RS256'
});
const params = new URLSearchParams();
await expect(addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)).rejects.toThrow(
/Key for the RS256 algorithm must be one of type CryptoKey, KeyObject, or JSON Web Key/
);
});
});

File diff suppressed because it is too large Load diff

View file

@ -1,153 +0,0 @@
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
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: Mock;
stdin?: { on: Mock; write: Mock };
stdout?: { on: Mock };
stderr?: null;
} = {
on: vi.fn((event: string, callback: () => void) => {
if (event === 'spawn') {
callback();
}
return mockProcess;
}),
stdin: {
on: vi.fn(),
write: vi.fn().mockReturnValue(true)
},
stdout: {
on: vi.fn()
},
stderr: null
};
return mockProcess as unknown as ChildProcess;
});
});
afterEach(() => {
vi.clearAllMocks();
});
test('should call cross-spawn correctly', async () => {
const transport = new StdioClientTransport({
command: 'test-command',
args: ['arg1', 'arg2']
});
await transport.start();
// verify spawn is called correctly
expect(mockSpawn).toHaveBeenCalledWith(
'test-command',
['arg1', 'arg2'],
expect.objectContaining({
shell: false
})
);
});
test('should pass environment variables correctly', async () => {
const customEnv = { TEST_VAR: 'test-value' };
const transport = new StdioClientTransport({
command: 'test-command',
env: customEnv
});
await transport.start();
// verify environment variables are merged correctly
expect(mockSpawn).toHaveBeenCalledWith(
'test-command',
[],
expect.objectContaining({
env: {
...getDefaultEnvironment(),
...customEnv
}
})
);
});
test('should use default environment when env is undefined', async () => {
const transport = new StdioClientTransport({
command: 'test-command',
env: undefined
});
await transport.start();
// verify default environment is used
expect(mockSpawn).toHaveBeenCalledWith(
'test-command',
[],
expect.objectContaining({
env: getDefaultEnvironment()
})
);
});
test('should send messages correctly', async () => {
const transport = new StdioClientTransport({
command: 'test-command'
});
// get the mock process object
const mockProcess: {
on: Mock;
stdin: {
on: Mock;
write: Mock;
once: Mock;
};
stdout: {
on: Mock;
};
stderr: null;
} = {
on: vi.fn((event: string, callback: () => void) => {
if (event === 'spawn') {
callback();
}
return mockProcess;
}),
stdin: {
on: vi.fn(),
write: vi.fn().mockReturnValue(true),
once: vi.fn()
},
stdout: {
on: vi.fn()
},
stderr: null
};
mockSpawn.mockReturnValue(mockProcess as unknown as ChildProcess);
await transport.start();
// 关键修复:确保 jsonrpc 是字面量 "2.0"
const message: JSONRPCMessage = {
jsonrpc: '2.0',
id: 'test-id',
method: 'test-method'
};
await transport.send(message);
// verify message is sent correctly
expect(mockProcess.stdin.write).toHaveBeenCalled();
});
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,77 +0,0 @@
import { JSONRPCMessage } from '../types.js';
import { StdioClientTransport, StdioServerParameters } from './stdio.js';
// Configure default server parameters based on OS
// Uses 'more' command for Windows and 'tee' command for Unix/Linux
const getDefaultServerParameters = (): StdioServerParameters => {
if (process.platform === 'win32') {
return { command: 'more' };
}
return { command: '/usr/bin/tee' };
};
const serverParameters = getDefaultServerParameters();
test('should start then close cleanly', async () => {
const client = new StdioClientTransport(serverParameters);
client.onerror = error => {
throw error;
};
let didClose = false;
client.onclose = () => {
didClose = true;
};
await client.start();
expect(didClose).toBeFalsy();
await client.close();
expect(didClose).toBeTruthy();
});
test('should read messages', async () => {
const client = new StdioClientTransport(serverParameters);
client.onerror = error => {
throw error;
};
const messages: JSONRPCMessage[] = [
{
jsonrpc: '2.0',
id: 1,
method: 'ping'
},
{
jsonrpc: '2.0',
method: 'notifications/initialized'
}
];
const readMessages: JSONRPCMessage[] = [];
const finished = new Promise<void>(resolve => {
client.onmessage = message => {
readMessages.push(message);
if (JSON.stringify(message) === JSON.stringify(messages[1])) {
resolve();
}
};
});
await client.start();
await client.send(messages[0]);
await client.send(messages[1]);
await finished;
expect(readMessages).toEqual(messages);
await client.close();
});
test('should return child process pid', async () => {
const client = new StdioClientTransport(serverParameters);
await client.start();
expect(client.pid).not.toBeNull();
await client.close();
expect(client.pid).toBeNull();
});

File diff suppressed because it is too large Load diff