Files
schpet__linear-cli/test/utils/mock_linear_server.ts
2026-04-02 11:49:05 -07:00

187 lines
4.8 KiB
TypeScript

/**
* Mock Linear API server for testing
*
* Usage:
* const server = new MockLinearServer([
* {
* queryName: "GetIssueDetails",
* variables: { id: "TEST-123" },
* response: { data: { issue: { title: "Test Issue", ... } } }
* }
* ]);
*/
interface MockResponse {
queryName: string
queryIncludes?: string
variables?: Record<string, unknown>
response: Record<string, unknown>
status?: number
}
export class MockLinearServer {
private server?: Deno.HttpServer
private port = 0
private mockResponses: MockResponse[]
constructor(responses: MockResponse[] = []) {
this.mockResponses = responses
}
async start(): Promise<void> {
this.server = Deno.serve({
hostname: "127.0.0.1",
port: this.port,
onListen: () => {},
}, (request) => {
// Handle CORS preflight
if (request.method === "OPTIONS") {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
})
}
// Handle GraphQL requests
if (
request.method === "POST" &&
new URL(request.url).pathname === "/graphql"
) {
return this.handleGraphQL(request)
}
return new Response("Not Found", { status: 404 })
})
if ("port" in this.server.addr) {
this.port = this.server.addr.port
}
// Wait a bit for server to start
await new Promise((resolve) => setTimeout(resolve, 100))
}
async stop(): Promise<void> {
if (this.server) {
await this.server.shutdown()
this.server = undefined
}
}
getEndpoint(): string {
return `http://localhost:${this.port}/graphql`
}
private async handleGraphQL(request: Request): Promise<Response> {
const headers = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
// Use fixed date header for deterministic snapshot tests
"Date": "Mon, 01 Jan 2024 00:00:00 GMT",
}
try {
const body = await request.json()
const { query, variables } = body
// Find matching mock response
const mockResponse = this.findMatchingResponse(query, variables)
if (mockResponse) {
return new Response(
JSON.stringify(mockResponse.response),
{ status: mockResponse.status ?? 200, headers },
)
}
// Default response for unhandled queries
return new Response(
JSON.stringify({
errors: [{
message: "No mock response configured for this query",
extensions: {
code: "NO_MOCK_CONFIGURED",
query: this.extractQueryName(query),
variables,
},
}],
}),
{ status: 200, headers },
)
} catch (_error) {
return new Response(
JSON.stringify({
errors: [{
message: "Invalid JSON in request body",
extensions: { code: "BAD_REQUEST" },
}],
}),
{ status: 400, headers },
)
}
}
private findMatchingResponse(
query: string,
variables: Record<string, unknown> = {},
): MockResponse | undefined {
const queryName = this.extractQueryName(query)
return this.mockResponses.find((mock) => {
// Check if query name matches
if (mock.queryName !== queryName) {
return false
}
if (mock.queryIncludes != null && !query.includes(mock.queryIncludes)) {
return false
}
// If no variables specified in mock, match any variables
if (!mock.variables) {
return true
}
// Check if all mock variables match the request variables (deep comparison)
return Object.entries(mock.variables).every(([key, value]) => {
return this.deepEqual(variables[key], value)
})
})
}
private deepEqual(a: unknown, b: unknown): boolean {
if (a === b) return true
if (a == null || b == null) return a === b
if (typeof a !== typeof b) return false
if (typeof a !== "object") return a === b
const aObj = a as Record<string, unknown>
const bObj = b as Record<string, unknown>
const aKeys = Object.keys(aObj)
const bKeys = Object.keys(bObj)
if (aKeys.length !== bKeys.length) return false
return aKeys.every((key) => this.deepEqual(aObj[key], bObj[key]))
}
private extractQueryName(query: string): string {
// Extract query name from GraphQL query string
// Examples: "query GetIssueDetails" -> "GetIssueDetails"
const match = query.match(/(?:query|mutation)\s+(\w+)/)
return match?.[1] || "UnknownQuery"
}
addResponse(response: MockResponse): void {
this.mockResponses.push(response)
}
clearResponses(): void {
this.mockResponses = []
}
}