feat: everything
This commit is contained in:
179
tests/graphql-client.test.ts
Normal file
179
tests/graphql-client.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user