Relocate tests under /test (#1220)
This commit is contained in:
parent
7655eeb51c
commit
5a9de1c638
8 changed files with 630 additions and 524 deletions
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Reference in a new issue