From 8ea66e7df9e0f081212ad4fd37d1e6cce49c6a06 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 11 Nov 2024 22:05:13 +0000 Subject: [PATCH] Tests for protocol version negotiation Resolves #44. --- src/client/index.test.ts | 125 ++++++++++++++++++++++++++++- src/server/index.test.ts | 166 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 285 insertions(+), 6 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index d93ca392..a6dcc732 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -3,11 +3,130 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Client } from "./index.js"; import { z } from "zod"; -import { RequestSchema, NotificationSchema, ResultSchema } from "../types.js"; +import { + RequestSchema, + NotificationSchema, + ResultSchema, + LATEST_PROTOCOL_VERSION, + SUPPORTED_PROTOCOL_VERSIONS, +} from "../types.js"; +import { Transport } from "../shared/transport.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) => { + if (message.method === "initialize") { + clientTransport.onmessage?.({ + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: {}, + serverInfo: { + name: "test", + version: "1.0", + }, + }, + }); + } + return Promise.resolve(); + }), + }; + + const client = new Client({ + name: "test client", + version: "1.0", + }); + + await client.connect(clientTransport); + + // Should have sent initialize with latest version + expect(clientTransport.send).toHaveBeenCalledWith( + expect.objectContaining({ + method: "initialize", + params: expect.objectContaining({ + protocolVersion: LATEST_PROTOCOL_VERSION, + }), + }), + ); +}); + +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) => { + if (message.method === "initialize") { + clientTransport.onmessage?.({ + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: OLD_VERSION, + capabilities: {}, + serverInfo: { + name: "test", + version: "1.0", + }, + }, + }); + } + return Promise.resolve(); + }), + }; + + const client = new Client({ + name: "test client", + version: "1.0", + }); + + await client.connect(clientTransport); + + // Connection should succeed with the older version + expect(client.getServerVersion()).toEqual({ + name: "test", + version: "1.0", + }); +}); + +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) => { + if (message.method === "initialize") { + clientTransport.onmessage?.({ + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: "invalid-version", + capabilities: {}, + serverInfo: { + name: "test", + version: "1.0", + }, + }, + }); + } + return Promise.resolve(); + }), + }; + + const client = new Client({ + name: "test client", + version: "1.0", + }); + + await expect(client.connect(clientTransport)).rejects.toThrow( + "Server's protocol version is not supported: invalid-version", + ); +}); /* -Test that custom request/notification/result schemas can be used with the Client class. -*/ + Test that custom request/notification/result schemas can be used with the Client class. + */ test("should typecheck", () => { const GetWeatherRequestSchema = RequestSchema.extend({ method: z.literal("weather/get"), diff --git a/src/server/index.test.ts b/src/server/index.test.ts index be33c58f..5045e2a7 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -3,11 +3,171 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Server } from "./index.js"; import { z } from "zod"; -import { RequestSchema, NotificationSchema, ResultSchema } from "../types.js"; +import { + RequestSchema, + NotificationSchema, + ResultSchema, + LATEST_PROTOCOL_VERSION, + SUPPORTED_PROTOCOL_VERSIONS, + InitializeRequestSchema, + InitializeResultSchema, +} from "../types.js"; +import { Transport } from "../shared/transport.js"; + +test("should accept latest protocol version", async () => { + let sendPromiseResolve: (value: unknown) => void; + const sendPromise = new Promise((resolve) => { + sendPromiseResolve = resolve; + }); + + const serverTransport: Transport = { + start: jest.fn().mockResolvedValue(undefined), + close: jest.fn().mockResolvedValue(undefined), + send: jest.fn().mockImplementation((message) => { + if (message.id === 1 && message.result) { + expect(message.result).toEqual({ + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: expect.any(Object), + serverInfo: { + name: "test server", + version: "1.0", + }, + }); + sendPromiseResolve(undefined); + } + return Promise.resolve(); + }), + }; + + const server = new Server({ + name: "test server", + version: "1.0", + }); + + await server.connect(serverTransport); + + // Simulate initialize request with latest version + serverTransport.onmessage?.({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { + name: "test client", + version: "1.0", + }, + }, + }); + + await expect(sendPromise).resolves.toBeUndefined(); +}); + +test("should accept supported older protocol version", async () => { + const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; + let sendPromiseResolve: (value: unknown) => void; + const sendPromise = new Promise((resolve) => { + sendPromiseResolve = resolve; + }); + + const serverTransport: Transport = { + start: jest.fn().mockResolvedValue(undefined), + close: jest.fn().mockResolvedValue(undefined), + send: jest.fn().mockImplementation((message) => { + if (message.id === 1 && message.result) { + expect(message.result).toEqual({ + protocolVersion: OLD_VERSION, + capabilities: expect.any(Object), + serverInfo: { + name: "test server", + version: "1.0", + }, + }); + sendPromiseResolve(undefined); + } + return Promise.resolve(); + }), + }; + + const server = new Server({ + name: "test server", + version: "1.0", + }); + + await server.connect(serverTransport); + + // Simulate initialize request with older version + serverTransport.onmessage?.({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: OLD_VERSION, + capabilities: {}, + clientInfo: { + name: "test client", + version: "1.0", + }, + }, + }); + + await expect(sendPromise).resolves.toBeUndefined(); +}); + +test("should handle unsupported protocol version", async () => { + let sendPromiseResolve: (value: unknown) => void; + const sendPromise = new Promise((resolve) => { + sendPromiseResolve = resolve; + }); + + const serverTransport: Transport = { + start: jest.fn().mockResolvedValue(undefined), + close: jest.fn().mockResolvedValue(undefined), + send: jest.fn().mockImplementation((message) => { + if (message.id === 1 && message.result) { + expect(message.result).toEqual({ + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: expect.any(Object), + serverInfo: { + name: "test server", + version: "1.0", + }, + }); + sendPromiseResolve(undefined); + } + return Promise.resolve(); + }), + }; + + const server = new Server({ + name: "test server", + version: "1.0", + }); + + await server.connect(serverTransport); + + // Simulate initialize request with unsupported version + serverTransport.onmessage?.({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "invalid-version", + capabilities: {}, + clientInfo: { + name: "test client", + version: "1.0", + }, + }, + }); + + await expect(sendPromise).resolves.toBeUndefined(); +}); /* -Test that custom request/notification/result schemas can be used with the Server class. -*/ + Test that custom request/notification/result schemas can be used with the Server class. + */ test("should typecheck", () => { const GetWeatherRequestSchema = RequestSchema.extend({ method: z.literal("weather/get"),