/** * Tests for GraphQLClientImpl. * * Validates: * - The proto request built by query()/mutation() matches the * graphql.v1.Plugin shape expected by the orchestrator. * - Optional per-request headers are forwarded as the proto's repeated * `headers` field (key/value Property entries) so dynamic values like * Authorization tokens can be sent from API code. * - Variables are serialized into the proto `custom.variables` Property. * - Trace metadata is passed through to the executeQuery callback. * - Response Zod validation throws RestApiValidationError on mismatch. */ import { describe, it, expect, vi } from "vitest"; import { z } from "zod"; import { RestApiValidationError } from "../../errors.js"; import type { IntegrationConfig } from "../types.js"; import { GraphQLClientImpl } from "./client.js"; const TEST_CONFIG: IntegrationConfig = { id: "graphql-test-id", name: "Test GraphQL", pluginId: "graphqlintegration", configuration: {}, }; function createClient(mockResult: unknown) { const executeQuery = vi.fn().mockResolvedValue(mockResult); const client = new GraphQLClientImpl(TEST_CONFIG, executeQuery); return { client, executeQuery }; } const UserResponseSchema = z.object({ data: z.object({ user: z.object({ id: z.string(), name: z.string(), }), }), }); const SAMPLE_QUERY = `query GetUser($id: ID!) { user(id: $id) { id name } }`; const SAMPLE_MUTATION = `mutation CreateUser($name: String!) { createUser(name: $name) { id name } }`; describe("GraphQLClientImpl", () => { describe("query()", () => { it("returns validated data on a successful response", async () => { const { client } = createClient({ data: { user: { id: "u1", name: "Alice" } }, }); const result = await client.query( SAMPLE_QUERY, { response: UserResponseSchema }, { id: "u1" }, ); expect(result.data.user).toEqual({ id: "u1", name: "Alice" }); }); it("builds the proto request with body, variables, and defaults", async () => { const { client, executeQuery } = createClient({ data: { user: { id: "u1", name: "Alice" } }, }); await client.query( SAMPLE_QUERY, { response: UserResponseSchema }, { id: "u1" }, ); expect(executeQuery).toHaveBeenCalledOnce(); const request = executeQuery.mock.calls[0][0]; expect(request.body).toBe(SAMPLE_QUERY); expect(request.verboseHttpOutput).toBe(false); expect(request.failOnGraphqlErrors).toBe(true); expect(request.custom).toEqual({ variables: { key: "variables", value: JSON.stringify({ id: "u1" }), }, }); expect(request.headers).toBeUndefined(); }); it("omits custom when no variables are provided", async () => { const { client, executeQuery } = createClient({ data: { user: { id: "u1", name: "Alice" } }, }); await client.query(SAMPLE_QUERY, { response: UserResponseSchema }); const request = executeQuery.mock.calls[0][0]; expect(request.custom).toBeUndefined(); }); it("forwards per-request headers into the proto headers field", async () => { const { client, executeQuery } = createClient({ data: { user: { id: "u1", name: "Alice" } }, }); await client.query( SAMPLE_QUERY, { response: UserResponseSchema }, { id: "u1" }, undefined, { Authorization: "Bearer abc123", "X-Trace-Id": "trace-1", }, ); const request = executeQuery.mock.calls[0][0]; expect(request.headers).toEqual([ { key: "Authorization", value: "Bearer abc123" }, { key: "X-Trace-Id", value: "trace-1" }, ]); }); it("does not set headers when an empty headers object is passed", async () => { const { client, executeQuery } = createClient({ data: { user: { id: "u1", name: "Alice" } }, }); await client.query( SAMPLE_QUERY, { response: UserResponseSchema }, undefined, undefined, {}, ); const request = executeQuery.mock.calls[0][0]; expect(request.headers).toBeUndefined(); }); it("passes trace metadata through to executeQuery", async () => { const { client, executeQuery } = createClient({ data: { user: { id: "u1", name: "Alice" } }, }); await client.query( SAMPLE_QUERY, { response: UserResponseSchema }, undefined, { label: "graphql.getUser", description: "Fetch a user by id" }, ); expect(executeQuery).toHaveBeenCalledWith(expect.any(Object), undefined, { label: "graphql.getUser", description: "Fetch a user by id", }); }); it("throws RestApiValidationError when the response fails schema validation", async () => { const { client } = createClient({ data: { user: { id: "u1" /* missing name */ } }, }); await expect( client.query(SAMPLE_QUERY, { response: UserResponseSchema }), ).rejects.toThrow(RestApiValidationError); }); }); describe("mutation()", () => { it("returns validated data on a successful response", async () => { const { client } = createClient({ data: { createUser: { id: "u2", name: "Bob" } }, }); const Schema = z.object({ data: z.object({ createUser: z.object({ id: z.string(), name: z.string() }), }), }); const result = await client.mutation( SAMPLE_MUTATION, { response: Schema }, { name: "Bob" }, ); expect(result.data.createUser).toEqual({ id: "u2", name: "Bob" }); }); it("forwards per-request headers into the proto headers field", async () => { const { client, executeQuery } = createClient({ data: { createUser: { id: "u2", name: "Bob" } }, }); const Schema = z.object({ data: z.object({ createUser: z.object({ id: z.string(), name: z.string() }), }), }); await client.mutation( SAMPLE_MUTATION, { response: Schema }, { name: "Bob" }, undefined, { Authorization: "Bearer xyz" }, ); const request = executeQuery.mock.calls[0][0]; expect(request.body).toBe(SAMPLE_MUTATION); expect(request.headers).toEqual([ { key: "Authorization", value: "Bearer xyz" }, ]); }); }); });