Skip to main content
Glama
request-matcher.ts5.3 kB
import qs from 'qs'; import { expect } from 'vitest'; export interface ExpectedRequest { url?: string; query?: Record<string, string | string[]>; method?: string; headers?: Headers; body?: object; // JSON-serialisable } // Helper to parse the request body based on content type async function parseRequestBody( request: Request, ): Promise<{ parsed: unknown; contentType: string | null }> { const text = await request.clone().text(); // don't consume original let parsed: unknown; const contentType = request.headers.get('content-type'); if (contentType?.includes('application/x-www-form-urlencoded')) { parsed = qs.parse(text); } else { try { parsed = JSON.parse(text); } catch { parsed = text; } } return { parsed, contentType }; } // Checks if two values are equal (for primitives) function areEqual(a: unknown, b: unknown): boolean { if (a === b) return true; // Handle special cases like NaN if (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) return true; return false; } // Simple deep equality check function deepEqual(a: unknown, b: unknown): boolean { // Handle primitives if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') { return areEqual(a, b); } // Handle arrays if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i])) return false; } return true; } // Handle objects const keysA = Object.keys(a); const keysB = new Set(Object.keys(b)); // Check if all keys in a are in b with equal values for (const key of keysA) { if (!keysB.has(key)) return false; if (!deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])) return false; keysB.delete(key); } // Check if any keys in b are not in a return keysB.size === 0; } /* --- Register the matcher with Vitest --------------------------- */ expect.extend({ async toMatchRequest(this, received: Request, expected: ExpectedRequest) { // Validate URL if (expected.url !== undefined) { const expectedUrl = new URL(`https://example.com${expected.url}`); // Add query parameters to URL for (const [key, value] of Object.entries(expected.query ?? {})) { if (Array.isArray(value)) { for (const v of value) { expectedUrl.searchParams.append(key, v); } } else { expectedUrl.searchParams.set(key, value); } } const expectedUrlString = String(expectedUrl); if (received.url !== expectedUrlString) { return { pass: false, actual: received.url, expected: expectedUrlString, message: () => `URL mismatch`, }; } } // Validate method if (expected.method !== undefined) { const expectedMethod = expected.method.toLowerCase(); const receivedMethod = received.method.toLowerCase(); if (receivedMethod !== expectedMethod) { return { pass: false, actual: receivedMethod, expected: expectedMethod, message: () => `Method mismatch`, }; } } // Validate headers if (expected.headers !== undefined) { const actualHeaders = Object.fromEntries(received.headers); const expectedHeaders = Object.fromEntries( Object.entries(expected.headers).map(([k, v]) => [k.toLowerCase(), v as unknown]), ); for (const [key, expectedValue] of Object.entries(expectedHeaders)) { const actualValue = actualHeaders[key]; if (actualValue === undefined) { return { pass: false, actual: actualValue, expected: expectedValue, message: () => `Missing header: ${key}`, }; } else if (actualValue !== expectedValue) { return { pass: false, actual: actualValue, expected: expectedValue, message: () => `Header '${key}' mismatch`, }; } } } // Validate body if (expected.body !== undefined) { const { parsed, contentType } = await parseRequestBody(received); // Do a deep equality check first if (!deepEqual(parsed, expected.body)) { // This is the key part: we're NOT throwing an error with a message // Instead, we're returning the objects so Vitest can generate a diff return { pass: false, actual: parsed, expected: expected.body, message: () => `Request body (contentType: ${contentType}) did not match expected body`, }; } } // Everything passed return { pass: true, message: () => 'Request matched expected shape', }; }, }); /* --- Type declarations so TS recognises the matcher ------------- */ declare module 'vitest' { // make it awaitable to avoid dangling promises // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Assertion<T> { toMatchRequest(expected: ExpectedRequest): Promise<void>; } interface AsymmetricMatchersContaining { toMatchRequest(expected: ExpectedRequest): Promise<void>; } }

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/wycats/mcpify'

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