console/integration-tests/tests/cli/introspect.spec.ts
Kamil Kisiela 580918de79
feat: introspection of subgraph schema in Hive CLI (#5853)
Co-authored-by: jdolle <1841898+jdolle@users.noreply.github.com>
Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
2026-03-17 14:18:25 +01:00

261 lines
6.6 KiB
TypeScript

import { AddressInfo } from 'node:net';
import { parse } from 'graphql';
import { createSchema, createYoga } from 'graphql-yoga';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { createServer } from '@hive/service-common';
import { introspect } from '../../testkit/cli';
async function createHTTPGraphQLServer() {
const server = await createServer({
sentryErrorHandler: false,
log: {
requests: false,
level: 'silent',
},
name: '',
});
const yoga = createYoga({
logging: false,
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`,
}),
});
const yogaProtected = createYoga({
graphqlEndpoint: '/graphql-protected',
logging: false,
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`,
}),
plugins: [
{
onRequest(ctx) {
if (ctx.request.headers.get('x-auth') !== 'AUTH_AUTH_BABY') {
ctx.endResponse(new Response('Nah', { status: 403 }));
return;
}
},
},
],
});
const yogaFederation = createYoga({
graphqlEndpoint: '/graphql-federation',
logging: false,
schema: buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
extend type Query {
me: User
user(id: ID!): User
users: [User]
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
`),
}),
});
const yogaFederationProtected = createYoga({
graphqlEndpoint: '/graphql-federation-protected',
logging: false,
schema: buildSubgraphSchema({
typeDefs: parse(/* GraphQL */ `
extend type Query {
me: User
user(id: ID!): User
users: [User]
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
`),
}),
plugins: [
{
onRequest(ctx) {
if (ctx.request.headers.get('x-auth') !== 'AUTH_AUTH_BABY') {
ctx.endResponse(new Response('Nah', { status: 403 }));
return;
}
},
},
],
});
server.route({
// Bind to the Yoga's endpoint to avoid rendering on any path
url: yoga.graphqlEndpoint,
method: ['GET', 'POST', 'OPTIONS'],
handler: (req, reply) => yoga.handleNodeRequestAndResponse(req, reply),
});
server.route({
// Bind to the Yoga's endpoint to avoid rendering on any path
url: yogaProtected.graphqlEndpoint,
method: ['GET', 'POST', 'OPTIONS'],
handler: (req, reply) => yogaProtected.handleNodeRequestAndResponse(req, reply),
});
server.route({
// Bind to the Yoga's endpoint to avoid rendering on any path
url: yogaFederation.graphqlEndpoint,
method: ['GET', 'POST', 'OPTIONS'],
handler: (req, reply) => yogaFederation.handleNodeRequestAndResponse(req, reply),
});
server.route({
// Bind to the Yoga's endpoint to avoid rendering on any path
url: yogaFederationProtected.graphqlEndpoint,
method: ['GET', 'POST', 'OPTIONS'],
handler: (req, reply) => yogaFederationProtected.handleNodeRequestAndResponse(req, reply),
});
await server.listen({
port: 0,
host: '0.0.0.0',
});
return {
url: 'http://localhost:' + (server.server.address() as AddressInfo).port,
[Symbol.asyncDispose]: () => {
server.close();
},
};
}
test.concurrent('can introspect monolith GraphQL service', async ({ expect }) => {
const server = await createHTTPGraphQLServer();
await expect(introspect([server.url + '/graphql'])).resolves.toMatchInlineSnapshot(`
schema {
query: Query
}
type Query {
hello: String!
}
`);
});
test.concurrent('can introspect federation GraphQL service', async ({ expect }) => {
const server = await createHTTPGraphQLServer();
const url = server.url + '/graphql-federation';
const result = await introspect([url]);
const forceResult = await introspect(['-t', 'federation', url]);
expect(result).toEqual(forceResult);
expect(result).toMatchInlineSnapshot(`
directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @external(reason: String) on OBJECT | FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @extends on OBJECT | INTERFACE
type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
extend type Query {
me: User
user(id: ID!): User
users: [User]
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
scalar _FieldSet
scalar _Any
type _Service {
sdl: String
}
union _Entity = User
`);
});
test.concurrent('can introspect protected monolith with header', async ({ expect }) => {
const server = await createHTTPGraphQLServer();
await expect(introspect([server.url + '/graphql-protected', '-H', 'x-auth:AUTH_AUTH_BABY']))
.resolves.toMatchInlineSnapshot(`
schema {
query: Query
}
type Query {
hello: String!
}
`);
});
test.concurrent('can introspect protected federation with header', async ({ expect }) => {
const server = await createHTTPGraphQLServer();
await expect(
introspect([server.url + '/graphql-federation-protected', '-H', 'x-auth:AUTH_AUTH_BABY']),
).resolves.toMatchInlineSnapshot(`
directive @key(fields: _FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
directive @external(reason: String) on OBJECT | FIELD_DEFINITION
directive @tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
directive @extends on OBJECT | INTERFACE
type Query {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service!
}
extend type Query {
me: User
user(id: ID!): User
users: [User]
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
scalar _FieldSet
scalar _Any
type _Service {
sdl: String
}
union _Entity = User
`);
});