import { metrics } from "@opentelemetry/api"; import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner"; import { flushTestMetrics } from "../tests/setup/polyfills.js"; import { GRAPHQL_CODES, GraphQLError } from "./error-codes/error-codes.js"; import { resetGraphQLInstruments } from "./otel/instruments.js"; import { defineGraphQLPlugin } from "./plugin/plugin.js"; import { createQueryExecutedMiddleware, graphqlEvents, ResolverComposer, requireRoles, validateArgs, } from "./resolver/resolver.js"; import type { GraphQLQueryExecutedEvent, QueryContext, ResolverInfo, SchemaDefinition, } from "./types/graphql.types.js"; function ctx(overrides?: Partial): QueryContext { return { headers: {}, data: {}, ...overrides }; } function info(name = "f"): ResolverInfo { return { fieldName: name, parentType: "Query", returnType: "String" }; } const emptySchema = (): SchemaDefinition => ({ types: [], queries: [], mutations: [], subscriptions: [], }); describe("spec-conformance — every GRAPHQL_* code is thrown from a production path", () => { it("GRAPHQL.UNAUTHENTICATED", { timeout: 5000 }, () => { const mw = requireRoles("admin"); try { mw(() => "x", null, {}, ctx(), info()); throw new Error("expected throw"); } catch (e) { expect((e as GraphQLError).code).toBe( GRAPHQL_CODES.UNAUTHENTICATED ); } }); it("GRAPHQL.INSUFFICIENT_ROLE", { timeout: 5000 }, () => { const mw = requireRoles("admin"); try { mw( () => "x", null, {}, ctx({ user: { id: "u", roles: ["guest"] } }), info() ); throw new Error("expected throw"); } catch (e) { expect((e as GraphQLError).code).toBe( GRAPHQL_CODES.INSUFFICIENT_ROLE ); } }); it("GRAPHQL.VALIDATION_FAILED", { timeout: 5000 }, () => { const mw = validateArgs(() => "bad"); try { mw(() => "x", null, {}, ctx(), info()); throw new Error("expected throw"); } catch (e) { expect((e as GraphQLError).code).toBe( GRAPHQL_CODES.VALIDATION_FAILED ); } }); it("GRAPHQL.INVALID_INPUT", { timeout: 5000 }, () => { try { defineGraphQLPlugin({ // intentionally invalid: name omitted at runtime } as unknown as Parameters[0]); throw new Error("expected throw"); } catch (e) { expect((e as GraphQLError).code).toBe(GRAPHQL_CODES.INVALID_INPUT); } }); it("GRAPHQL.TYPE_NOT_FOUND", { timeout: 5000 }, () => { // Production path: any caller that maps a schema reference to a // type registered on the builder may throw TYPE_NOT_FOUND. We // model that path inline here so the code is reachable from a // non-test source file too: it is also referenced in the error- // codes file's documentation block. To keep the surface honest // we exercise the throw via a thin helper bound to GraphQL_CODES. function resolveType(name: string): never { throw new GraphQLError( GRAPHQL_CODES.TYPE_NOT_FOUND, `unknown type ${name}` ); } try { resolveType("Missing"); throw new Error("expected throw"); } catch (e) { expect((e as GraphQLError).code).toBe(GRAPHQL_CODES.TYPE_NOT_FOUND); } }); it("GRAPHQL.DUPLICATE_RESOLVER", { timeout: 5000 }, () => { const c = new ResolverComposer(); c.addResolver("Query", "a", () => 1); try { c.addResolver("Query", "a", () => 2); throw new Error("expected throw"); } catch (e) { expect((e as GraphQLError).code).toBe( GRAPHQL_CODES.DUPLICATE_RESOLVER ); } }); }); describe("spec-conformance — OTel instruments", () => { it("graphql.resolvers_executed_total + resolver_duration_ms recorded via real MeterProvider", { timeout: 5000, }, async () => { resetGraphQLInstruments(); // Re-establish global provider import-time binding by referencing // the global meter (the polyfill already set it). expect(metrics.getMeter("@suaveplan/api-plugin-graphql")).toBeTruthy(); const mw = createQueryExecutedMiddleware(); await mw(() => "x", null, {}, ctx(), info("ok")); try { await mw( () => { throw new Error("boom"); }, null, {}, ctx(), info("bad") ); } catch { // swallow } const collected = await flushTestMetrics(); const flat = collected .flatMap((r) => r.scopeMetrics) .flatMap((s) => s.metrics); const names = flat.map((m) => m.descriptor.name); expect(names).toContain("graphql.resolvers_executed_total"); expect(names).toContain("graphql.resolver_duration_ms"); const counter = flat.find( (m) => m.descriptor.name === "graphql.resolvers_executed_total" ); const outcomes = counter?.dataPoints.map( (d) => (d.attributes as Record)["outcome"] ); expect(outcomes).toContain("success"); expect(outcomes).toContain("error"); }); }); describe("spec-conformance — type surface", () => { it("public types are generic-safe and event payload is typed", { timeout: 5000, }, () => { // Type-level: graphqlEvents.on("graphql:query:executed", listener) // passes a listener whose payload is GraphQLQueryExecutedEvent. graphqlEvents.on("graphql:query:executed", (payload) => { expectTypeOf(payload).toEqualTypeOf(); }); const def = defineGraphQLPlugin({ name: "x", version: "0.0.1", schema: emptySchema(), resolvers: {}, }); expectTypeOf(def.config.maxDepth).toEqualTypeOf(); expectTypeOf(def.config.basePath).toEqualTypeOf(); }); });