From ee748a0a8c643a3c5318ace413e2994e4bd946f8 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:31:42 +0200 Subject: [PATCH 01/35] v0 gateway-testing --- packages/gateway-testing/package.json | 55 ++++++++ .../src/createGatewayTester.ts | 117 ++++++++++++++++++ packages/gateway-testing/src/index.ts | 0 packages/runtime/src/types.ts | 2 +- yarn.lock | 15 +++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 packages/gateway-testing/package.json create mode 100644 packages/gateway-testing/src/createGatewayTester.ts create mode 100644 packages/gateway-testing/src/index.ts diff --git a/packages/gateway-testing/package.json b/packages/gateway-testing/package.json new file mode 100644 index 000000000..8d9f81a22 --- /dev/null +++ b/packages/gateway-testing/package.json @@ -0,0 +1,55 @@ +{ + "name": "@graphql-hive/gateway-testing", + "version": "0.0.0", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/graphql-hive/gateway.git", + "directory": "packages/gateway-testing" + }, + "author": { + "email": "contact@the-guild.dev", + "name": "The Guild", + "url": "https://the-guild.dev" + }, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "pkgroll --clean-dist", + "prepack": "yarn build" + }, + "peerDependencies": { + "@graphql-hive/gateway-runtime": "workspace:^", + "graphql": "^15.9.0 || ^16.9.0" + }, + "dependencies": { + "@graphql-mesh/fusion-composition": "^0.8.17", + "@graphql-tools/utils": "^10.9.1", + "graphql-yoga": "^5.16.0" + }, + "devDependencies": { + "graphql": "^16.9.0", + "pkgroll": "2.20.1" + }, + "sideEffects": false +} diff --git a/packages/gateway-testing/src/createGatewayTester.ts b/packages/gateway-testing/src/createGatewayTester.ts new file mode 100644 index 000000000..ae2e26cf8 --- /dev/null +++ b/packages/gateway-testing/src/createGatewayTester.ts @@ -0,0 +1,117 @@ +import { + createGatewayRuntime, + GatewayConfigSchemaBase, + GatewayRuntime, + UnifiedGraphConfig, + useCustomFetch, +} from '@graphql-hive/gateway-runtime'; +import { + getUnifiedGraphGracefully, + type SubgraphConfig, +} from '@graphql-mesh/fusion-composition'; +import { buildHTTPExecutor } from '@graphql-tools/executor-http'; +import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; +import { parse } from 'graphql'; +import { createYoga, type YogaServerInstance } from 'graphql-yoga'; + +export type GatewayTesterConfig< + TContext extends Record = Record, +> = GatewayConfigSchemaBase & + ( + | { + // gateway + supergraph: UnifiedGraphConfig; + } + | { + // gateway (composes subgraphs) + subgraphs: SubgraphConfig[]; + } + ); +// TODO: proxy mode +// TODO: subgraph mode + +export interface GatewayTester< + TContext extends Record = Record, +> { + runtime: GatewayRuntime; + fetch: typeof fetch; + execute(args: { + query: string; + variables?: Record; + operationName?: string; + extensions?: Record; + headers?: Record; + }): Promise>>; +} + +export function createGatewayTester< + TContext extends Record = Record, +>(config: GatewayTesterConfig): GatewayTester { + // + let runtime: GatewayRuntime; + if ('supergraph' in config) { + runtime = createGatewayRuntime(config); + } else { + // compose subgraphs and create runtime + const subgraphs = config.subgraphs.reduce( + (acc, subgraph) => ({ + ...acc, + [subgraph.name]: { + ...subgraph, + url: subgraph.url || `http://${subgraph.name}/graphql`, + yoga: createYoga({ + schema: subgraph.schema, + // TODO: toggle if necessary for testing + logging: false, + }), + }, + }), + {} as Record< + string, + SubgraphConfig & { yoga: YogaServerInstance } + >, + ); + runtime = createGatewayRuntime({ + ...config, + supergraph: getUnifiedGraphGracefully(Object.values(subgraphs)), + plugins: (ctx) => [ + useCustomFetch((url, options, context, info) => { + const subgraph = subgraphs[context.subgraphName]; + if (!subgraph) { + throw new Error( + `Subgraph with name "${context.subgraphName}" not found`, + ); + } + return subgraph.yoga.fetch( + // @ts-expect-error TODO: url can be a string, not only an instance of URL + url, + options, + context, + info, + ); + }), + ...(config.plugins?.(ctx) || []), + ], + }); + } + + const runtimeExecute = buildHTTPExecutor({ + fetch: runtime.fetch, + headers: (execReq) => execReq?.rootValue.headers, + }); + + return { + runtime, + execute(args) { + return runtimeExecute({ + document: parse(args.query), + variables: args.variables, + operationName: args.operationName, + extensions: args.extensions, + rootValue: { headers: args.headers }, + }); + }, + // @ts-expect-error native and whatwg-node fetch has conflicts + fetch: runtime.fetch, + }; +} diff --git a/packages/gateway-testing/src/index.ts b/packages/gateway-testing/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 4642499ce..7b4c16785 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -246,7 +246,7 @@ export interface GatewayConfigSubgraph< subgraph: UnifiedGraphConfig; } -interface GatewayConfigSchemaBase> +export interface GatewayConfigSchemaBase> extends GatewayConfigBase { /** * Additional GraphQL schema type definitions. diff --git a/yarn.lock b/yarn.lock index 7b41d38af..ece55d7ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4102,6 +4102,21 @@ __metadata: languageName: unknown linkType: soft +"@graphql-hive/gateway-testing@workspace:packages/gateway-testing": + version: 0.0.0-use.local + resolution: "@graphql-hive/gateway-testing@workspace:packages/gateway-testing" + dependencies: + "@graphql-mesh/fusion-composition": "npm:^0.8.17" + "@graphql-tools/utils": "npm:^10.9.1" + graphql: "npm:^16.9.0" + graphql-yoga: "npm:^5.16.0" + pkgroll: "npm:2.20.1" + peerDependencies: + "@graphql-hive/gateway-runtime": "workspace:^" + graphql: ^15.9.0 || ^16.9.0 + languageName: unknown + linkType: soft + "@graphql-hive/gateway@workspace:*, @graphql-hive/gateway@workspace:^, @graphql-hive/gateway@workspace:packages/gateway": version: 0.0.0-use.local resolution: "@graphql-hive/gateway@workspace:packages/gateway" From 825f7dadeceb1e2ff3a0b2907063534ca2b77d3b Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:35:59 +0200 Subject: [PATCH 02/35] provide maybe yoga --- .../src/createGatewayTester.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/gateway-testing/src/createGatewayTester.ts b/packages/gateway-testing/src/createGatewayTester.ts index ae2e26cf8..9766acf90 100644 --- a/packages/gateway-testing/src/createGatewayTester.ts +++ b/packages/gateway-testing/src/createGatewayTester.ts @@ -14,6 +14,11 @@ import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; import { parse } from 'graphql'; import { createYoga, type YogaServerInstance } from 'graphql-yoga'; +export interface GatewayTesterSubgraphConfig extends SubgraphConfig { + /** An optional GraphQL Yoga server instance that runs the provided schema. */ + yoga?: YogaServerInstance; +} + export type GatewayTesterConfig< TContext extends Record = Record, > = GatewayConfigSchemaBase & @@ -24,7 +29,7 @@ export type GatewayTesterConfig< } | { // gateway (composes subgraphs) - subgraphs: SubgraphConfig[]; + subgraphs: GatewayTesterSubgraphConfig[]; } ); // TODO: proxy mode @@ -59,17 +64,17 @@ export function createGatewayTester< [subgraph.name]: { ...subgraph, url: subgraph.url || `http://${subgraph.name}/graphql`, - yoga: createYoga({ - schema: subgraph.schema, - // TODO: toggle if necessary for testing - logging: false, - }), + yoga: + subgraph.yoga || + createYoga({ + schema: subgraph.schema, + maskedErrors: false, + // TODO: toggle if necessary for testing + logging: false, + }), }, }), - {} as Record< - string, - SubgraphConfig & { yoga: YogaServerInstance } - >, + {} as Record, ); runtime = createGatewayRuntime({ ...config, @@ -82,7 +87,7 @@ export function createGatewayTester< `Subgraph with name "${context.subgraphName}" not found`, ); } - return subgraph.yoga.fetch( + return subgraph.yoga!.fetch( // @ts-expect-error TODO: url can be a string, not only an instance of URL url, options, From 8f1f2a94063f9eff922bbad68e5a8f47ea033621 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:37:01 +0200 Subject: [PATCH 03/35] rename to simply testing --- packages/{gateway-testing => testing}/package.json | 0 .../{gateway-testing => testing}/src/createGatewayTester.ts | 0 packages/{gateway-testing => testing}/src/index.ts | 0 yarn.lock | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) rename packages/{gateway-testing => testing}/package.json (100%) rename packages/{gateway-testing => testing}/src/createGatewayTester.ts (100%) rename packages/{gateway-testing => testing}/src/index.ts (100%) diff --git a/packages/gateway-testing/package.json b/packages/testing/package.json similarity index 100% rename from packages/gateway-testing/package.json rename to packages/testing/package.json diff --git a/packages/gateway-testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts similarity index 100% rename from packages/gateway-testing/src/createGatewayTester.ts rename to packages/testing/src/createGatewayTester.ts diff --git a/packages/gateway-testing/src/index.ts b/packages/testing/src/index.ts similarity index 100% rename from packages/gateway-testing/src/index.ts rename to packages/testing/src/index.ts diff --git a/yarn.lock b/yarn.lock index ece55d7ad..c96886dd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4102,9 +4102,9 @@ __metadata: languageName: unknown linkType: soft -"@graphql-hive/gateway-testing@workspace:packages/gateway-testing": +"@graphql-hive/gateway-testing@workspace:packages/testing": version: 0.0.0-use.local - resolution: "@graphql-hive/gateway-testing@workspace:packages/gateway-testing" + resolution: "@graphql-hive/gateway-testing@workspace:packages/testing" dependencies: "@graphql-mesh/fusion-composition": "npm:^0.8.17" "@graphql-tools/utils": "npm:^10.9.1" From 3fe1550e5d6da5372582cd19666cb1a1b6356ce2 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:38:54 +0200 Subject: [PATCH 04/35] export necessary --- packages/testing/src/index.ts | 1 + tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index e69de29bb..117f9fa8f 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -0,0 +1 @@ +export * from './createGatewayTester'; diff --git a/tsconfig.json b/tsconfig.json index c8a611862..5628a701b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,7 @@ "./packages/gateway/src/opentelemetry/setup.ts" ], "@graphql-hive/gateway-runtime": ["./packages/runtime/src/index.ts"], + "@graphql-hive/gateway-testing": ["./packages/testing/src/index.ts"], "@graphql-mesh/fusion-runtime": [ "./packages/fusion-runtime/src/index.ts" ], From c36d6b3eec8cb0b3eea0cb8872ac8f808335064f Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:39:03 +0200 Subject: [PATCH 05/35] try out --- .../prometheus/tests/prometheus.spec.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/plugins/prometheus/tests/prometheus.spec.ts b/packages/plugins/prometheus/tests/prometheus.spec.ts index c4d6f6971..0d92dbaa4 100644 --- a/packages/plugins/prometheus/tests/prometheus.spec.ts +++ b/packages/plugins/prometheus/tests/prometheus.spec.ts @@ -1,12 +1,10 @@ -import { - createGatewayRuntime, - useCustomFetch, -} from '@graphql-hive/gateway-runtime'; +import { createGatewayRuntime } from '@graphql-hive/gateway-runtime'; +import { createGatewayTester } from '@graphql-hive/gateway-testing'; import InMemoryLRUCache from '@graphql-mesh/cache-inmemory-lru'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { createDefaultExecutor } from '@graphql-mesh/transport-common'; import { isDebug } from '@internal/testing'; -import { createSchema, createYoga } from 'graphql-yoga'; +import { createSchema } from 'graphql-yoga'; import { Registry, register as registry } from 'prom-client'; import { beforeEach, describe, expect, it } from 'vitest'; import usePrometheus, { @@ -281,23 +279,19 @@ describe('Prometheus', () => { it('should not increment fetch count on cached responses', async () => { const registry = new Registry(); - const subgraph = createYoga({ schema: subgraphSchema }); await using cache = new InMemoryLRUCache(); - const gateway = createGatewayRuntime({ - supergraph: getUnifiedGraphGracefully([ + const gateway = createGatewayTester({ + subgraphs: [ { name: 'TEST_SUBGRAPH', schema: subgraphSchema, - url: 'http://subgraph/graphql', }, - ]), + ], cache, responseCaching: { session: () => null, }, plugins: (ctx) => [ - // @ts-expect-error - useCustomFetch(subgraph.fetch), usePrometheus({ ...ctx, registry, From 5de48c443b22e5a0508c5c0c1b7a3867397aeff1 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:41:08 +0200 Subject: [PATCH 06/35] use subgraph url --- packages/testing/src/createGatewayTester.ts | 39 +++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 9766acf90..dc02bbbca 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -59,21 +59,24 @@ export function createGatewayTester< } else { // compose subgraphs and create runtime const subgraphs = config.subgraphs.reduce( - (acc, subgraph) => ({ - ...acc, - [subgraph.name]: { - ...subgraph, - url: subgraph.url || `http://${subgraph.name}/graphql`, - yoga: - subgraph.yoga || - createYoga({ - schema: subgraph.schema, - maskedErrors: false, - // TODO: toggle if necessary for testing - logging: false, - }), - }, - }), + (acc, subgraph) => { + const url = subgraph.url || `http://${subgraph.name}/graphql`; + return { + ...acc, + [url]: { + ...subgraph, + url, + yoga: + subgraph.yoga || + createYoga({ + schema: subgraph.schema, + maskedErrors: false, + // TODO: toggle if necessary for testing + logging: false, + }), + }, + }; + }, {} as Record, ); runtime = createGatewayRuntime({ @@ -81,11 +84,9 @@ export function createGatewayTester< supergraph: getUnifiedGraphGracefully(Object.values(subgraphs)), plugins: (ctx) => [ useCustomFetch((url, options, context, info) => { - const subgraph = subgraphs[context.subgraphName]; + const subgraph = subgraphs[url]; if (!subgraph) { - throw new Error( - `Subgraph with name "${context.subgraphName}" not found`, - ); + throw new Error(`Subgraph for URL "${url}" not found`); } return subgraph.yoga!.fetch( // @ts-expect-error TODO: url can be a string, not only an instance of URL From ac2c4bb8dc975bed14d1f2c2cb52f8d0344d24af Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:42:15 +0200 Subject: [PATCH 07/35] changeset --- .changeset/dry-numbers-deliver.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dry-numbers-deliver.md diff --git a/.changeset/dry-numbers-deliver.md b/.changeset/dry-numbers-deliver.md new file mode 100644 index 000000000..535c3fa31 --- /dev/null +++ b/.changeset/dry-numbers-deliver.md @@ -0,0 +1,5 @@ +--- +'@graphql-hive/gateway-testing': major +--- + +Hive Gateway tester and testing utilities From d560b9a6b5d5ebf415492573ba3b45f78c2be323 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:43:53 +0200 Subject: [PATCH 08/35] really test --- packages/plugins/prometheus/tests/prometheus.spec.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/plugins/prometheus/tests/prometheus.spec.ts b/packages/plugins/prometheus/tests/prometheus.spec.ts index 0d92dbaa4..e933b5860 100644 --- a/packages/plugins/prometheus/tests/prometheus.spec.ts +++ b/packages/plugins/prometheus/tests/prometheus.spec.ts @@ -305,20 +305,15 @@ describe('Prometheus', () => { }); for (let i = 0; i < 3; i++) { - const res = await gateway.fetch('http://gateway/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: /* GraphQL */ ` { hello } `, }), - }); - await expect(res.json()).resolves.toEqual({ + ).resolves.toEqual({ data: { hello: 'Hello world!' }, }); } From 0959980a16e339758d7680de3be071fe6fcef6ac Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 14:55:05 +0200 Subject: [PATCH 09/35] easier build schema and better yoga provider --- packages/testing/package.json | 1 + packages/testing/src/createGatewayTester.ts | 49 +++++++++++++-------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/testing/package.json b/packages/testing/package.json index 8d9f81a22..f26b639cf 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "@graphql-mesh/fusion-composition": "^0.8.17", + "@graphql-tools/schema": "^10.0.25", "@graphql-tools/utils": "^10.9.1", "graphql-yoga": "^5.16.0" }, diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index dc02bbbca..8d0512e54 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -5,18 +5,23 @@ import { UnifiedGraphConfig, useCustomFetch, } from '@graphql-hive/gateway-runtime'; -import { - getUnifiedGraphGracefully, - type SubgraphConfig, -} from '@graphql-mesh/fusion-composition'; +import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; +import { + makeExecutableSchema, + type IExecutableSchemaDefinition, +} from '@graphql-tools/schema'; import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; -import { parse } from 'graphql'; +import { parse, type GraphQLSchema } from 'graphql'; import { createYoga, type YogaServerInstance } from 'graphql-yoga'; -export interface GatewayTesterSubgraphConfig extends SubgraphConfig { - /** An optional GraphQL Yoga server instance that runs the provided schema. */ - yoga?: YogaServerInstance; +export interface GatewayTesterSubgraphConfig { + /** The name of the subgraph. */ + name: string; + /** The subgraph schema. */ + schema: GraphQLSchema | IExecutableSchemaDefinition; + /** An optional GraphQL Yoga server instance that runs the {@link schema built subgraph}. */ + yoga?: (schema: GraphQLSchema) => YogaServerInstance; } export type GatewayTesterConfig< @@ -60,24 +65,32 @@ export function createGatewayTester< // compose subgraphs and create runtime const subgraphs = config.subgraphs.reduce( (acc, subgraph) => { - const url = subgraph.url || `http://${subgraph.name}/graphql`; + const url = `http://${subgraph.name}/graphql`; + const schema = + 'typeDefs' in subgraph.schema + ? makeExecutableSchema(subgraph.schema) + : subgraph.schema; return { ...acc, [url]: { - ...subgraph, + name: subgraph.name, url, + schema, yoga: - subgraph.yoga || - createYoga({ - schema: subgraph.schema, - maskedErrors: false, - // TODO: toggle if necessary for testing - logging: false, - }), + subgraph.yoga?.(schema) || + createYoga({ schema, maskedErrors: false, logging: false }), }, }; }, - {} as Record, + {} as Record< + string, + { + name: string; + url: string; + schema: GraphQLSchema; + yoga: YogaServerInstance; + } + >, ); runtime = createGatewayRuntime({ ...config, From 2d4355fd69ecdeb7b87cf0fc5930030b51172acb Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 15:02:38 +0200 Subject: [PATCH 10/35] async disposable --- packages/testing/src/createGatewayTester.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 8d0512e54..9c1832554 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -13,7 +13,11 @@ import { } from '@graphql-tools/schema'; import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; import { parse, type GraphQLSchema } from 'graphql'; -import { createYoga, type YogaServerInstance } from 'graphql-yoga'; +import { + createYoga, + DisposableSymbols, + type YogaServerInstance, +} from 'graphql-yoga'; export interface GatewayTesterSubgraphConfig { /** The name of the subgraph. */ @@ -42,7 +46,7 @@ export type GatewayTesterConfig< export interface GatewayTester< TContext extends Record = Record, -> { +> extends AsyncDisposable { runtime: GatewayRuntime; fetch: typeof fetch; execute(args: { @@ -121,6 +125,8 @@ export function createGatewayTester< return { runtime, + // @ts-expect-error native and whatwg-node fetch has conflicts + fetch: runtime.fetch, execute(args) { return runtimeExecute({ document: parse(args.query), @@ -130,7 +136,8 @@ export function createGatewayTester< rootValue: { headers: args.headers }, }); }, - // @ts-expect-error native and whatwg-node fetch has conflicts - fetch: runtime.fetch, + [DisposableSymbols.asyncDispose]() { + return runtime[DisposableSymbols.asyncDispose](); + }, }; } From 9a72245d94cda210ea84167fad303fd859fdafd5 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 15:03:38 +0200 Subject: [PATCH 11/35] update lockfile --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index c96886dd2..b3020c620 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4107,6 +4107,7 @@ __metadata: resolution: "@graphql-hive/gateway-testing@workspace:packages/testing" dependencies: "@graphql-mesh/fusion-composition": "npm:^0.8.17" + "@graphql-tools/schema": "npm:^10.0.25" "@graphql-tools/utils": "npm:^10.9.1" graphql: "npm:^16.9.0" graphql-yoga: "npm:^5.16.0" From a0bfd37189083c5b2aae9257e63b806949f3db64 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 15:14:31 +0200 Subject: [PATCH 12/35] dispose --- packages/testing/src/createGatewayTester.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 9c1832554..ac2282ac8 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -56,6 +56,7 @@ export interface GatewayTester< extensions?: Record; headers?: Record; }): Promise>>; + dispose(): Promise; } export function createGatewayTester< @@ -139,5 +140,8 @@ export function createGatewayTester< [DisposableSymbols.asyncDispose]() { return runtime[DisposableSymbols.asyncDispose](); }, + async dispose() { + await runtime.dispose(); + }, }; } From c758f4fbd79744d3f36264cd284eb8c5299b4a48 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 15:22:02 +0200 Subject: [PATCH 13/35] bun is ok now? --- packages/testing/src/createGatewayTester.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index ac2282ac8..b1dfeb0f2 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -70,7 +70,7 @@ export function createGatewayTester< // compose subgraphs and create runtime const subgraphs = config.subgraphs.reduce( (acc, subgraph) => { - const url = `http://${subgraph.name}/graphql`; + const url = `http://subgraph-${subgraph.name}/graphql`; const schema = 'typeDefs' in subgraph.schema ? makeExecutableSchema(subgraph.schema) @@ -120,6 +120,7 @@ export function createGatewayTester< } const runtimeExecute = buildHTTPExecutor({ + endpoint: 'http://gateway/graphql', fetch: runtime.fetch, headers: (execReq) => execReq?.rootValue.headers, }); From 76f75eccf2efd1ae8a96f8e39bc43c4d041d29ab Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Fri, 24 Oct 2025 17:44:43 +0200 Subject: [PATCH 14/35] use build subgraph schema instead --- packages/testing/package.json | 2 +- packages/testing/src/createGatewayTester.ts | 39 +++++++++++++++++---- yarn.lock | 2 +- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/testing/package.json b/packages/testing/package.json index f26b639cf..1cf6c3883 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -43,8 +43,8 @@ "graphql": "^15.9.0 || ^16.9.0" }, "dependencies": { + "@apollo/subgraph": "^2.11.3", "@graphql-mesh/fusion-composition": "^0.8.17", - "@graphql-tools/schema": "^10.0.25", "@graphql-tools/utils": "^10.9.1", "graphql-yoga": "^5.16.0" }, diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index b1dfeb0f2..c54b3a2e8 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -1,3 +1,4 @@ +import { buildSubgraphSchema } from '@apollo/subgraph'; import { createGatewayRuntime, GatewayConfigSchemaBase, @@ -7,23 +8,42 @@ import { } from '@graphql-hive/gateway-runtime'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; -import { - makeExecutableSchema, - type IExecutableSchemaDefinition, -} from '@graphql-tools/schema'; import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; -import { parse, type GraphQLSchema } from 'graphql'; +import { + GraphQLFieldResolver, + GraphQLScalarType, + parse, + type GraphQLSchema, +} from 'graphql'; import { createYoga, DisposableSymbols, type YogaServerInstance, } from 'graphql-yoga'; +/** Thanks @apollo/subgraph for not re-exporting this! */ +export interface GraphQLResolverMap { + [typeName: string]: + | { + [fieldName: string]: + | GraphQLFieldResolver + | { + requires?: string; + resolve?: GraphQLFieldResolver; + subscribe?: GraphQLFieldResolver; + }; + } + | GraphQLScalarType + | { + [enumValue: string]: string | number; + }; +} + export interface GatewayTesterSubgraphConfig { /** The name of the subgraph. */ name: string; /** The subgraph schema. */ - schema: GraphQLSchema | IExecutableSchemaDefinition; + schema: GraphQLSchema | { typeDefs: string; resolvers?: GraphQLResolverMap }; /** An optional GraphQL Yoga server instance that runs the {@link schema built subgraph}. */ yoga?: (schema: GraphQLSchema) => YogaServerInstance; } @@ -73,7 +93,12 @@ export function createGatewayTester< const url = `http://subgraph-${subgraph.name}/graphql`; const schema = 'typeDefs' in subgraph.schema - ? makeExecutableSchema(subgraph.schema) + ? buildSubgraphSchema([ + { + ...subgraph.schema, + typeDefs: parse(subgraph.schema.typeDefs), + }, + ]) : subgraph.schema; return { ...acc, diff --git a/yarn.lock b/yarn.lock index b3020c620..f7481268c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4106,8 +4106,8 @@ __metadata: version: 0.0.0-use.local resolution: "@graphql-hive/gateway-testing@workspace:packages/testing" dependencies: + "@apollo/subgraph": "npm:^2.11.3" "@graphql-mesh/fusion-composition": "npm:^0.8.17" - "@graphql-tools/schema": "npm:^10.0.25" "@graphql-tools/utils": "npm:^10.9.1" graphql: "npm:^16.9.0" graphql-yoga: "npm:^5.16.0" From bd3d40501ce68cf2bd553a0a104f8aeddcbce22c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 24 Oct 2025 15:45:22 +0000 Subject: [PATCH 15/35] chore(dependencies): updated changesets for modified dependencies --- .../@graphql-hive_plugin-aws-sigv4-1625-dependencies.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/@graphql-hive_plugin-aws-sigv4-1625-dependencies.md diff --git a/.changeset/@graphql-hive_plugin-aws-sigv4-1625-dependencies.md b/.changeset/@graphql-hive_plugin-aws-sigv4-1625-dependencies.md new file mode 100644 index 000000000..45fd4e58e --- /dev/null +++ b/.changeset/@graphql-hive_plugin-aws-sigv4-1625-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/plugin-aws-sigv4': patch +--- + +dependencies updates: + +- Updated dependency [`@aws-sdk/client-sts@^3.916.0` ↗︎](https://www.npmjs.com/package/@aws-sdk/client-sts/v/3.916.0) (from `^3.914.0`, in `dependencies`) From a00b3148889baa5c06a6872743a54a8dbc2d9fe0 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:00:48 +0100 Subject: [PATCH 16/35] executor http is a dep --- packages/testing/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/testing/package.json b/packages/testing/package.json index 1cf6c3883..0ce33db12 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -45,6 +45,7 @@ "dependencies": { "@apollo/subgraph": "^2.11.3", "@graphql-mesh/fusion-composition": "^0.8.17", + "@graphql-tools/executor-http": "workspace:^", "@graphql-tools/utils": "^10.9.1", "graphql-yoga": "^5.16.0" }, diff --git a/yarn.lock b/yarn.lock index f7481268c..8580b8891 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4108,6 +4108,7 @@ __metadata: dependencies: "@apollo/subgraph": "npm:^2.11.3" "@graphql-mesh/fusion-composition": "npm:^0.8.17" + "@graphql-tools/executor-http": "workspace:^" "@graphql-tools/utils": "npm:^10.9.1" graphql: "npm:^16.9.0" graphql-yoga: "npm:^5.16.0" From b9e88712c363cbd2795f5be6bac939432ef54f8f Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:08:21 +0100 Subject: [PATCH 17/35] no mask errors when testing --- packages/testing/src/createGatewayTester.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index c54b3a2e8..5665974e6 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -82,10 +82,9 @@ export interface GatewayTester< export function createGatewayTester< TContext extends Record = Record, >(config: GatewayTesterConfig): GatewayTester { - // let runtime: GatewayRuntime; if ('supergraph' in config) { - runtime = createGatewayRuntime(config); + runtime = createGatewayRuntime({ maskedErrors: false, ...config }); } else { // compose subgraphs and create runtime const subgraphs = config.subgraphs.reduce( @@ -123,6 +122,7 @@ export function createGatewayTester< >, ); runtime = createGatewayRuntime({ + maskedErrors: false, ...config, supergraph: getUnifiedGraphGracefully(Object.values(subgraphs)), plugins: (ctx) => [ From 32071c856f61b0b7390ddbf24c9f06b5c6e29a0b Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:11:47 +0100 Subject: [PATCH 18/35] use yoga graphql endpoint for url --- packages/testing/src/createGatewayTester.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 5665974e6..3f4c4b91f 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -89,7 +89,6 @@ export function createGatewayTester< // compose subgraphs and create runtime const subgraphs = config.subgraphs.reduce( (acc, subgraph) => { - const url = `http://subgraph-${subgraph.name}/graphql`; const schema = 'typeDefs' in subgraph.schema ? buildSubgraphSchema([ @@ -99,15 +98,17 @@ export function createGatewayTester< }, ]) : subgraph.schema; + const yoga = + subgraph.yoga?.(schema) || + createYoga({ schema, maskedErrors: false, logging: false }); + const url = `http://subgraph-${subgraph.name}${yoga.graphqlEndpoint}`; return { ...acc, [url]: { name: subgraph.name, url, schema, - yoga: - subgraph.yoga?.(schema) || - createYoga({ schema, maskedErrors: false, logging: false }), + yoga, }, }; }, From f26cd2877a326387446bb71ed0f1db42d167516c Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:11:57 +0100 Subject: [PATCH 19/35] refactor use gateway tester --- .../tests/aws-sigv4-incoming.test.ts | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/packages/plugins/aws-sigv4/tests/aws-sigv4-incoming.test.ts b/packages/plugins/aws-sigv4/tests/aws-sigv4-incoming.test.ts index 26d452c60..4377370c5 100644 --- a/packages/plugins/aws-sigv4/tests/aws-sigv4-incoming.test.ts +++ b/packages/plugins/aws-sigv4/tests/aws-sigv4-incoming.test.ts @@ -1,12 +1,7 @@ import { buildSubgraphSchema } from '@apollo/subgraph'; import { createInlineSigningKeyProvider, useJWT } from '@graphql-hive/gateway'; -import { - createGatewayRuntime, - useCustomFetch, -} from '@graphql-hive/gateway-runtime'; -import { composeLocalSchemasWithApollo } from '@internal/testing'; +import { createGatewayTester } from '@graphql-hive/gateway-testing'; import { parse } from 'graphql'; -import { createYoga } from 'graphql-yoga'; import { describe, expect, it } from 'vitest'; import { useAWSSigv4 } from '../src'; @@ -23,18 +18,14 @@ describe('AWS Sigv4 Incoming requests', () => { }, }, }); - const subgraphServer = createYoga({ - schema: subgraphSchema, - }); it('validates incoming requests', async () => { - await using gw = createGatewayRuntime({ - supergraph: composeLocalSchemasWithApollo([ + await using gw = createGatewayTester({ + subgraphs: [ { name: 'subgraph', schema: subgraphSchema, - url: 'http://localhost:4000/graphql', }, - ]), + ], landingPage: false, graphqlEndpoint: '/', plugins: () => [ @@ -43,10 +34,6 @@ describe('AWS Sigv4 Incoming requests', () => { secretAccessKey: () => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', }, }), - useCustomFetch( - // @ts-expect-error - MeshFetch is not compatible with Yoga.fetch - subgraphServer.fetch, - ), ], }); const response = await gw.fetch( @@ -59,7 +46,7 @@ describe('AWS Sigv4 Incoming requests', () => { Date: 'Mon, 29 Dec 2015 00:00:00 GMT', 'content-type': 'application/json', Host: 'sigv4examplegraphqlbucket.s3-eu-central-1.amazonaws.com', - 'Content-Length': 30, + 'Content-Length': '30', 'X-Amz-Content-Sha256': '34c77dc7b593717e0231ac99a16ae3be5ee2e8d652bce6518738a6449dfd2647', 'X-Amz-Date': '20151229T000000Z', @@ -82,14 +69,13 @@ describe('AWS Sigv4 Incoming requests', () => { }); it('works with JWT', async () => { const JWT_SECRET = 'a-string-secret-at-least-256-bits-long'; - await using gw = createGatewayRuntime({ - supergraph: composeLocalSchemasWithApollo([ + await using gw = createGatewayTester({ + subgraphs: [ { name: 'subgraph', schema: subgraphSchema, - url: 'http://localhost:4000/graphql', }, - ]), + ], landingPage: false, graphqlEndpoint: '/', plugins: () => [ @@ -108,10 +94,6 @@ describe('AWS Sigv4 Incoming requests', () => { secretAccessKey: () => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', }, }), - useCustomFetch( - // @ts-expect-error - MeshFetch is not compatible with Yoga.fetch - subgraphServer.fetch, - ), ], }); @@ -125,7 +107,7 @@ describe('AWS Sigv4 Incoming requests', () => { Date: 'Mon, 29 Dec 2015 00:00:00 GMT', 'content-type': 'application/json', Host: 'sigv4examplegraphqlbucket.s3-eu-central-1.amazonaws.com', - 'Content-Length': 30, + 'Content-Length': '30', 'X-Amz-Content-Sha256': '34c77dc7b593717e0231ac99a16ae3be5ee2e8d652bce6518738a6449dfd2647', 'X-Amz-Date': '20151229T000000Z', From 89772828aadd56269a6b690daaa51c4a0ecb970e Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:15:41 +0100 Subject: [PATCH 20/35] tester host for whatever --- packages/testing/src/createGatewayTester.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 3f4c4b91f..9614fb258 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -44,6 +44,8 @@ export interface GatewayTesterSubgraphConfig { name: string; /** The subgraph schema. */ schema: GraphQLSchema | { typeDefs: string; resolvers?: GraphQLResolverMap }; + /** The hostname of the subgraph. URL will become `http://${host}${yoga.graphqlEndpoint}`. */ + host?: string; /** An optional GraphQL Yoga server instance that runs the {@link schema built subgraph}. */ yoga?: (schema: GraphQLSchema) => YogaServerInstance; } @@ -101,7 +103,8 @@ export function createGatewayTester< const yoga = subgraph.yoga?.(schema) || createYoga({ schema, maskedErrors: false, logging: false }); - const url = `http://subgraph-${subgraph.name}${yoga.graphqlEndpoint}`; + const host = subgraph.host || `subgraph-${subgraph.name}`; + const url = `http://${host}${yoga.graphqlEndpoint}`; return { ...acc, [url]: { From 1a0015628b47b87d7f9361217184e26ef8b6891d Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:16:20 +0100 Subject: [PATCH 21/35] aws outgoing --- .../tests/aws-sigv4-outgoing.test.ts | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts b/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts index fb25e06f3..6e72c46b3 100644 --- a/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts +++ b/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts @@ -1,10 +1,6 @@ import { buildSubgraphSchema } from '@apollo/subgraph'; -import { - createGatewayRuntime, - useCustomFetch, -} from '@graphql-hive/gateway-runtime'; +import { createGatewayTester } from '@graphql-hive/gateway-testing'; import { useAWSSigv4 } from '@graphql-hive/plugin-aws-sigv4'; -import { composeLocalSchemasWithApollo } from '@internal/testing'; import { parse } from 'graphql'; import { createYoga } from 'graphql-yoga'; import { describe, expect, it } from 'vitest'; @@ -24,26 +20,27 @@ describe('AWS Sigv4', () => { }, }); let receivedSubgraphRequest: Request | undefined; - await using subgraphServer = createYoga({ - schema: subgraphSchema, - plugins: [ - { - onRequest({ request }) { - receivedSubgraphRequest = request; - }, - }, - ], - landingPage: false, - graphqlEndpoint: '/', - }); - await using gw = createGatewayRuntime({ - supergraph: await composeLocalSchemasWithApollo([ + await using gw = createGatewayTester({ + subgraphs: [ { name: 'subgraph', schema: subgraphSchema, - url: 'http://sigv4examplegraphqlbucket.s3-eu-central-1.amazonaws.com', + host: 'sigv4examplegraphqlbucket.s3-eu-central-1.amazonaws.com', + yoga: (schema) => + createYoga({ + schema, + plugins: [ + { + onRequest({ request }) { + receivedSubgraphRequest = request; + }, + }, + ], + landingPage: false, + graphqlEndpoint: '/', + }), }, - ]), + ], transportEntries: { subgraph: { headers: [['Date', 'Mon, 29 Dec 2015 00:00:00 GMT']], @@ -57,10 +54,6 @@ describe('AWS Sigv4', () => { secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', }, }), - useCustomFetch( - // @ts-expect-error - MeshFetch is not compatible with Yoga.fetch - subgraphServer.fetch, - ), ], }); const res = await gw.fetch('http://localhost:4000/graphql', { From 4c85a610cb3788e2a95daae200f34597c6d9d2de Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:28:24 +0100 Subject: [PATCH 22/35] proxy mode --- packages/runtime/src/types.ts | 2 +- packages/testing/src/createGatewayTester.ts | 135 +++++++++++++------- 2 files changed, 87 insertions(+), 50 deletions(-) diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 7b4c16785..25da635b6 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -435,7 +435,7 @@ export interface GatewayHivePersistedDocumentsOptions { | ((request: Request) => MaybePromise); } -interface GatewayConfigBase> { +export interface GatewayConfigBase> { /** Usage reporting options. */ reporting?: GatewayHiveReportingOptions | GatewayGraphOSReportingOptions; /** Persisted documents options. */ diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 9614fb258..647dae0ac 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -1,6 +1,7 @@ import { buildSubgraphSchema } from '@apollo/subgraph'; import { createGatewayRuntime, + GatewayConfigBase, GatewayConfigSchemaBase, GatewayRuntime, UnifiedGraphConfig, @@ -39,31 +40,32 @@ export interface GraphQLResolverMap { }; } -export interface GatewayTesterSubgraphConfig { - /** The name of the subgraph. */ +export interface GatewayTesterRemoteSchemaConfig { + /** The name of the remote schema / subgraph / proxied server. */ name: string; - /** The subgraph schema. */ + /** The remote schema. */ schema: GraphQLSchema | { typeDefs: string; resolvers?: GraphQLResolverMap }; - /** The hostname of the subgraph. URL will become `http://${host}${yoga.graphqlEndpoint}`. */ + /** The hostname of the remote schema. URL will become `http://${host}${yoga.graphqlEndpoint}`. */ host?: string; - /** An optional GraphQL Yoga server instance that runs the {@link schema built subgraph}. */ + /** An optional GraphQL Yoga server instance that runs the {@link schema built schema}. */ yoga?: (schema: GraphQLSchema) => YogaServerInstance; } export type GatewayTesterConfig< TContext extends Record = Record, -> = GatewayConfigSchemaBase & - ( - | { - // gateway - supergraph: UnifiedGraphConfig; - } - | { - // gateway (composes subgraphs) - subgraphs: GatewayTesterSubgraphConfig[]; - } - ); -// TODO: proxy mode +> = + | ({ + // gateway + supergraph: UnifiedGraphConfig; + } & GatewayConfigSchemaBase) + | ({ + // gateway (composes subgraphs) + subgraphs: GatewayTesterRemoteSchemaConfig[]; + } & GatewayConfigSchemaBase) + | ({ + // proxy + proxy: GatewayTesterRemoteSchemaConfig; + } & GatewayConfigBase); // TODO: subgraph mode export interface GatewayTester< @@ -86,47 +88,27 @@ export function createGatewayTester< >(config: GatewayTesterConfig): GatewayTester { let runtime: GatewayRuntime; if ('supergraph' in config) { - runtime = createGatewayRuntime({ maskedErrors: false, ...config }); - } else { - // compose subgraphs and create runtime + // use supergraph + runtime = createGatewayRuntime({ + maskedErrors: false, + logging: false, + ...config, + }); + } else if ('subgraphs' in config) { + // compose subgraphs const subgraphs = config.subgraphs.reduce( (acc, subgraph) => { - const schema = - 'typeDefs' in subgraph.schema - ? buildSubgraphSchema([ - { - ...subgraph.schema, - typeDefs: parse(subgraph.schema.typeDefs), - }, - ]) - : subgraph.schema; - const yoga = - subgraph.yoga?.(schema) || - createYoga({ schema, maskedErrors: false, logging: false }); - const host = subgraph.host || `subgraph-${subgraph.name}`; - const url = `http://${host}${yoga.graphqlEndpoint}`; + const remoteSchema = buildRemoteSchema(subgraph); return { ...acc, - [url]: { - name: subgraph.name, - url, - schema, - yoga, - }, + [remoteSchema.url]: remoteSchema, }; }, - {} as Record< - string, - { - name: string; - url: string; - schema: GraphQLSchema; - yoga: YogaServerInstance; - } - >, + {} as Record, ); runtime = createGatewayRuntime({ maskedErrors: false, + logging: false, ...config, supergraph: getUnifiedGraphGracefully(Object.values(subgraphs)), plugins: (ctx) => [ @@ -146,6 +128,29 @@ export function createGatewayTester< ...(config.plugins?.(ctx) || []), ], }); + } else if ('proxy' in config) { + // build remote schema and proxy + const remoteSchema = buildRemoteSchema(config.proxy); + runtime = createGatewayRuntime({ + maskedErrors: false, + logging: false, + ...config, + proxy: { endpoint: remoteSchema.url }, + plugins: (ctx) => [ + useCustomFetch((url, options, context, info) => { + return remoteSchema.yoga!.fetch( + // @ts-expect-error TODO: url can be a string, not only an instance of URL + url, + options, + context, + info, + ); + }), + ...(config.plugins?.(ctx) || []), + ], + }); + } else { + throw new Error('Unsupported gateway tester configuration'); } const runtimeExecute = buildHTTPExecutor({ @@ -175,3 +180,35 @@ export function createGatewayTester< }, }; } + +interface GatewayTesterRemoteSchema { + name: string; + url: string; + schema: GraphQLSchema; + yoga: YogaServerInstance; +} + +function buildRemoteSchema( + config: GatewayTesterRemoteSchemaConfig, +): GatewayTesterRemoteSchema { + const schema = + 'typeDefs' in config.schema + ? buildSubgraphSchema([ + { + ...config.schema, + typeDefs: parse(config.schema.typeDefs), + }, + ]) + : config.schema; + const yoga = + config.yoga?.(schema) || + createYoga({ schema, maskedErrors: false, logging: false }); + const host = config.host || `config-${config.name}`; + const url = `http://${host}${yoga.graphqlEndpoint}`; + return { + name: config.name, + url, + schema, + yoga, + }; +} From aae05142079fa9714d2d477838fa995e6b53a071 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:55:01 +0100 Subject: [PATCH 23/35] unnecessary createyoga --- packages/testing/src/createGatewayTester.ts | 27 ++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 647dae0ac..a2488c41f 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -9,7 +9,11 @@ import { } from '@graphql-hive/gateway-runtime'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; -import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; +import type { + ExecutionResult, + MaybeAsyncIterable, + MaybePromise, +} from '@graphql-tools/utils'; import { GraphQLFieldResolver, GraphQLScalarType, @@ -19,6 +23,7 @@ import { import { createYoga, DisposableSymbols, + YogaServerOptions, type YogaServerInstance, } from 'graphql-yoga'; @@ -40,6 +45,10 @@ export interface GraphQLResolverMap { }; } +export type GatewayTesterRemoteSchemaConfigYoga = + | Exclude, 'schema'> + | ((schema: GraphQLSchema) => YogaServerInstance); + export interface GatewayTesterRemoteSchemaConfig { /** The name of the remote schema / subgraph / proxied server. */ name: string; @@ -48,7 +57,7 @@ export interface GatewayTesterRemoteSchemaConfig { /** The hostname of the remote schema. URL will become `http://${host}${yoga.graphqlEndpoint}`. */ host?: string; /** An optional GraphQL Yoga server instance that runs the {@link schema built schema}. */ - yoga?: (schema: GraphQLSchema) => YogaServerInstance; + yoga?: GatewayTesterRemoteSchemaConfigYoga; } export type GatewayTesterConfig< @@ -163,7 +172,7 @@ export function createGatewayTester< runtime, // @ts-expect-error native and whatwg-node fetch has conflicts fetch: runtime.fetch, - execute(args) { + async execute(args) { return runtimeExecute({ document: parse(args.query), variables: args.variables, @@ -201,9 +210,15 @@ function buildRemoteSchema( ]) : config.schema; const yoga = - config.yoga?.(schema) || - createYoga({ schema, maskedErrors: false, logging: false }); - const host = config.host || `config-${config.name}`; + typeof config.yoga === 'function' + ? config.yoga?.(schema) + : createYoga({ + maskedErrors: false, + logging: false, + ...config.yoga, + schema, + }); + const host = config.host || config.name; const url = `http://${host}${yoga.graphqlEndpoint}`; return { name: config.name, From aa8eb81af6114112944b254ca7bc2d59cdfaa511 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:55:04 +0100 Subject: [PATCH 24/35] hmac signatures --- .../tests/hmac-upstream-signature.spec.ts | 331 ++++++++---------- 1 file changed, 146 insertions(+), 185 deletions(-) diff --git a/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts b/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts index 2100ecfcc..ca8170714 100644 --- a/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts +++ b/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts @@ -1,35 +1,18 @@ import { createHmac } from 'node:crypto'; +import { GatewayPlugin } from '@graphql-hive/gateway-runtime'; import { - createGatewayRuntime, - GatewayPlugin, - useCustomFetch, -} from '@graphql-hive/gateway-runtime'; -import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; -import { MeshFetch } from '@graphql-mesh/types'; -import { GraphQLSchema, stripIgnoredCharacters } from 'graphql'; -import { createSchema, createYoga, type Plugin } from 'graphql-yoga'; + createGatewayTester, + GatewayTesterConfig, + GatewayTesterRemoteSchemaConfigYoga, +} from '@graphql-hive/gateway-testing'; +import { stripIgnoredCharacters } from 'graphql'; +import { createSchema, type Plugin } from 'graphql-yoga'; import { beforeEach, describe, expect, it, test, vi } from 'vitest'; import { defaultParamsSerializer, useHmacSignatureValidation, } from '../src/index'; -const cases = { - asProxy: () => ({ - proxy: { - endpoint: 'https://upstream/graphql', - }, - }), - asSubgraph: (upstreamSchema: GraphQLSchema) => ({ - supergraph: getUnifiedGraphGracefully([ - { - name: 'upstream', - schema: upstreamSchema, - url: 'http://upstream/graphql', - }, - ]), - }), -}; const upstreamSchema = createSchema({ typeDefs: /* GraphQL */ ` type Query { @@ -48,92 +31,78 @@ const exampleQuery = stripIgnoredCharacters(/* GraphQL */ ` hello } `); -for (const [name, createConfig] of Object.entries(cases)) { +for (const [name, configure] of Object.entries({ + 'as proxy': (yoga?: GatewayTesterRemoteSchemaConfigYoga) => + ({ + proxy: { name: 'upstream', schema: upstreamSchema, yoga }, + }) as GatewayTesterConfig, + 'as subgraph': (yoga?: GatewayTesterRemoteSchemaConfigYoga) => + ({ + subgraphs: [ + { + name: 'upstream', + schema: upstreamSchema, + yoga, + }, + ], + }) as GatewayTesterConfig, +})) { describe(`when used ${name}`, () => { describe('useHmacSignatureValidation', () => { test('should throw when header is missing or invalid', async () => { - await using upstream = createYoga({ - schema: upstreamSchema, - plugins: [], - logging: false, - }); - await using gateway = createGatewayRuntime({ - ...createConfig(upstreamSchema), + await using gateway = createGatewayTester({ + ...configure(), plugins: () => [ useHmacSignatureValidation({ secret: 'topSecret', }), - useCustomFetch(upstream.fetch as MeshFetch), ], - logging: false, }); - let response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: exampleQuery, }), - }); - - expect(await response.json()).toEqual({ + ).resolves.toEqual({ errors: [ - { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - }, - message: 'Unexpected error.', - }, + expect.objectContaining({ + message: + 'Missing HMAC signature: extension hmac-signature not found in request.', + }), ], }); - response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + + await expect( + gateway.execute({ query: exampleQuery, extensions: { 'hmac-signature': 'invalid', }, }), - }); - - expect(await response.json()).toEqual({ + ).resolves.toEqual({ errors: [ - { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - }, - message: 'Unexpected error.', - }, + expect.objectContaining({ + message: + 'Invalid HMAC signature: extension hmac-signature does not match the body content.', + }), ], }); }); test('should build a valid hmac and validate it correctly in a Yoga setup', async () => { const sharedSecret = 'topSecret'; - await using upstream = createYoga({ - schema: upstreamSchema, - plugins: [ - useHmacSignatureValidation({ - secret: sharedSecret, - }), - ], - logging: false, - }); - await using gateway = createGatewayRuntime({ - ...createConfig(upstreamSchema), + await using gateway = createGatewayTester({ + ...configure({ + plugins: [ + useHmacSignatureValidation({ + secret: sharedSecret, + }), + ], + }), hmacSignature: { secret: sharedSecret, }, plugins: () => [ - useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), { onSubgraphExecute(payload) { payload.executionRequest.extensions ||= {}; @@ -141,20 +110,20 @@ for (const [name, createConfig] of Object.entries(cases)) { }, } satisfies GatewayPlugin, ], - logging: false, }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: exampleQuery, }), - }); - - expect(response.status).toBe(200); + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "__typename": "Query", + "hello": "world", + }, + } + `); }); }); @@ -169,31 +138,18 @@ for (const [name, createConfig] of Object.entries(cases)) { const requestTrackerPlugin = { onParams: vi.fn((() => {}) as Plugin['onParams']), }; - function createUpstream() { - return createYoga({ - schema: upstreamSchema, - plugins: [requestTrackerPlugin], - logging: false, - }); - } beforeEach(() => { requestTrackerPlugin.onParams.mockClear(); }); it('should build valid hmac signature based on the request body even when its modified in other plugins', async () => { const secret = 'secret'; - await using upstream = createUpstream(); - await using gateway = createGatewayRuntime({ - ...createConfig(upstreamSchema), + await using gateway = createGatewayTester({ + ...configure({ plugins: [requestTrackerPlugin] }), hmacSignature: { secret, }, plugins: () => [ - useCustomFetch( - // We cast instead of using @ts-expect-error because when `upstream` is not defined, it doesn't error - // If you want to try, remove `upstream` variable above, then add ts-expect-error here. - upstream.fetch as MeshFetch, - ), { onSubgraphExecute(payload) { payload.executionRequest.extensions ||= {}; @@ -204,143 +160,148 @@ for (const [name, createConfig] of Object.entries(cases)) { logging: false, }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: exampleQuery, }), - }); + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "__typename": "Query", + "hello": "world", + }, + } + `); - expect(response.status).toBe(200); expect(requestTrackerPlugin.onParams).toHaveBeenCalledTimes( - name === 'asProxy' ? 2 : 1, + name === 'as proxy' ? 2 : 1, ); - const callIndex = name === 'asProxy' ? 1 : 0; const upstreamReqParams = - requestTrackerPlugin.onParams.mock.calls[callIndex]![0].params; - const upstreamExtensions = upstreamReqParams.extensions!; - expect(upstreamExtensions['hmac-signature']).toBeDefined(); + requestTrackerPlugin.onParams.mock.calls[ + name === 'as proxy' ? 1 : 0 + ]![0].params; const upstreamReqBody = defaultParamsSerializer(upstreamReqParams); - expect(upstreamReqParams.extensions?.['addedToPayload']).toBeTruthy(); - // Signature on the upstream call should match when manually validated - expect(upstreamExtensions['hmac-signature']).toEqual( - hashSHA256(secret, upstreamReqBody), - ); + expect(upstreamReqParams).toEqual({ + extensions: { + addedToPayload: true, + 'hmac-signature': hashSHA256(secret, upstreamReqBody), + }, + query: '{__typename hello}', + }); }); it('should include hmac signature based on the request body', async () => { const secret = 'secret'; - await using upstream = createUpstream(); - await using gateway = createGatewayRuntime({ - ...createConfig(upstreamSchema), + await using gateway = createGatewayTester({ + ...configure({ plugins: [requestTrackerPlugin] }), hmacSignature: { secret, }, - plugins: () => [useCustomFetch(upstream.fetch as MeshFetch)], - logging: false, }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: exampleQuery, }), - }); + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "__typename": "Query", + "hello": "world", + }, + } + `); - expect(response.status).toBe(200); expect(requestTrackerPlugin.onParams).toHaveBeenCalledTimes( - name === 'asProxy' ? 2 : 1, + name === 'as proxy' ? 2 : 1, ); - const callIndex = name === 'asProxy' ? 1 : 0; const upstreamReqParams = - requestTrackerPlugin.onParams.mock.calls[callIndex]![0].params; - const upstreamExtensions = upstreamReqParams.extensions!; - const upstreamHmacExtension = upstreamExtensions['hmac-signature']; - expect(upstreamHmacExtension).toBeDefined(); + requestTrackerPlugin.onParams.mock.calls[ + name === 'as proxy' ? 1 : 0 + ]![0].params; const upstreamReqBody = defaultParamsSerializer(upstreamReqParams); - // Signature on the upstream call should match when manually validated - expect(upstreamHmacExtension).toEqual( - hashSHA256(secret, upstreamReqBody), - ); + expect(upstreamReqParams).toEqual({ + extensions: { + // addedToPayload: true, not added by other plugin + 'hmac-signature': hashSHA256(secret, upstreamReqBody), + }, + query: '{__typename hello}', + }); }); it('should allow to customize header name', async () => { const secret = 'secret'; const customExtensionName = 'custom-hmac-signature'; - await using upstream = createUpstream(); - await using gateway = createGatewayRuntime({ - ...createConfig(upstreamSchema), + await using gateway = createGatewayTester({ + ...configure({ plugins: [requestTrackerPlugin] }), hmacSignature: { secret, extensionName: customExtensionName, }, - plugins: () => [useCustomFetch(upstream.fetch as MeshFetch)], - logging: false, }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: exampleQuery, }), - }); + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "__typename": "Query", + "hello": "world", + }, + } + `); - expect(response.status).toBe(200); expect(requestTrackerPlugin.onParams).toHaveBeenCalledTimes( - name === 'asProxy' ? 2 : 1, + name === 'as proxy' ? 2 : 1, ); - const callIndex = name === 'asProxy' ? 1 : 0; const upstreamReqParams = - requestTrackerPlugin.onParams.mock.calls[callIndex]![0].params; - const upstreamExtensions = upstreamReqParams.extensions!; - const upstreamHmacExtension = upstreamExtensions[customExtensionName]; - expect(upstreamHmacExtension).toBeDefined(); + requestTrackerPlugin.onParams.mock.calls[ + name === 'as proxy' ? 1 : 0 + ]![0].params; const upstreamReqBody = defaultParamsSerializer(upstreamReqParams); - // Signature on the upstream call should match when manually validated - expect(upstreamHmacExtension).toEqual( - hashSHA256(secret, upstreamReqBody), - ); + expect(upstreamReqParams).toEqual({ + extensions: { + [customExtensionName]: hashSHA256(secret, upstreamReqBody), + }, + query: '{__typename hello}', + }); }); it('should allow to filter upstream calls', async () => { const secret = 'secret'; - await using upstream = createUpstream(); - await using gateway = createGatewayRuntime({ - ...createConfig(upstreamSchema), + await using gateway = createGatewayTester({ + ...configure({ plugins: [requestTrackerPlugin] }), hmacSignature: { secret, shouldSign: () => false, }, - plugins: () => [useCustomFetch(upstream.fetch as MeshFetch)], - logging: false, }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: exampleQuery, }), - }); + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "__typename": "Query", + "hello": "world", + }, + } + `); - expect(response.status).toBe(200); expect(requestTrackerPlugin.onParams).toHaveBeenCalledTimes( - name === 'asProxy' ? 2 : 1, + name === 'as proxy' ? 2 : 1, ); - for (const call of requestTrackerPlugin.onParams.mock.calls) { - expect(call[0].params.extensions?.['hmac-signature']).toBeUndefined(); - } + const upstreamReqParams = + requestTrackerPlugin.onParams.mock.calls[ + name === 'as proxy' ? 1 : 0 + ]![0].params; + expect(upstreamReqParams).toEqual({ + query: '{__typename hello}', + }); }); }); }); From f6cc85495616b3e2c72a618bd163bf7ac5089adb Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 13:57:09 +0100 Subject: [PATCH 25/35] no createyoga in tester --- .../tests/aws-sigv4-outgoing.test.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts b/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts index 6e72c46b3..2dab9d59a 100644 --- a/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts +++ b/packages/plugins/aws-sigv4/tests/aws-sigv4-outgoing.test.ts @@ -2,7 +2,6 @@ import { buildSubgraphSchema } from '@apollo/subgraph'; import { createGatewayTester } from '@graphql-hive/gateway-testing'; import { useAWSSigv4 } from '@graphql-hive/plugin-aws-sigv4'; import { parse } from 'graphql'; -import { createYoga } from 'graphql-yoga'; import { describe, expect, it } from 'vitest'; describe('AWS Sigv4', () => { @@ -26,19 +25,17 @@ describe('AWS Sigv4', () => { name: 'subgraph', schema: subgraphSchema, host: 'sigv4examplegraphqlbucket.s3-eu-central-1.amazonaws.com', - yoga: (schema) => - createYoga({ - schema, - plugins: [ - { - onRequest({ request }) { - receivedSubgraphRequest = request; - }, + yoga: { + plugins: [ + { + onRequest({ request }) { + receivedSubgraphRequest = request; }, - ], - landingPage: false, - graphqlEndpoint: '/', - }), + }, + ], + landingPage: false, + graphqlEndpoint: '/', + }, }, ], transportEntries: { From b32b8abfb10c1d8551daf9875968fb4ffac65210 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 14:04:08 +0100 Subject: [PATCH 26/35] support polling --- packages/testing/src/createGatewayTester.ts | 50 +++++++++++++-------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index a2488c41f..09c2a24ec 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -9,11 +9,7 @@ import { } from '@graphql-hive/gateway-runtime'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; -import type { - ExecutionResult, - MaybeAsyncIterable, - MaybePromise, -} from '@graphql-tools/utils'; +import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; import { GraphQLFieldResolver, GraphQLScalarType, @@ -69,7 +65,9 @@ export type GatewayTesterConfig< } & GatewayConfigSchemaBase) | ({ // gateway (composes subgraphs) - subgraphs: GatewayTesterRemoteSchemaConfig[]; + subgraphs: + | GatewayTesterRemoteSchemaConfig[] + | (() => GatewayTesterRemoteSchemaConfig[]); } & GatewayConfigSchemaBase) | ({ // proxy @@ -105,26 +103,40 @@ export function createGatewayTester< }); } else if ('subgraphs' in config) { // compose subgraphs - const subgraphs = config.subgraphs.reduce( - (acc, subgraph) => { - const remoteSchema = buildRemoteSchema(subgraph); - return { - ...acc, - [remoteSchema.url]: remoteSchema, - }; - }, - {} as Record, - ); + const subgraphsConfig = config.subgraphs; + function buildSubgraphs() { + subgraphsRef.ref = ( + typeof subgraphsConfig === 'function' + ? subgraphsConfig() + : subgraphsConfig + ).reduce( + (acc, subgraph) => { + const remoteSchema = buildRemoteSchema(subgraph); + return { + ...acc, + [remoteSchema.url]: remoteSchema, + }; + }, + {} as Record, + ); + return Object.values(subgraphsRef.ref); + } + const subgraphsRef = { + ref: null as Record | null, + }; runtime = createGatewayRuntime({ maskedErrors: false, logging: false, ...config, - supergraph: getUnifiedGraphGracefully(Object.values(subgraphs)), + supergraph: + typeof config.subgraphs === 'function' + ? () => getUnifiedGraphGracefully(buildSubgraphs()) + : getUnifiedGraphGracefully(buildSubgraphs()), plugins: (ctx) => [ useCustomFetch((url, options, context, info) => { - const subgraph = subgraphs[url]; + const subgraph = subgraphsRef.ref?.[url]; if (!subgraph) { - throw new Error(`Subgraph for URL "${url}" not found`); + throw new Error(`Subgraph for URL "${url}" not found or not ready`); } return subgraph.yoga!.fetch( // @ts-expect-error TODO: url can be a string, not only an instance of URL From cacff364c557c34c6a9787c2ce4713c0d085e9a4 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 14:08:36 +0100 Subject: [PATCH 27/35] match mode types --- packages/testing/src/createGatewayTester.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 09c2a24ec..7c312d92f 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -2,7 +2,7 @@ import { buildSubgraphSchema } from '@apollo/subgraph'; import { createGatewayRuntime, GatewayConfigBase, - GatewayConfigSchemaBase, + GatewayConfigSupergraph, GatewayRuntime, UnifiedGraphConfig, useCustomFetch, @@ -62,13 +62,13 @@ export type GatewayTesterConfig< | ({ // gateway supergraph: UnifiedGraphConfig; - } & GatewayConfigSchemaBase) + } & Omit, 'supergraph'>) | ({ // gateway (composes subgraphs) subgraphs: | GatewayTesterRemoteSchemaConfig[] | (() => GatewayTesterRemoteSchemaConfig[]); - } & GatewayConfigSchemaBase) + } & Omit, 'supergraph'>) | ({ // proxy proxy: GatewayTesterRemoteSchemaConfig; From 7e6c17bab20b80483576a33bfa4ed96cd41036cd Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 14:13:50 +0100 Subject: [PATCH 28/35] better fetch setter and susbiscrpns migrate --- packages/runtime/tests/subscriptions.test.ts | 72 ++++++++------------ packages/testing/src/createGatewayTester.ts | 34 +++++---- 2 files changed, 46 insertions(+), 60 deletions(-) diff --git a/packages/runtime/tests/subscriptions.test.ts b/packages/runtime/tests/subscriptions.test.ts index e0e0327b1..05bf395fb 100644 --- a/packages/runtime/tests/subscriptions.test.ts +++ b/packages/runtime/tests/subscriptions.test.ts @@ -1,12 +1,10 @@ -import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; +import { createGatewayTester } from '@graphql-hive/gateway-testing'; import { type MaybePromise } from '@graphql-tools/utils'; import { isDebug } from '@internal/testing'; import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { createClient as createSSEClient } from 'graphql-sse'; -import { createSchema, createYoga, Repeater } from 'graphql-yoga'; +import { createSchema, Repeater } from 'graphql-yoga'; import { afterAll, describe, expect, it } from 'vitest'; -import { createGatewayRuntime } from '../src/createGatewayRuntime'; -import { useCustomFetch } from '../src/plugins/useCustomFetch'; describe('Subscriptions', () => { const leftovers: (() => MaybePromise)[] = []; @@ -35,25 +33,14 @@ describe('Subscriptions', () => { }, }, }); - const upstream = createYoga({ schema: upstreamSchema }); it('should terminate subscriptions gracefully on shutdown', async () => { - await using serve = createGatewayRuntime({ - logging: false, - supergraph() { - return getUnifiedGraphGracefully([ - { - name: 'upstream', - schema: upstreamSchema, - url: 'http://upstream/graphql', - }, - ]); - }, - plugins: () => [ - useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), + await using serve = createGatewayTester({ + subgraphs: [ + { + name: 'upstream', + schema: upstreamSchema, + }, ], }); @@ -98,32 +85,32 @@ describe('Subscriptions', () => { it('should terminate subscriptions gracefully on schema update', async () => { let changeSchema = false; - await using serve = createGatewayRuntime({ + await using serve = createGatewayTester({ logging: isDebug(), pollingInterval: 500, - async supergraph() { + subgraphs: () => { if (changeSchema) { - return /* GraphQL */ ` - type Query { - foo: String! - } - `; + return [ + { + name: 'upstream', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + foo: String! + } + `, + }, + }, + ]; } changeSchema = true; - return getUnifiedGraphGracefully([ + return [ { name: 'upstream', schema: upstreamSchema, - url: 'http://upstream/graphql', }, - ]); + ]; }, - plugins: () => [ - useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), - ], }); const sse = createSSEClient({ @@ -141,20 +128,15 @@ describe('Subscriptions', () => { const msgs: unknown[] = []; globalThis.setTimeout(async () => { - const res = await serve.fetch('http://mesh/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + await expect( + serve.execute({ query: /* GraphQL */ ` query { __typename } `, }), - }); - expect(await res.json()).toMatchObject({ + ).resolves.toMatchObject({ data: { __typename: 'Query', }, diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 7c312d92f..7645bee70 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -3,6 +3,7 @@ import { createGatewayRuntime, GatewayConfigBase, GatewayConfigSupergraph, + GatewayPlugin, GatewayRuntime, UnifiedGraphConfig, useCustomFetch, @@ -114,10 +115,10 @@ export function createGatewayTester< const remoteSchema = buildRemoteSchema(subgraph); return { ...acc, - [remoteSchema.url]: remoteSchema, + [remoteSchema.name]: remoteSchema, }; }, - {} as Record, + {} as { [subgraphName: string]: GatewayTesterRemoteSchema }, ); return Object.values(subgraphsRef.ref); } @@ -133,19 +134,22 @@ export function createGatewayTester< ? () => getUnifiedGraphGracefully(buildSubgraphs()) : getUnifiedGraphGracefully(buildSubgraphs()), plugins: (ctx) => [ - useCustomFetch((url, options, context, info) => { - const subgraph = subgraphsRef.ref?.[url]; - if (!subgraph) { - throw new Error(`Subgraph for URL "${url}" not found or not ready`); - } - return subgraph.yoga!.fetch( - // @ts-expect-error TODO: url can be a string, not only an instance of URL - url, - options, - context, - info, - ); - }), + { + onFetch({ executionRequest, setFetchFn }) { + const subgraphName = executionRequest?.subgraphName; + if (!subgraphName) { + return; + } + if (!subgraphsRef.ref) { + throw new Error('Subgraphs are not built yet'); + } + const subgraph = subgraphsRef.ref[subgraphName]; + if (!subgraph) { + throw new Error(`Subgraph "${subgraphName}" not found`); + } + setFetchFn(subgraph.yoga.fetch); + }, + } as GatewayPlugin, ...(config.plugins?.(ctx) || []), ], }); From 36b9ff6e3aec2a99af77ba070f2851457703ade6 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 14:52:03 +0100 Subject: [PATCH 29/35] start prop headers --- .../runtime/tests/propagateHeaders.spec.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/runtime/tests/propagateHeaders.spec.ts b/packages/runtime/tests/propagateHeaders.spec.ts index d486da7ff..5ca60ff5a 100644 --- a/packages/runtime/tests/propagateHeaders.spec.ts +++ b/packages/runtime/tests/propagateHeaders.spec.ts @@ -1,3 +1,4 @@ +import { createGatewayTester } from '@graphql-hive/gateway-testing'; import InMemoryLRUCache from '@graphql-mesh/cache-inmemory-lru'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import useHttpCache from '@graphql-mesh/plugin-http-cache'; @@ -12,19 +13,20 @@ describe('usePropagateHeaders', () => { const requestTrackerPlugin = { onParams: vi.fn((() => {}) as Plugin['onParams']), }; - const upstream = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello: String - } - `, - resolvers: { - Query: { - hello: () => 'world', - }, + const upstreamSchema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String + } + `, + resolvers: { + Query: { + hello: () => 'world', }, - }), + }, + }); + const upstream = createYoga({ + schema: upstreamSchema, plugins: [requestTrackerPlugin], logging: isDebug(), }); @@ -32,9 +34,13 @@ describe('usePropagateHeaders', () => { requestTrackerPlugin.onParams.mockClear(); }); it('forwards specified headers', async () => { - await using gateway = createGatewayRuntime({ + await using gateway = createGatewayTester({ proxy: { - endpoint: 'http://localhost:4001/graphql', + name: 'upstream', + schema: upstreamSchema, + yoga: { + plugins: [requestTrackerPlugin], + }, }, propagateHeaders: { fromClientToSubgraphs({ request }) { @@ -44,12 +50,6 @@ describe('usePropagateHeaders', () => { }; }, }, - plugins: () => [ - useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), - ], logging: isDebug(), }); const response = await gateway.fetch('http://localhost:4000/graphql', { From c86b839e7134540289a9fca93e684e299e97277e Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 16:45:17 +0100 Subject: [PATCH 30/35] propagat hed teste --- .../runtime/tests/propagateHeaders.spec.ts | 577 ++++++------------ 1 file changed, 198 insertions(+), 379 deletions(-) diff --git a/packages/runtime/tests/propagateHeaders.spec.ts b/packages/runtime/tests/propagateHeaders.spec.ts index 5ca60ff5a..191eb3184 100644 --- a/packages/runtime/tests/propagateHeaders.spec.ts +++ b/packages/runtime/tests/propagateHeaders.spec.ts @@ -1,47 +1,43 @@ -import { createGatewayTester } from '@graphql-hive/gateway-testing'; +import { + createGatewayTester, + GatewayTesterRemoteSchemaConfig, +} from '@graphql-hive/gateway-testing'; import InMemoryLRUCache from '@graphql-mesh/cache-inmemory-lru'; -import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import useHttpCache from '@graphql-mesh/plugin-http-cache'; -import { isDebug } from '@internal/testing'; -import { createSchema, createYoga, type Plugin } from 'graphql-yoga'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { createGatewayRuntime } from '../src/createGatewayRuntime'; -import { useCustomFetch } from '../src/plugins/useCustomFetch'; +import { createSchema, type Plugin } from 'graphql-yoga'; +import { describe, expect, it, vi } from 'vitest'; describe('usePropagateHeaders', () => { describe('From Client to the Subgraphs', () => { - const requestTrackerPlugin = { - onParams: vi.fn((() => {}) as Plugin['onParams']), - }; - const upstreamSchema = createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello: String - } - `, - resolvers: { - Query: { - hello: () => 'world', + function prepare() { + const requestTrackerPlugin = { + onParams: vi.fn((() => {}) as Plugin['onParams']), + }; + return { + requestTrackerPlugin, + name: 'upstream', + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String + } + `, + resolvers: { + Query: { + hello: () => 'world', + }, + }, + }), + yoga: { + plugins: [requestTrackerPlugin], }, - }, - }); - const upstream = createYoga({ - schema: upstreamSchema, - plugins: [requestTrackerPlugin], - logging: isDebug(), - }); - beforeEach(() => { - requestTrackerPlugin.onParams.mockClear(); - }); + }; + } + it('forwards specified headers', async () => { + const { requestTrackerPlugin, ...config } = prepare(); await using gateway = createGatewayTester({ - proxy: { - name: 'upstream', - schema: upstreamSchema, - yoga: { - plugins: [requestTrackerPlugin], - }, - }, + proxy: config, propagateHeaders: { fromClientToSubgraphs({ request }) { return { @@ -50,7 +46,6 @@ describe('usePropagateHeaders', () => { }; }, }, - logging: isDebug(), }); const response = await gateway.fetch('http://localhost:4000/graphql', { method: 'POST', @@ -91,71 +86,10 @@ describe('usePropagateHeaders', () => { expect(headersObj['x-my-other']).toBe('other-value'); expect(headersObj['x-extra-header']).toBeUndefined(); }); - it.skip("forwards specified headers but doesn't override the provided headers", async () => { - await using gateway = createGatewayRuntime({ - logging: isDebug(), - proxy: { - endpoint: 'http://localhost:4001/graphql', - headers: { - 'x-my-header': 'my-value', - 'x-extra-header': 'extra-value', - }, - }, - propagateHeaders: { - fromClientToSubgraphs({ request }) { - return { - 'x-my-header': request.headers.get('x-my-header')!, - 'x-my-other': request.headers.get('x-my-other')!, - }; - }, - }, - plugins: () => [ - useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), - ], - }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - 'x-my-header': 'my-new-value', - 'x-my-other': 'other-value', - }, - body: JSON.stringify({ - query: /* GraphQL */ ` - query { - hello - } - `, - }), - }); - - const resJson = await response.json(); - expect(resJson).toEqual({ - data: { - hello: 'world', - }, - }); - - // The first call is for the introspection - expect(requestTrackerPlugin.onParams).toHaveBeenCalledTimes(2); - const onParamsPayload = requestTrackerPlugin.onParams.mock.calls[1]?.[0]!; - // Do not pass extensions - expect(onParamsPayload.params.extensions).toBeUndefined(); - const headersObj = Object.fromEntries( - onParamsPayload.request.headers.entries(), - ); - expect(headersObj['x-my-header']).toBe('my-value'); - expect(headersObj['x-extra-header']).toBe('extra-value'); - expect(headersObj['x-my-other']).toBe('other-value'); - }); it("won't forward empty headers", async () => { - await using gateway = createGatewayRuntime({ - proxy: { - endpoint: 'http://localhost:4001/graphql', - }, + const { requestTrackerPlugin, ...config } = prepare(); + await using gateway = createGatewayTester({ + proxy: config, propagateHeaders: { fromClientToSubgraphs({ request }) { return { @@ -163,20 +97,10 @@ describe('usePropagateHeaders', () => { }; }, }, - plugins: () => [ - useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), - ], - logging: isDebug(), }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ + + await expect( + gateway.execute({ query: /* GraphQL */ ` query { hello @@ -186,10 +110,7 @@ describe('usePropagateHeaders', () => { randomThing: 'randomValue', }, }), - }); - - const resJson = await response.json(); - expect(resJson).toEqual({ + ).resolves.toEqual({ data: { hello: 'world', }, @@ -206,72 +127,67 @@ describe('usePropagateHeaders', () => { expect(headersObj['x-empty-header']).toBeUndefined(); }); }); + describe('From Subgraphs to the Client', () => { - const upstream1 = createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello1: String - } - `, - resolvers: { - Query: { - hello1: () => 'world1', - }, - }, - }); - const upstream1Fetch = createYoga({ - schema: upstream1, - plugins: [ - { - onResponse: ({ response }) => { - response.headers.set('cache-control', 'max-age=60, private'); - response.headers.set('upstream1', 'upstream1'); - response.headers.append('set-cookie', 'cookie1=value1'); - response.headers.append('set-cookie', 'cookie2=value2'); + const subgraphs: GatewayTesterRemoteSchemaConfig[] = [ + { + name: 'upstream1', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello1: String + } + `, + resolvers: { + Query: { + hello1: () => 'world1', + }, }, }, - ], - }).fetch; - const upstream2 = createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello2: String - } - `, - resolvers: { - Query: { - hello2: () => 'world2', + yoga: { + plugins: [ + { + onResponse: ({ response }) => { + response.headers.set('cache-control', 'max-age=60, private'); + response.headers.set('upstream1', 'upstream1'); + response.headers.append('set-cookie', 'cookie1=value1'); + response.headers.append('set-cookie', 'cookie2=value2'); + }, + } as Plugin, + ], }, }, - }); - const upstream2Fetch = createYoga({ - schema: upstream2, - plugins: [ - { - onResponse: ({ response }) => { - response.headers.set('upstream2', 'upstream2'); - response.headers.append('set-cookie', 'cookie3=value3'); - response.headers.append('set-cookie', 'cookie4=value4'); + { + name: 'upstream2', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello2: String + } + `, + resolvers: { + Query: { + hello2: () => 'world2', + }, }, }, - ], - }).fetch; - it('Aggregates cookies from all subgraphs', async () => { - await using gateway = createGatewayRuntime({ - supergraph: () => { - return getUnifiedGraphGracefully([ + yoga: { + plugins: [ { - name: 'upstream1', - schema: upstream1, - url: 'http://localhost:4001/graphql', - }, - { - name: 'upstream2', - schema: upstream2, - url: 'http://localhost:4002/graphql', - }, - ]); + onResponse: ({ response }) => { + response.headers.set('upstream2', 'upstream2'); + response.headers.append('set-cookie', 'cookie3=value3'); + response.headers.append('set-cookie', 'cookie4=value4'); + }, + } as Plugin, + ], }, + }, + ]; + + it('Aggregates cookies from all subgraphs', async () => { + await using gateway = createGatewayTester({ + subgraphs, propagateHeaders: { fromSubgraphsToClient({ response }) { const cookies = response.headers.getSetCookie(); @@ -292,22 +208,8 @@ describe('usePropagateHeaders', () => { return returns; }, }, - plugins: () => [ - useCustomFetch((url, options, context, info) => { - switch (url) { - case 'http://localhost:4001/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream1Fetch(url, options, context, info); - case 'http://localhost:4002/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream2Fetch(url, options, context, info); - default: - throw new Error('Invalid URL'); - } - }), - ], - logging: isDebug(), }); + const response = await gateway.fetch('http://localhost:4000/graphql', { method: 'POST', headers: { @@ -323,8 +225,7 @@ describe('usePropagateHeaders', () => { }), }); - const resJson = await response.json(); - expect(resJson).toEqual({ + await expect(response.json()).resolves.toEqual({ data: { hello1: 'world1', hello2: 'world2', @@ -340,22 +241,9 @@ describe('usePropagateHeaders', () => { it('should propagate headers when caching upstream', async () => { await using cache = new InMemoryLRUCache(); - await using gateway = createGatewayRuntime({ + await using gateway = createGatewayTester({ + subgraphs, cache, - supergraph: () => { - return getUnifiedGraphGracefully([ - { - name: 'upstream1', - schema: upstream1, - url: 'http://localhost:4001/graphql', - }, - { - name: 'upstream2', - schema: upstream2, - url: 'http://localhost:4002/graphql', - }, - ]); - }, propagateHeaders: { fromSubgraphsToClient({ response }) { const cookies = response.headers.getSetCookie(); @@ -377,22 +265,7 @@ describe('usePropagateHeaders', () => { return returns; }, }, - plugins: (context) => [ - useHttpCache(context), - useCustomFetch((url, options, context, info) => { - switch (url) { - case 'http://localhost:4001/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream1Fetch(url, options, context, info); - case 'http://localhost:4002/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream2Fetch(url, options, context, info); - default: - throw new Error('Invalid URL'); - } - }), - ], - logging: isDebug(), + plugins: (context) => [useHttpCache(context)], }); for (let i = 0; i < 3; i++) { @@ -426,77 +299,65 @@ describe('usePropagateHeaders', () => { } }); it('should deduplicate non-cookie headers from multiple subgraphs when deduplicateHeaders is true', async () => { - const upstream1WithDuplicates = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello1: String - } - `, - resolvers: { - Query: { - hello1: () => 'world1', - }, - }, - }), - plugins: [ + await using gateway = createGatewayTester({ + subgraphs: [ { - onResponse: ({ response }) => { - response.headers.set('x-shared-header', 'value-from-upstream1'); - response.headers.append('set-cookie', 'cookie1=value1'); + name: 'upstream1', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello1: String + } + `, + resolvers: { + Query: { + hello1: () => 'world1', + }, + }, }, - }, - ], - }).fetch; - const upstream2WithDuplicates = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello2: String - } - `, - resolvers: { - Query: { - hello2: () => 'world2', + yoga: { + plugins: [ + { + onResponse: ({ response }) => { + response.headers.set( + 'x-shared-header', + 'value-from-upstream1', + ); + response.headers.append('set-cookie', 'cookie1=value1'); + }, + }, + ], }, }, - }), - plugins: [ { - onResponse: ({ response }) => { - response.headers.set('x-shared-header', 'value-from-upstream2'); - response.headers.append('set-cookie', 'cookie2=value2'); + name: 'upstream2', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello2: String + } + `, + resolvers: { + Query: { + hello2: () => 'world2', + }, + }, + }, + yoga: { + plugins: [ + { + onResponse: ({ response }) => { + response.headers.set( + 'x-shared-header', + 'value-from-upstream2', + ); + response.headers.append('set-cookie', 'cookie2=value2'); + }, + }, + ], }, }, ], - }).fetch; - await using gateway = createGatewayRuntime({ - supergraph: () => { - return getUnifiedGraphGracefully([ - { - name: 'upstream1', - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello1: String - } - `, - }), - url: 'http://localhost:4001/graphql', - }, - { - name: 'upstream2', - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello2: String - } - `, - }), - url: 'http://localhost:4002/graphql', - }, - ]); - }, propagateHeaders: { deduplicateHeaders: true, fromSubgraphsToClient({ response }) { @@ -514,21 +375,6 @@ describe('usePropagateHeaders', () => { return returns; }, }, - plugins: () => [ - useCustomFetch((url, options, context, info) => { - switch (url) { - case 'http://localhost:4001/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream1WithDuplicates(url, options, context, info); - case 'http://localhost:4002/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream2WithDuplicates(url, options, context, info); - default: - throw new Error('Invalid URL'); - } - }), - ], - logging: isDebug(), }); const response = await gateway.fetch('http://localhost:4000/graphql', { method: 'POST', @@ -564,77 +410,65 @@ describe('usePropagateHeaders', () => { ); }); it('should append all non-cookie headers from multiple subgraphs when deduplicateHeaders is false', async () => { - const upstream1WithDuplicates = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello1: String - } - `, - resolvers: { - Query: { - hello1: () => 'world1', - }, - }, - }), - plugins: [ + await using gateway = createGatewayTester({ + subgraphs: [ { - onResponse: ({ response }) => { - response.headers.set('x-shared-header', 'value-from-upstream1'); - response.headers.append('set-cookie', 'cookie1=value1'); + name: 'upstream1', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello1: String + } + `, + resolvers: { + Query: { + hello1: () => 'world1', + }, + }, }, - }, - ], - }).fetch; - const upstream2WithDuplicates = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello2: String - } - `, - resolvers: { - Query: { - hello2: () => 'world2', + yoga: { + plugins: [ + { + onResponse: ({ response }) => { + response.headers.set( + 'x-shared-header', + 'value-from-upstream1', + ); + response.headers.append('set-cookie', 'cookie1=value1'); + }, + }, + ], }, }, - }), - plugins: [ { - onResponse: ({ response }) => { - response.headers.set('x-shared-header', 'value-from-upstream2'); - response.headers.append('set-cookie', 'cookie2=value2'); + name: 'upstream2', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello2: String + } + `, + resolvers: { + Query: { + hello2: () => 'world2', + }, + }, + }, + yoga: { + plugins: [ + { + onResponse: ({ response }) => { + response.headers.set( + 'x-shared-header', + 'value-from-upstream2', + ); + response.headers.append('set-cookie', 'cookie2=value2'); + }, + }, + ], }, }, ], - }).fetch; - await using gateway = createGatewayRuntime({ - supergraph: () => { - return getUnifiedGraphGracefully([ - { - name: 'upstream1', - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello1: String - } - `, - }), - url: 'http://localhost:4001/graphql', - }, - { - name: 'upstream2', - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello2: String - } - `, - }), - url: 'http://localhost:4002/graphql', - }, - ]); - }, propagateHeaders: { deduplicateHeaders: false, fromSubgraphsToClient({ response }) { @@ -652,21 +486,6 @@ describe('usePropagateHeaders', () => { return returns; }, }, - plugins: () => [ - useCustomFetch((url, options, context, info) => { - switch (url) { - case 'http://localhost:4001/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream1WithDuplicates(url, options, context, info); - case 'http://localhost:4002/graphql': - // @ts-expect-error TODO: url can be a string, not only an instance of URL - return upstream2WithDuplicates(url, options, context, info); - default: - throw new Error('Invalid URL'); - } - }), - ], - logging: isDebug(), }); const response = await gateway.fetch('http://localhost:4000/graphql', { method: 'POST', From cb6291eaa71eda39c2f6a07d3ad7b17d2e2a6077 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 16:46:04 +0100 Subject: [PATCH 31/35] debug enables logging --- .../tests/hmac-upstream-signature.spec.ts | 1 - .../prometheus/tests/prometheus.spec.ts | 1 - packages/runtime/tests/subscriptions.test.ts | 1 - packages/testing/src/createGatewayTester.ts | 21 ++++++++++++------- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts b/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts index ca8170714..5b0fa61e9 100644 --- a/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts +++ b/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts @@ -157,7 +157,6 @@ for (const [name, configure] of Object.entries({ }, } satisfies GatewayPlugin, ], - logging: false, }); await expect( diff --git a/packages/plugins/prometheus/tests/prometheus.spec.ts b/packages/plugins/prometheus/tests/prometheus.spec.ts index e933b5860..167ad47fb 100644 --- a/packages/plugins/prometheus/tests/prometheus.spec.ts +++ b/packages/plugins/prometheus/tests/prometheus.spec.ts @@ -62,7 +62,6 @@ describe('Prometheus', () => { }, }), ], - logging: isDebug(), maskedErrors: false, }); } diff --git a/packages/runtime/tests/subscriptions.test.ts b/packages/runtime/tests/subscriptions.test.ts index 05bf395fb..d6d0ee48c 100644 --- a/packages/runtime/tests/subscriptions.test.ts +++ b/packages/runtime/tests/subscriptions.test.ts @@ -86,7 +86,6 @@ describe('Subscriptions', () => { let changeSchema = false; await using serve = createGatewayTester({ - logging: isDebug(), pollingInterval: 500, subgraphs: () => { if (changeSchema) { diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 7645bee70..6903dfc65 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -11,6 +11,7 @@ import { import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { buildHTTPExecutor } from '@graphql-tools/executor-http'; import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; +import { isDebug } from '~internal/env'; import { GraphQLFieldResolver, GraphQLScalarType, @@ -72,7 +73,10 @@ export type GatewayTesterConfig< } & Omit, 'supergraph'>) | ({ // proxy - proxy: GatewayTesterRemoteSchemaConfig; + proxy: GatewayTesterRemoteSchemaConfig & { + /** Additional headers to be sent to the remote schema on every request. */ + headers?: Record; + }; } & GatewayConfigBase); // TODO: subgraph mode @@ -99,7 +103,7 @@ export function createGatewayTester< // use supergraph runtime = createGatewayRuntime({ maskedErrors: false, - logging: false, + logging: isDebug(), ...config, }); } else if ('subgraphs' in config) { @@ -127,7 +131,7 @@ export function createGatewayTester< }; runtime = createGatewayRuntime({ maskedErrors: false, - logging: false, + logging: isDebug(), ...config, supergraph: typeof config.subgraphs === 'function' @@ -158,9 +162,9 @@ export function createGatewayTester< const remoteSchema = buildRemoteSchema(config.proxy); runtime = createGatewayRuntime({ maskedErrors: false, - logging: false, + logging: isDebug(), ...config, - proxy: { endpoint: remoteSchema.url }, + proxy: { endpoint: remoteSchema.url, headers: config.proxy.headers }, plugins: (ctx) => [ useCustomFetch((url, options, context, info) => { return remoteSchema.yoga!.fetch( @@ -181,7 +185,10 @@ export function createGatewayTester< const runtimeExecute = buildHTTPExecutor({ endpoint: 'http://gateway/graphql', fetch: runtime.fetch, - headers: (execReq) => execReq?.rootValue.headers, + headers: (execReq) => ({ + ...config, + ...execReq?.rootValue.headers, + }), }); return { @@ -230,7 +237,7 @@ function buildRemoteSchema( ? config.yoga?.(schema) : createYoga({ maskedErrors: false, - logging: false, + logging: isDebug(), ...config.yoga, schema, }); From 533f2a0740d868986948818d0c84a7d45affcdc0 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 16:48:07 +0100 Subject: [PATCH 32/35] headers only for proxy --- packages/testing/src/createGatewayTester.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts index 6903dfc65..745b99809 100644 --- a/packages/testing/src/createGatewayTester.ts +++ b/packages/testing/src/createGatewayTester.ts @@ -185,10 +185,7 @@ export function createGatewayTester< const runtimeExecute = buildHTTPExecutor({ endpoint: 'http://gateway/graphql', fetch: runtime.fetch, - headers: (execReq) => ({ - ...config, - ...execReq?.rootValue.headers, - }), + headers: (execReq) => execReq?.rootValue.headers, }); return { From 80cf1f9fe885b3e558df38efdc962ff08ef6f271 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 16:52:07 +0100 Subject: [PATCH 33/35] up tim --- .../runtime/tests/upstream-timeout.test.ts | 70 ++++++------------- 1 file changed, 23 insertions(+), 47 deletions(-) diff --git a/packages/runtime/tests/upstream-timeout.test.ts b/packages/runtime/tests/upstream-timeout.test.ts index b613c7928..902ad44b3 100644 --- a/packages/runtime/tests/upstream-timeout.test.ts +++ b/packages/runtime/tests/upstream-timeout.test.ts @@ -1,9 +1,5 @@ -import { - createGatewayRuntime, - useCustomFetch, -} from '@graphql-hive/gateway-runtime'; +import { createGatewayTester } from '@graphql-hive/gateway-testing'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; -import { MeshFetch } from '@graphql-mesh/types'; import { createDeferred } from '@graphql-tools/utils'; import { createDisposableServer } from '@internal/testing'; import { createSchema, createYoga } from 'graphql-yoga'; @@ -12,32 +8,24 @@ import { describe, expect, it } from 'vitest'; describe('Upstream Timeout', () => { it('times out based on factory function', async () => { const greetingsDeferred = createDeferred(); - const upstreamSchema = createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello: String - } - `, - resolvers: { - Query: { - hello: () => greetingsDeferred.promise, - }, - }, - }); - await using upstreamServer = createYoga({ - schema: upstreamSchema, - }); - await using gateway = createGatewayRuntime({ - supergraph: getUnifiedGraphGracefully([ + await using gateway = createGatewayTester({ + subgraphs: [ { name: 'upstream', - schema: upstreamSchema, - url: 'http://localhost:4001/graphql', + schema: { + typeDefs: /* GraphQL */ ` + type Query { + hello: String + } + `, + resolvers: { + Query: { + hello: () => greetingsDeferred.promise, + }, + }, + }, }, - ]), - plugins() { - return [useCustomFetch(upstreamServer.fetch as MeshFetch)]; - }, + ], upstreamTimeout({ subgraphName }) { if (subgraphName === 'upstream') { return 1000; @@ -45,21 +33,15 @@ describe('Upstream Timeout', () => { throw new Error('Unexpected subgraph'); }, }); - const res = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: /* GraphQL */ ` query { hello } `, }), - }); - const resJson = await res.json(); - expect(resJson).toEqual({ + ).resolves.toEqual({ data: { hello: null, }, @@ -91,7 +73,7 @@ describe('Upstream Timeout', () => { schema: upstreamSchema, }); await using upstreamHttpServer = await createDisposableServer(upstreamYoga); - await using gateway = createGatewayRuntime({ + await using gateway = createGatewayTester({ supergraph: getUnifiedGraphGracefully([ { name: 'upstream', @@ -101,21 +83,15 @@ describe('Upstream Timeout', () => { ]), upstreamTimeout: 10_000, }); - const res = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + await expect( + gateway.execute({ query: /* GraphQL */ ` query { hello } `, }), - }); - const resJson = await res.json(); - expect(resJson).toEqual({ + ).resolves.toEqual({ data: { hello: 'Hello, World!', }, From 23e982841cdfbdc03b5db22e3f02935fd216b63b Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 16:59:59 +0100 Subject: [PATCH 34/35] tester in index because why not --- packages/testing/src/createGatewayTester.ts | 249 ------------------- packages/testing/src/index.ts | 250 +++++++++++++++++++- 2 files changed, 249 insertions(+), 250 deletions(-) delete mode 100644 packages/testing/src/createGatewayTester.ts diff --git a/packages/testing/src/createGatewayTester.ts b/packages/testing/src/createGatewayTester.ts deleted file mode 100644 index 745b99809..000000000 --- a/packages/testing/src/createGatewayTester.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { buildSubgraphSchema } from '@apollo/subgraph'; -import { - createGatewayRuntime, - GatewayConfigBase, - GatewayConfigSupergraph, - GatewayPlugin, - GatewayRuntime, - UnifiedGraphConfig, - useCustomFetch, -} from '@graphql-hive/gateway-runtime'; -import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; -import { buildHTTPExecutor } from '@graphql-tools/executor-http'; -import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; -import { isDebug } from '~internal/env'; -import { - GraphQLFieldResolver, - GraphQLScalarType, - parse, - type GraphQLSchema, -} from 'graphql'; -import { - createYoga, - DisposableSymbols, - YogaServerOptions, - type YogaServerInstance, -} from 'graphql-yoga'; - -/** Thanks @apollo/subgraph for not re-exporting this! */ -export interface GraphQLResolverMap { - [typeName: string]: - | { - [fieldName: string]: - | GraphQLFieldResolver - | { - requires?: string; - resolve?: GraphQLFieldResolver; - subscribe?: GraphQLFieldResolver; - }; - } - | GraphQLScalarType - | { - [enumValue: string]: string | number; - }; -} - -export type GatewayTesterRemoteSchemaConfigYoga = - | Exclude, 'schema'> - | ((schema: GraphQLSchema) => YogaServerInstance); - -export interface GatewayTesterRemoteSchemaConfig { - /** The name of the remote schema / subgraph / proxied server. */ - name: string; - /** The remote schema. */ - schema: GraphQLSchema | { typeDefs: string; resolvers?: GraphQLResolverMap }; - /** The hostname of the remote schema. URL will become `http://${host}${yoga.graphqlEndpoint}`. */ - host?: string; - /** An optional GraphQL Yoga server instance that runs the {@link schema built schema}. */ - yoga?: GatewayTesterRemoteSchemaConfigYoga; -} - -export type GatewayTesterConfig< - TContext extends Record = Record, -> = - | ({ - // gateway - supergraph: UnifiedGraphConfig; - } & Omit, 'supergraph'>) - | ({ - // gateway (composes subgraphs) - subgraphs: - | GatewayTesterRemoteSchemaConfig[] - | (() => GatewayTesterRemoteSchemaConfig[]); - } & Omit, 'supergraph'>) - | ({ - // proxy - proxy: GatewayTesterRemoteSchemaConfig & { - /** Additional headers to be sent to the remote schema on every request. */ - headers?: Record; - }; - } & GatewayConfigBase); -// TODO: subgraph mode - -export interface GatewayTester< - TContext extends Record = Record, -> extends AsyncDisposable { - runtime: GatewayRuntime; - fetch: typeof fetch; - execute(args: { - query: string; - variables?: Record; - operationName?: string; - extensions?: Record; - headers?: Record; - }): Promise>>; - dispose(): Promise; -} - -export function createGatewayTester< - TContext extends Record = Record, ->(config: GatewayTesterConfig): GatewayTester { - let runtime: GatewayRuntime; - if ('supergraph' in config) { - // use supergraph - runtime = createGatewayRuntime({ - maskedErrors: false, - logging: isDebug(), - ...config, - }); - } else if ('subgraphs' in config) { - // compose subgraphs - const subgraphsConfig = config.subgraphs; - function buildSubgraphs() { - subgraphsRef.ref = ( - typeof subgraphsConfig === 'function' - ? subgraphsConfig() - : subgraphsConfig - ).reduce( - (acc, subgraph) => { - const remoteSchema = buildRemoteSchema(subgraph); - return { - ...acc, - [remoteSchema.name]: remoteSchema, - }; - }, - {} as { [subgraphName: string]: GatewayTesterRemoteSchema }, - ); - return Object.values(subgraphsRef.ref); - } - const subgraphsRef = { - ref: null as Record | null, - }; - runtime = createGatewayRuntime({ - maskedErrors: false, - logging: isDebug(), - ...config, - supergraph: - typeof config.subgraphs === 'function' - ? () => getUnifiedGraphGracefully(buildSubgraphs()) - : getUnifiedGraphGracefully(buildSubgraphs()), - plugins: (ctx) => [ - { - onFetch({ executionRequest, setFetchFn }) { - const subgraphName = executionRequest?.subgraphName; - if (!subgraphName) { - return; - } - if (!subgraphsRef.ref) { - throw new Error('Subgraphs are not built yet'); - } - const subgraph = subgraphsRef.ref[subgraphName]; - if (!subgraph) { - throw new Error(`Subgraph "${subgraphName}" not found`); - } - setFetchFn(subgraph.yoga.fetch); - }, - } as GatewayPlugin, - ...(config.plugins?.(ctx) || []), - ], - }); - } else if ('proxy' in config) { - // build remote schema and proxy - const remoteSchema = buildRemoteSchema(config.proxy); - runtime = createGatewayRuntime({ - maskedErrors: false, - logging: isDebug(), - ...config, - proxy: { endpoint: remoteSchema.url, headers: config.proxy.headers }, - plugins: (ctx) => [ - useCustomFetch((url, options, context, info) => { - return remoteSchema.yoga!.fetch( - // @ts-expect-error TODO: url can be a string, not only an instance of URL - url, - options, - context, - info, - ); - }), - ...(config.plugins?.(ctx) || []), - ], - }); - } else { - throw new Error('Unsupported gateway tester configuration'); - } - - const runtimeExecute = buildHTTPExecutor({ - endpoint: 'http://gateway/graphql', - fetch: runtime.fetch, - headers: (execReq) => execReq?.rootValue.headers, - }); - - return { - runtime, - // @ts-expect-error native and whatwg-node fetch has conflicts - fetch: runtime.fetch, - async execute(args) { - return runtimeExecute({ - document: parse(args.query), - variables: args.variables, - operationName: args.operationName, - extensions: args.extensions, - rootValue: { headers: args.headers }, - }); - }, - [DisposableSymbols.asyncDispose]() { - return runtime[DisposableSymbols.asyncDispose](); - }, - async dispose() { - await runtime.dispose(); - }, - }; -} - -interface GatewayTesterRemoteSchema { - name: string; - url: string; - schema: GraphQLSchema; - yoga: YogaServerInstance; -} - -function buildRemoteSchema( - config: GatewayTesterRemoteSchemaConfig, -): GatewayTesterRemoteSchema { - const schema = - 'typeDefs' in config.schema - ? buildSubgraphSchema([ - { - ...config.schema, - typeDefs: parse(config.schema.typeDefs), - }, - ]) - : config.schema; - const yoga = - typeof config.yoga === 'function' - ? config.yoga?.(schema) - : createYoga({ - maskedErrors: false, - logging: isDebug(), - ...config.yoga, - schema, - }); - const host = config.host || config.name; - const url = `http://${host}${yoga.graphqlEndpoint}`; - return { - name: config.name, - url, - schema, - yoga, - }; -} diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 117f9fa8f..745b99809 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -1 +1,249 @@ -export * from './createGatewayTester'; +import { buildSubgraphSchema } from '@apollo/subgraph'; +import { + createGatewayRuntime, + GatewayConfigBase, + GatewayConfigSupergraph, + GatewayPlugin, + GatewayRuntime, + UnifiedGraphConfig, + useCustomFetch, +} from '@graphql-hive/gateway-runtime'; +import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; +import { buildHTTPExecutor } from '@graphql-tools/executor-http'; +import type { ExecutionResult, MaybeAsyncIterable } from '@graphql-tools/utils'; +import { isDebug } from '~internal/env'; +import { + GraphQLFieldResolver, + GraphQLScalarType, + parse, + type GraphQLSchema, +} from 'graphql'; +import { + createYoga, + DisposableSymbols, + YogaServerOptions, + type YogaServerInstance, +} from 'graphql-yoga'; + +/** Thanks @apollo/subgraph for not re-exporting this! */ +export interface GraphQLResolverMap { + [typeName: string]: + | { + [fieldName: string]: + | GraphQLFieldResolver + | { + requires?: string; + resolve?: GraphQLFieldResolver; + subscribe?: GraphQLFieldResolver; + }; + } + | GraphQLScalarType + | { + [enumValue: string]: string | number; + }; +} + +export type GatewayTesterRemoteSchemaConfigYoga = + | Exclude, 'schema'> + | ((schema: GraphQLSchema) => YogaServerInstance); + +export interface GatewayTesterRemoteSchemaConfig { + /** The name of the remote schema / subgraph / proxied server. */ + name: string; + /** The remote schema. */ + schema: GraphQLSchema | { typeDefs: string; resolvers?: GraphQLResolverMap }; + /** The hostname of the remote schema. URL will become `http://${host}${yoga.graphqlEndpoint}`. */ + host?: string; + /** An optional GraphQL Yoga server instance that runs the {@link schema built schema}. */ + yoga?: GatewayTesterRemoteSchemaConfigYoga; +} + +export type GatewayTesterConfig< + TContext extends Record = Record, +> = + | ({ + // gateway + supergraph: UnifiedGraphConfig; + } & Omit, 'supergraph'>) + | ({ + // gateway (composes subgraphs) + subgraphs: + | GatewayTesterRemoteSchemaConfig[] + | (() => GatewayTesterRemoteSchemaConfig[]); + } & Omit, 'supergraph'>) + | ({ + // proxy + proxy: GatewayTesterRemoteSchemaConfig & { + /** Additional headers to be sent to the remote schema on every request. */ + headers?: Record; + }; + } & GatewayConfigBase); +// TODO: subgraph mode + +export interface GatewayTester< + TContext extends Record = Record, +> extends AsyncDisposable { + runtime: GatewayRuntime; + fetch: typeof fetch; + execute(args: { + query: string; + variables?: Record; + operationName?: string; + extensions?: Record; + headers?: Record; + }): Promise>>; + dispose(): Promise; +} + +export function createGatewayTester< + TContext extends Record = Record, +>(config: GatewayTesterConfig): GatewayTester { + let runtime: GatewayRuntime; + if ('supergraph' in config) { + // use supergraph + runtime = createGatewayRuntime({ + maskedErrors: false, + logging: isDebug(), + ...config, + }); + } else if ('subgraphs' in config) { + // compose subgraphs + const subgraphsConfig = config.subgraphs; + function buildSubgraphs() { + subgraphsRef.ref = ( + typeof subgraphsConfig === 'function' + ? subgraphsConfig() + : subgraphsConfig + ).reduce( + (acc, subgraph) => { + const remoteSchema = buildRemoteSchema(subgraph); + return { + ...acc, + [remoteSchema.name]: remoteSchema, + }; + }, + {} as { [subgraphName: string]: GatewayTesterRemoteSchema }, + ); + return Object.values(subgraphsRef.ref); + } + const subgraphsRef = { + ref: null as Record | null, + }; + runtime = createGatewayRuntime({ + maskedErrors: false, + logging: isDebug(), + ...config, + supergraph: + typeof config.subgraphs === 'function' + ? () => getUnifiedGraphGracefully(buildSubgraphs()) + : getUnifiedGraphGracefully(buildSubgraphs()), + plugins: (ctx) => [ + { + onFetch({ executionRequest, setFetchFn }) { + const subgraphName = executionRequest?.subgraphName; + if (!subgraphName) { + return; + } + if (!subgraphsRef.ref) { + throw new Error('Subgraphs are not built yet'); + } + const subgraph = subgraphsRef.ref[subgraphName]; + if (!subgraph) { + throw new Error(`Subgraph "${subgraphName}" not found`); + } + setFetchFn(subgraph.yoga.fetch); + }, + } as GatewayPlugin, + ...(config.plugins?.(ctx) || []), + ], + }); + } else if ('proxy' in config) { + // build remote schema and proxy + const remoteSchema = buildRemoteSchema(config.proxy); + runtime = createGatewayRuntime({ + maskedErrors: false, + logging: isDebug(), + ...config, + proxy: { endpoint: remoteSchema.url, headers: config.proxy.headers }, + plugins: (ctx) => [ + useCustomFetch((url, options, context, info) => { + return remoteSchema.yoga!.fetch( + // @ts-expect-error TODO: url can be a string, not only an instance of URL + url, + options, + context, + info, + ); + }), + ...(config.plugins?.(ctx) || []), + ], + }); + } else { + throw new Error('Unsupported gateway tester configuration'); + } + + const runtimeExecute = buildHTTPExecutor({ + endpoint: 'http://gateway/graphql', + fetch: runtime.fetch, + headers: (execReq) => execReq?.rootValue.headers, + }); + + return { + runtime, + // @ts-expect-error native and whatwg-node fetch has conflicts + fetch: runtime.fetch, + async execute(args) { + return runtimeExecute({ + document: parse(args.query), + variables: args.variables, + operationName: args.operationName, + extensions: args.extensions, + rootValue: { headers: args.headers }, + }); + }, + [DisposableSymbols.asyncDispose]() { + return runtime[DisposableSymbols.asyncDispose](); + }, + async dispose() { + await runtime.dispose(); + }, + }; +} + +interface GatewayTesterRemoteSchema { + name: string; + url: string; + schema: GraphQLSchema; + yoga: YogaServerInstance; +} + +function buildRemoteSchema( + config: GatewayTesterRemoteSchemaConfig, +): GatewayTesterRemoteSchema { + const schema = + 'typeDefs' in config.schema + ? buildSubgraphSchema([ + { + ...config.schema, + typeDefs: parse(config.schema.typeDefs), + }, + ]) + : config.schema; + const yoga = + typeof config.yoga === 'function' + ? config.yoga?.(schema) + : createYoga({ + maskedErrors: false, + logging: isDebug(), + ...config.yoga, + schema, + }); + const host = config.host || config.name; + const url = `http://${host}${yoga.graphqlEndpoint}`; + return { + name: config.name, + url, + schema, + yoga, + }; +} From f555607556652b06a7b1e5c992d6d87454afc1e5 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Mon, 27 Oct 2025 17:19:47 +0100 Subject: [PATCH 35/35] unused --- packages/plugins/prometheus/tests/prometheus.spec.ts | 1 - packages/runtime/tests/subscriptions.test.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/plugins/prometheus/tests/prometheus.spec.ts b/packages/plugins/prometheus/tests/prometheus.spec.ts index 167ad47fb..a07cdb77f 100644 --- a/packages/plugins/prometheus/tests/prometheus.spec.ts +++ b/packages/plugins/prometheus/tests/prometheus.spec.ts @@ -3,7 +3,6 @@ import { createGatewayTester } from '@graphql-hive/gateway-testing'; import InMemoryLRUCache from '@graphql-mesh/cache-inmemory-lru'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { createDefaultExecutor } from '@graphql-mesh/transport-common'; -import { isDebug } from '@internal/testing'; import { createSchema } from 'graphql-yoga'; import { Registry, register as registry } from 'prom-client'; import { beforeEach, describe, expect, it } from 'vitest'; diff --git a/packages/runtime/tests/subscriptions.test.ts b/packages/runtime/tests/subscriptions.test.ts index d6d0ee48c..6044aff60 100644 --- a/packages/runtime/tests/subscriptions.test.ts +++ b/packages/runtime/tests/subscriptions.test.ts @@ -1,6 +1,5 @@ import { createGatewayTester } from '@graphql-hive/gateway-testing'; import { type MaybePromise } from '@graphql-tools/utils'; -import { isDebug } from '@internal/testing'; import { DisposableSymbols } from '@whatwg-node/disposablestack'; import { createClient as createSSEClient } from 'graphql-sse'; import { createSchema, Repeater } from 'graphql-yoga';