Skip to main content
Glama
server-http.test.ts18.8 kB
import { beforeEach, describe, expect, mock, test } from "bun:test"; import { ShortcutClient } from "@shortcut/client"; import type { Request, Response } from "express"; // Mock the ShortcutClient module const mockGetCurrentMemberInfo = mock(async () => ({ data: { id: "test-user", mention_name: "testuser", name: "Test User", email_address: "test@example.com", }, })); // We need to mock the module before importing the server components mock.module("@shortcut/client", () => { return { ShortcutClient: class { constructor(_token: string) {} getCurrentMemberInfo = mockGetCurrentMemberInfo; }, }; }); // Helper to create mock Express Request/Response objects interface MockRequest extends Partial<Request> { headers: Record<string, string | string[] | undefined>; body?: unknown; } interface MockResponse extends Partial<Response> { statusCode?: number; headers?: Record<string, string>; _data?: unknown; headersSent?: boolean; } function createMockRequest(overrides: Partial<MockRequest> = {}): MockRequest { return { headers: {}, body: {}, method: "POST", ...overrides, }; } function createMockResponse(): MockResponse { const res: MockResponse = { statusCode: 200, headers: {}, _data: undefined, headersSent: false, }; res.status = mock((code: number) => { res.statusCode = code; return res; }) as unknown as Response["status"]; res.json = mock((data: unknown) => { res._data = data; res.headersSent = true; return res as Response; }) as unknown as Response["json"]; res.send = mock((data: unknown) => { res._data = data; res.headersSent = true; return res as Response; }) as unknown as Response["send"]; res.header = mock((name: string, value: string) => { if (!res.headers) res.headers = {}; res.headers[name] = value; return res as Response; }) as unknown as Response["header"]; res.sendStatus = mock((code: number) => { res.statusCode = code; res.headersSent = true; return res as Response; }) as unknown as Response["sendStatus"]; return res; } describe("Server-HTTP Security Tests", () => { describe("extractApiToken", () => { test("should extract token from Authorization Bearer header", () => { const extractApiToken = (req: Request): string | null => { const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { return authHeader.slice("Bearer ".length); } const customHeader = req.headers["x-shortcut-api-token"]; if (typeof customHeader === "string") { return customHeader; } return null; }; const req = createMockRequest({ headers: { authorization: "Bearer test-token-123" }, }) as Request; const token = extractApiToken(req); expect(token).toBe("test-token-123"); }); test("should extract token from X-Shortcut-API-Token header", () => { const extractApiToken = (req: Request): string | null => { const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { return authHeader.slice("Bearer ".length); } const customHeader = req.headers["x-shortcut-api-token"]; if (typeof customHeader === "string") { return customHeader; } return null; }; const req = createMockRequest({ headers: { "x-shortcut-api-token": "test-token-456" }, }) as Request; const token = extractApiToken(req); expect(token).toBe("test-token-456"); }); test("should return null when no token provided", () => { const extractApiToken = (req: Request): string | null => { const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { return authHeader.slice("Bearer ".length); } const customHeader = req.headers["x-shortcut-api-token"]; if (typeof customHeader === "string") { return customHeader; } return null; }; const req = createMockRequest() as Request; const token = extractApiToken(req); expect(token).toBeNull(); }); test("should prioritize Authorization header over custom header", () => { const extractApiToken = (req: Request): string | null => { const authHeader = req.headers.authorization; if (authHeader?.startsWith("Bearer ")) { return authHeader.slice("Bearer ".length); } const customHeader = req.headers["x-shortcut-api-token"]; if (typeof customHeader === "string") { return customHeader; } return null; }; const req = createMockRequest({ headers: { authorization: "Bearer priority-token", "x-shortcut-api-token": "fallback-token", }, }) as Request; const token = extractApiToken(req); expect(token).toBe("priority-token"); }); }); describe("validateApiToken", () => { beforeEach(() => { mockGetCurrentMemberInfo.mockClear(); }); test("should return true for valid token", async () => { mockGetCurrentMemberInfo.mockResolvedValueOnce({ data: { id: "user-1", mention_name: "testuser", name: "Test User", email_address: "testuser@example.com", }, }); const validateApiToken = async (token: string): Promise<boolean> => { try { const client = new ShortcutClient(token); await client.getCurrentMemberInfo(); return true; } catch { return false; } }; const result = await validateApiToken("valid-token"); expect(result).toBe(true); expect(mockGetCurrentMemberInfo).toHaveBeenCalledTimes(1); }); test("should return false for invalid token", async () => { mockGetCurrentMemberInfo.mockRejectedValueOnce(new Error("Unauthorized")); const validateApiToken = async (token: string): Promise<boolean> => { try { const client = new ShortcutClient(token); await client.getCurrentMemberInfo(); return true; } catch { return false; } }; const result = await validateApiToken("invalid-token"); expect(result).toBe(false); expect(mockGetCurrentMemberInfo).toHaveBeenCalledTimes(1); }); }); describe("SessionManager", () => { // Simplified SessionManager for testing interface SessionData { apiToken: string; createdAt: Date; lastAccessedAt: Date; } class TestSessionManager { private sessions: Map<string, SessionData> = new Map(); has(sessionId: string): boolean { return this.sessions.has(sessionId); } get(sessionId: string): SessionData | undefined { const session = this.sessions.get(sessionId); if (session) { session.lastAccessedAt = new Date(); } return session; } add(sessionId: string, apiToken: string): void { this.sessions.set(sessionId, { apiToken, createdAt: new Date(), lastAccessedAt: new Date(), }); } remove(sessionId: string): void { this.sessions.delete(sessionId); } validateToken(sessionId: string, providedToken: string): boolean { const session = this.sessions.get(sessionId); if (!session) { return false; } return session.apiToken === providedToken; } getStaleSessions(timeoutMs: number): string[] { const now = Date.now(); const staleSessionIds: string[] = []; for (const [sessionId, session] of this.sessions.entries()) { const timeSinceLastAccess = now - session.lastAccessedAt.getTime(); if (timeSinceLastAccess > timeoutMs) { staleSessionIds.push(sessionId); } } return staleSessionIds; } } test("should add and retrieve sessions", () => { const manager = new TestSessionManager(); const sessionId = "session-123"; const apiToken = "token-abc"; manager.add(sessionId, apiToken); expect(manager.has(sessionId)).toBe(true); const session = manager.get(sessionId); expect(session).toBeDefined(); expect(session?.apiToken).toBe(apiToken); }); test("should validate tokens correctly", () => { const manager = new TestSessionManager(); const sessionId = "session-123"; const apiToken = "correct-token"; manager.add(sessionId, apiToken); expect(manager.validateToken(sessionId, "correct-token")).toBe(true); expect(manager.validateToken(sessionId, "wrong-token")).toBe(false); expect(manager.validateToken("non-existent", "correct-token")).toBe(false); }); test("should remove sessions", () => { const manager = new TestSessionManager(); const sessionId = "session-123"; manager.add(sessionId, "token"); expect(manager.has(sessionId)).toBe(true); manager.remove(sessionId); expect(manager.has(sessionId)).toBe(false); }); test("should identify stale sessions", async () => { const manager = new TestSessionManager(); const timeoutMs = 100; // 100ms timeout for testing // Add a session manager.add("session-1", "token-1"); // Wait for it to become stale await new Promise((resolve) => setTimeout(resolve, 150)); // Add a fresh session manager.add("session-2", "token-2"); const staleSessions = manager.getStaleSessions(timeoutMs); expect(staleSessions).toContain("session-1"); expect(staleSessions).not.toContain("session-2"); }); test("should update lastAccessedAt on get", async () => { const manager = new TestSessionManager(); manager.add("session-1", "token"); const session1 = manager.get("session-1"); const firstAccessTime = session1?.lastAccessedAt.getTime(); await new Promise((resolve) => setTimeout(resolve, 50)); const session2 = manager.get("session-1"); const secondAccessTime = session2?.lastAccessedAt.getTime(); expect(secondAccessTime).toBeGreaterThan(firstAccessTime!); }); }); describe("Error Response Helpers", () => { test("should send unauthorized error", () => { const res = createMockResponse(); const sendUnauthorizedError = (res: Response): void => { res.status(401).json({ jsonrpc: "2.0", error: { code: -32000, message: "Unauthorized", }, id: null, }); }; sendUnauthorizedError(res as Response); expect(res.statusCode).toBe(401); expect(res._data).toMatchObject({ jsonrpc: "2.0", error: { code: -32000, message: "Unauthorized", }, }); }); test("should send invalid token error", () => { const res = createMockResponse(); const sendInvalidTokenError = (res: Response, requestId?: unknown): void => { res.status(401).json({ jsonrpc: "2.0", error: { code: -32002, message: "Invalid API token", }, id: requestId || null, }); }; sendInvalidTokenError(res as Response, "req-123"); expect(res.statusCode).toBe(401); expect(res._data).toMatchObject({ jsonrpc: "2.0", error: { code: -32002, message: "Invalid API token", }, id: "req-123", }); }); test("should send session not found error", () => { const res = createMockResponse(); const sendSessionNotFoundError = ( res: Response, _sessionId: string, requestId?: unknown, ): void => { res.status(404).json({ jsonrpc: "2.0", error: { code: -32001, message: "Session not found", }, id: requestId || null, }); }; sendSessionNotFoundError(res as Response, "session-123", "req-456"); expect(res.statusCode).toBe(404); expect(res._data).toMatchObject({ jsonrpc: "2.0", error: { code: -32001, message: "Session not found", }, id: "req-456", }); }); test("should send bad request error", () => { const res = createMockResponse(); const sendBadRequestError = (res: Response, message: string, requestId?: unknown): void => { res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message, }, id: requestId || null, }); }; sendBadRequestError(res as Response, "Missing parameter", "req-789"); expect(res.statusCode).toBe(400); expect(res._data).toMatchObject({ jsonrpc: "2.0", error: { code: -32000, message: "Missing parameter", }, id: "req-789", }); }); test("should not send internal error if headers already sent", () => { const res = createMockResponse(); res.headersSent = true; const sendInternalError = (res: Response): void => { if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error", }, id: null, }); } }; sendInternalError(res as Response); // Should not call status or json because headers are already sent expect(res.statusCode).toBe(200); // Still the default }); }); describe("Request Handler Scenarios", () => { interface SessionData { apiToken: string; createdAt: Date; lastAccessedAt: Date; } class TestSessionManager { private sessions: Map<string, SessionData> = new Map(); has(sessionId: string): boolean { return this.sessions.has(sessionId); } get(sessionId: string): SessionData | undefined { return this.sessions.get(sessionId); } add(sessionId: string, apiToken: string): void { this.sessions.set(sessionId, { apiToken, createdAt: new Date(), lastAccessedAt: new Date(), }); } validateToken(sessionId: string, providedToken: string): boolean { const session = this.sessions.get(sessionId); if (!session) return false; return session.apiToken === providedToken; } } test("POST - should reject request without API token", () => { const res = createMockResponse(); const extractApiToken = (): string | null => null; const apiToken = extractApiToken(); if (!apiToken) { (res.status as any)(401).json({ jsonrpc: "2.0", error: { code: -32000, message: "Unauthorized" }, id: null, }); } expect(res.statusCode).toBe(401); expect((res._data as any).error.message).toBe("Unauthorized"); }); test("POST - should reject existing session with wrong token", () => { const manager = new TestSessionManager(); manager.add("session-123", "correct-token"); const res = createMockResponse(); const sessionId = "session-123"; const apiToken = "wrong-token"; if (sessionId && manager.has(sessionId)) { if (!manager.validateToken(sessionId, apiToken)) { (res.status as any)(401).json({ jsonrpc: "2.0", error: { code: -32000, message: "Token mismatch" }, id: null, }); } } expect(res.statusCode).toBe(401); }); test("GET - should reject SSE request without token", () => { const manager = new TestSessionManager(); manager.add("session-123", "correct-token"); const res = createMockResponse(); const sessionId = "session-123"; const apiToken = null; if (!sessionId || !manager.has(sessionId)) { (res.status as any)(400).send("Invalid or missing session ID"); } else if (!apiToken) { (res.status as any)(401).json({ jsonrpc: "2.0", error: { code: -32000, message: "Unauthorized" }, id: null, }); } expect(res.statusCode).toBe(401); }); test("DELETE - should validate token before terminating session", () => { const manager = new TestSessionManager(); manager.add("session-123", "correct-token"); const res = createMockResponse(); const sessionId = "session-123"; const apiToken = "wrong-token"; if (!sessionId || !manager.has(sessionId)) { (res.status as any)(400).send("Invalid or missing session ID"); } else if (!apiToken) { (res.status as any)(401).json({ jsonrpc: "2.0", error: { code: -32000, message: "Unauthorized" }, id: null, }); } else if (!manager.validateToken(sessionId, apiToken)) { (res.status as any)(401).json({ jsonrpc: "2.0", error: { code: -32000, message: "Token mismatch" }, id: null, }); } expect(res.statusCode).toBe(401); }); }); describe("CORS Middleware", () => { test("should set correct CORS headers", () => { const mockReq = createMockRequest({ method: "POST", }); const res = createMockResponse(); let nextCalled = false; const corsMiddleware = (req: Request, res: Response, next: () => void): void => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header( "Access-Control-Allow-Headers", "Content-Type, Authorization, X-Shortcut-API-Token, Mcp-Session-Id, Last-Event-Id", ); res.header("Access-Control-Expose-Headers", "Mcp-Session-Id"); if (req.method === "OPTIONS") { res.sendStatus(204); return; } next(); }; corsMiddleware(mockReq as Request, res as Response, () => { nextCalled = true; }); expect(res.headers).toMatchObject({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS", "Access-Control-Expose-Headers": "Mcp-Session-Id", }); expect(nextCalled).toBe(true); }); test("should handle OPTIONS preflight request", () => { const mockReq = createMockRequest({ method: "OPTIONS", }); const res = createMockResponse(); let nextCalled = false; const corsMiddleware = (req: Request, res: Response, next: () => void): void => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); res.header( "Access-Control-Allow-Headers", "Content-Type, Authorization, X-Shortcut-API-Token, Mcp-Session-Id, Last-Event-Id", ); res.header("Access-Control-Expose-Headers", "Mcp-Session-Id"); if (req.method === "OPTIONS") { res.sendStatus(204); return; } next(); }; corsMiddleware(mockReq as Request, res as Response, () => { nextCalled = true; }); expect(res.statusCode).toBe(204); expect(nextCalled).toBe(false); }); }); describe("Configuration", () => { test("should parse tools list correctly", () => { const parseToolsList = (toolsStr: string): string[] => { return toolsStr .split(",") .map((tool) => tool.trim()) .filter(Boolean); }; expect(parseToolsList("story,epic,iteration")).toEqual(["story", "epic", "iteration"]); expect(parseToolsList("story, epic, iteration")).toEqual(["story", "epic", "iteration"]); expect(parseToolsList("")).toEqual([]); expect(parseToolsList("story,,epic")).toEqual(["story", "epic"]); }); test("should load config with defaults", () => { const DEFAULT_PORT = 9292; const SESSION_TIMEOUT_MS = 30 * 60 * 1000; const config = { port: DEFAULT_PORT, isReadonly: true, enabledTools: [], sessionTimeoutMs: SESSION_TIMEOUT_MS, }; expect(config.port).toBe(9292); expect(config.isReadonly).toBe(true); expect(config.sessionTimeoutMs).toBe(1800000); // 30 minutes }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/useshortcut/mcp-server-shortcut'

If you have feedback or need assistance with the MCP directory API, please join our Discord server