Merge pull request #391 from christian-bromann/cb/sse-tests

test(server): add more tests for `SSEServerTransport` class
This commit is contained in:
Inna Harper 2025-06-26 18:28:53 +01:00 • committed by GitHub
commit be845dd4ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 157 additions and 2 deletions

View file

@ -18,6 +18,36 @@ const createMockResponse = () => {
return res as unknown as jest.Mocked<http.ServerResponse>;
};
const createMockRequest = ({ headers = {}, body }: { headers?: Record<string, string>, body?: string } = {}) => {
const mockReq = {
headers,
body: body ? body : undefined,
auth: {
token: 'test-token',
},
on: jest.fn<http.IncomingMessage['on']>().mockImplementation((event, listener) => {
const mockListener = listener as unknown as (...args: unknown[]) => void;
if (event === 'data') {
mockListener(Buffer.from(body || '') as unknown as Error);
}
if (event === 'error') {
mockListener(new Error('test'));
}
if (event === 'end') {
mockListener();
}
if (event === 'close') {
setTimeout(listener, 100);
}
return mockReq;
}),
listeners: jest.fn<http.IncomingMessage['listeners']>(),
removeListener: jest.fn<http.IncomingMessage['removeListener']>(),
} as unknown as http.IncomingMessage;
return mockReq;
};
/**
* Helper to create and start test HTTP server with MCP setup
*/
@ -298,4 +328,129 @@ describe('SSEServerTransport', () => {
expect(mockRes.write).toHaveBeenCalledWith(`event: message\ndata: ${JSON.stringify(expectedMessage)}\n\n`);
});
});
});
describe('handlePostMessage method', () => {
it('should return 500 if server has not started', async () => {
const mockReq = createMockRequest();
const mockRes = createMockResponse();
const endpoint = '/messages';
const transport = new SSEServerTransport(endpoint, mockRes);
const error = 'SSE connection not established';
await expect(transport.handlePostMessage(mockReq, mockRes))
.rejects.toThrow(error);
expect(mockRes.writeHead).toHaveBeenCalledWith(500);
expect(mockRes.end).toHaveBeenCalledWith(error);
});
it('should return 400 if content-type is not application/json', async () => {
const mockReq = createMockRequest({ headers: { 'content-type': 'text/plain' } });
const mockRes = createMockResponse();
const endpoint = '/messages';
const transport = new SSEServerTransport(endpoint, mockRes);
await transport.start();
transport.onerror = jest.fn();
const error = 'Unsupported content-type: text/plain';
await expect(transport.handlePostMessage(mockReq, mockRes))
.resolves.toBe(undefined);
expect(mockRes.writeHead).toHaveBeenCalledWith(400);
expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining(error));
expect(transport.onerror).toHaveBeenCalledWith(new Error(error));
});
it('should return 400 if message has not a valid schema', async () => {
const invalidMessage = JSON.stringify({
// missing jsonrpc field
method: 'call',
params: [1, 2, 3],
id: 1,
})
const mockReq = createMockRequest({
headers: { 'content-type': 'application/json' },
body: invalidMessage,
});
const mockRes = createMockResponse();
const endpoint = '/messages';
const transport = new SSEServerTransport(endpoint, mockRes);
await transport.start();
transport.onmessage = jest.fn();
await transport.handlePostMessage(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(400);
expect(transport.onmessage).not.toHaveBeenCalled();
expect(mockRes.end).toHaveBeenCalledWith(`Invalid message: ${invalidMessage}`);
});
it('should return 202 if message has a valid schema', async () => {
const validMessage = JSON.stringify({
jsonrpc: "2.0",
method: 'call',
params: {
a: 1,
b: 2,
c: 3,
},
id: 1
})
const mockReq = createMockRequest({
headers: { 'content-type': 'application/json' },
body: validMessage,
});
const mockRes = createMockResponse();
const endpoint = '/messages';
const transport = new SSEServerTransport(endpoint, mockRes);
await transport.start();
transport.onmessage = jest.fn();
await transport.handlePostMessage(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(202);
expect(mockRes.end).toHaveBeenCalledWith('Accepted');
expect(transport.onmessage).toHaveBeenCalledWith({
jsonrpc: "2.0",
method: 'call',
params: {
a: 1,
b: 2,
c: 3,
},
id: 1
}, {
authInfo: {
token: 'test-token',
},
requestInfo: {
headers: {
'content-type': 'application/json',
},
},
});
});
});
describe('close method', () => {
it('should call onclose', async () => {
const mockRes = createMockResponse();
const endpoint = '/messages';
const transport = new SSEServerTransport(endpoint, mockRes);
await transport.start();
transport.onclose = jest.fn();
await transport.close();
expect(transport.onclose).toHaveBeenCalled();
});
});
describe('send method', () => {
it('should call onsend', async () => {
const mockRes = createMockResponse();
const endpoint = '/messages';
const transport = new SSEServerTransport(endpoint, mockRes);
await transport.start();
expect(mockRes.write).toHaveBeenCalledTimes(1);
expect(mockRes.write).toHaveBeenCalledWith(
expect.stringContaining('event: endpoint'));
expect(mockRes.write).toHaveBeenCalledWith(
expect.stringContaining(`data: /messages?sessionId=${transport.sessionId}`));
});
});
});

View file

@ -92,7 +92,7 @@ export class SSEServerTransport implements Transport {
try {
const ct = contentType.parse(req.headers["content-type"] ?? "");
if (ct.type !== "application/json") {
throw new Error(`Unsupported content-type: ${ct}`);
throw new Error(`Unsupported content-type: ${ct.type}`);
}
body = parsedBody ?? await getRawBody(req, {