Files
sage-graphql-mcp/tests/graphql-client.test.ts
2026-03-13 15:00:22 +00:00

180 lines
5.5 KiB
TypeScript

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> = {}): 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");
}
});
});