Skip to content

Commit be845dd

Browse files
authored
Merge pull request #391 from christian-bromann/cb/sse-tests
test(server): add more tests for `SSEServerTransport` class
2 parents 6e0b699 + f76652b commit be845dd

File tree

2 files changed

+157
-2
lines changed

2 files changed

+157
-2
lines changed

src/server/sse.test.ts

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ const createMockResponse = () => {
1818
return res as unknown as jest.Mocked<http.ServerResponse>;
1919
};
2020

21+
const createMockRequest = ({ headers = {}, body }: { headers?: Record<string, string>, body?: string } = {}) => {
22+
const mockReq = {
23+
headers,
24+
body: body ? body : undefined,
25+
auth: {
26+
token: 'test-token',
27+
},
28+
on: jest.fn<http.IncomingMessage['on']>().mockImplementation((event, listener) => {
29+
const mockListener = listener as unknown as (...args: unknown[]) => void;
30+
if (event === 'data') {
31+
mockListener(Buffer.from(body || '') as unknown as Error);
32+
}
33+
if (event === 'error') {
34+
mockListener(new Error('test'));
35+
}
36+
if (event === 'end') {
37+
mockListener();
38+
}
39+
if (event === 'close') {
40+
setTimeout(listener, 100);
41+
}
42+
return mockReq;
43+
}),
44+
listeners: jest.fn<http.IncomingMessage['listeners']>(),
45+
removeListener: jest.fn<http.IncomingMessage['removeListener']>(),
46+
} as unknown as http.IncomingMessage;
47+
48+
return mockReq;
49+
};
50+
2151
/**
2252
* Helper to create and start test HTTP server with MCP setup
2353
*/
@@ -298,4 +328,129 @@ describe('SSEServerTransport', () => {
298328
expect(mockRes.write).toHaveBeenCalledWith(`event: message\ndata: ${JSON.stringify(expectedMessage)}\n\n`);
299329
});
300330
});
301-
});
331+
332+
describe('handlePostMessage method', () => {
333+
it('should return 500 if server has not started', async () => {
334+
const mockReq = createMockRequest();
335+
const mockRes = createMockResponse();
336+
const endpoint = '/messages';
337+
const transport = new SSEServerTransport(endpoint, mockRes);
338+
339+
const error = 'SSE connection not established';
340+
await expect(transport.handlePostMessage(mockReq, mockRes))
341+
.rejects.toThrow(error);
342+
expect(mockRes.writeHead).toHaveBeenCalledWith(500);
343+
expect(mockRes.end).toHaveBeenCalledWith(error);
344+
});
345+
346+
it('should return 400 if content-type is not application/json', async () => {
347+
const mockReq = createMockRequest({ headers: { 'content-type': 'text/plain' } });
348+
const mockRes = createMockResponse();
349+
const endpoint = '/messages';
350+
const transport = new SSEServerTransport(endpoint, mockRes);
351+
await transport.start();
352+
353+
transport.onerror = jest.fn();
354+
const error = 'Unsupported content-type: text/plain';
355+
await expect(transport.handlePostMessage(mockReq, mockRes))
356+
.resolves.toBe(undefined);
357+
expect(mockRes.writeHead).toHaveBeenCalledWith(400);
358+
expect(mockRes.end).toHaveBeenCalledWith(expect.stringContaining(error));
359+
expect(transport.onerror).toHaveBeenCalledWith(new Error(error));
360+
});
361+
362+
it('should return 400 if message has not a valid schema', async () => {
363+
const invalidMessage = JSON.stringify({
364+
// missing jsonrpc field
365+
method: 'call',
366+
params: [1, 2, 3],
367+
id: 1,
368+
})
369+
const mockReq = createMockRequest({
370+
headers: { 'content-type': 'application/json' },
371+
body: invalidMessage,
372+
});
373+
const mockRes = createMockResponse();
374+
const endpoint = '/messages';
375+
const transport = new SSEServerTransport(endpoint, mockRes);
376+
await transport.start();
377+
378+
transport.onmessage = jest.fn();
379+
await transport.handlePostMessage(mockReq, mockRes);
380+
expect(mockRes.writeHead).toHaveBeenCalledWith(400);
381+
expect(transport.onmessage).not.toHaveBeenCalled();
382+
expect(mockRes.end).toHaveBeenCalledWith(`Invalid message: ${invalidMessage}`);
383+
});
384+
385+
it('should return 202 if message has a valid schema', async () => {
386+
const validMessage = JSON.stringify({
387+
jsonrpc: "2.0",
388+
method: 'call',
389+
params: {
390+
a: 1,
391+
b: 2,
392+
c: 3,
393+
},
394+
id: 1
395+
})
396+
const mockReq = createMockRequest({
397+
headers: { 'content-type': 'application/json' },
398+
body: validMessage,
399+
});
400+
const mockRes = createMockResponse();
401+
const endpoint = '/messages';
402+
const transport = new SSEServerTransport(endpoint, mockRes);
403+
await transport.start();
404+
405+
transport.onmessage = jest.fn();
406+
await transport.handlePostMessage(mockReq, mockRes);
407+
expect(mockRes.writeHead).toHaveBeenCalledWith(202);
408+
expect(mockRes.end).toHaveBeenCalledWith('Accepted');
409+
expect(transport.onmessage).toHaveBeenCalledWith({
410+
jsonrpc: "2.0",
411+
method: 'call',
412+
params: {
413+
a: 1,
414+
b: 2,
415+
c: 3,
416+
},
417+
id: 1
418+
}, {
419+
authInfo: {
420+
token: 'test-token',
421+
},
422+
requestInfo: {
423+
headers: {
424+
'content-type': 'application/json',
425+
},
426+
},
427+
});
428+
});
429+
});
430+
431+
describe('close method', () => {
432+
it('should call onclose', async () => {
433+
const mockRes = createMockResponse();
434+
const endpoint = '/messages';
435+
const transport = new SSEServerTransport(endpoint, mockRes);
436+
await transport.start();
437+
transport.onclose = jest.fn();
438+
await transport.close();
439+
expect(transport.onclose).toHaveBeenCalled();
440+
});
441+
});
442+
443+
describe('send method', () => {
444+
it('should call onsend', async () => {
445+
const mockRes = createMockResponse();
446+
const endpoint = '/messages';
447+
const transport = new SSEServerTransport(endpoint, mockRes);
448+
await transport.start();
449+
expect(mockRes.write).toHaveBeenCalledTimes(1);
450+
expect(mockRes.write).toHaveBeenCalledWith(
451+
expect.stringContaining('event: endpoint'));
452+
expect(mockRes.write).toHaveBeenCalledWith(
453+
expect.stringContaining(`data: /messages?sessionId=${transport.sessionId}`));
454+
});
455+
});
456+
});

src/server/sse.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class SSEServerTransport implements Transport {
9292
try {
9393
const ct = contentType.parse(req.headers["content-type"] ?? "");
9494
if (ct.type !== "application/json") {
95-
throw new Error(`Unsupported content-type: ${ct}`);
95+
throw new Error(`Unsupported content-type: ${ct.type}`);
9696
}
9797

9898
body = parsedBody ?? await getRawBody(req, {

0 commit comments

Comments
 (0)