import { describe, it, expect, mock, afterEach } from "bun:test"; import { executeGraphQL, X3RequestError } from "../src/graphql/client.js"; import type { X3Config } from "../src/config.js"; function makeConfig(overrides: Partial = {}): X3Config { return { url: "https://example.com/api", endpoint: "SEED", clientId: "test-client-id", secret: "test-secret-key-for-hs256", user: "TEST_USER", tokenLifetime: 600, mode: "authenticated", tlsRejectUnauthorized: true, ...overrides, }; } describe("executeGraphQL — mutation guard", () => { it("rejects queries starting with 'mutation'", () => { expect( executeGraphQL(makeConfig(), 'mutation { createItem(input: {}) { id } }'), ).rejects.toThrow("read-only"); }); it("rejects case-insensitive 'MUTATION'", () => { expect( executeGraphQL(makeConfig(), "MUTATION { deleteItem(id: 1) { ok } }"), ).rejects.toThrow("read-only"); }); it("rejects mutation with leading whitespace", () => { expect( executeGraphQL(makeConfig(), " mutation { updateItem { id } }"), ).rejects.toThrow("read-only"); }); it("rejects mixed-case 'Mutation'", () => { expect( executeGraphQL(makeConfig(), "Mutation { foo { bar } }"), ).rejects.toThrow("read-only"); }); it("throws an X3RequestError with statusCode 0", async () => { try { await executeGraphQL(makeConfig(), "mutation { foo }"); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(X3RequestError); expect((err as X3RequestError).statusCode).toBe(0); } }); }); describe("executeGraphQL — valid queries", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); it("does not reject a standard query", async () => { globalThis.fetch = mock(() => Promise.resolve( new Response(JSON.stringify({ data: { test: true } }), { status: 200, headers: { "Content-Type": "application/json" }, }), ), ) as typeof fetch; const result = await executeGraphQL(makeConfig(), "{ xtremX3MasterData { company { edges { node { id } } } } }"); expect(result.data).toEqual({ test: true }); }); it("does not reject a query with 'query' keyword", async () => { globalThis.fetch = mock(() => Promise.resolve( new Response(JSON.stringify({ data: { items: [] } }), { status: 200, headers: { "Content-Type": "application/json" }, }), ), ) as typeof fetch; const result = await executeGraphQL( makeConfig(), "query { xtremX3Stock { stockDetail { edges { node { id } } } } }", ); expect(result.data).toEqual({ items: [] }); }); }); describe("executeGraphQL — HTTP error handling", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); it("throws X3RequestError on 401", async () => { globalThis.fetch = mock(() => Promise.resolve(new Response("Unauthorized", { status: 401 })), ) as typeof fetch; try { await executeGraphQL(makeConfig(), "{ test }"); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(X3RequestError); expect((err as X3RequestError).statusCode).toBe(401); expect((err as X3RequestError).message).toContain("Authentication failed"); } }); it("throws X3RequestError on 500 with structured body", async () => { const errorBody = { $message: "Division by zero", $source: "X3RUNTIME", }; globalThis.fetch = mock(() => Promise.resolve( new Response(JSON.stringify(errorBody), { status: 500, headers: { "Content-Type": "application/json" }, }), ), ) as typeof fetch; try { await executeGraphQL(makeConfig(), "{ test }"); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(X3RequestError); expect((err as X3RequestError).statusCode).toBe(500); expect((err as X3RequestError).message).toContain("Division by zero"); expect((err as X3RequestError).message).toContain("X3RUNTIME"); } }); it("throws X3RequestError on 400 with GraphQL errors", async () => { const errorBody = { errors: [{ message: "Field 'foo' not found" }], }; globalThis.fetch = mock(() => Promise.resolve( new Response(JSON.stringify(errorBody), { status: 400, headers: { "Content-Type": "application/json" }, }), ), ) as typeof fetch; try { await executeGraphQL(makeConfig(), "{ foo }"); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(X3RequestError); expect((err as X3RequestError).statusCode).toBe(400); expect((err as X3RequestError).message).toContain("Field 'foo' not found"); } }); it("throws X3RequestError on network failure", async () => { globalThis.fetch = mock(() => Promise.reject(new Error("ECONNREFUSED")), ) as typeof fetch; try { await executeGraphQL(makeConfig(), "{ test }"); expect.unreachable("should have thrown"); } catch (err) { expect(err).toBeInstanceOf(X3RequestError); expect((err as X3RequestError).statusCode).toBe(0); expect((err as X3RequestError).message).toContain("Network error"); expect((err as X3RequestError).message).toContain("ECONNREFUSED"); } }); });