import { expect } from 'chai';
import { describe, it } from 'mocha';
import { dedent } from '../../__testUtils__/dedent';
import { invariant } from '../../jsutils/invariant';
import {
assertEnumType,
GraphQLEnumType,
GraphQLObjectType,
} from '../../type/definition';
import {
GraphQLBoolean,
GraphQLFloat,
GraphQLID,
GraphQLInt,
GraphQLString,
} from '../../type/scalars';
import { GraphQLSchema } from '../../type/schema';
import { graphqlSync } from '../../graphql';
import { buildSchema } from '../buildASTSchema';
import { buildClientSchema } from '../buildClientSchema';
import { introspectionFromSchema } from '../introspectionFromSchema';
import { printSchema } from '../printSchema';
function cycleIntrospection(sdlString: string): string {
const serverSchema = buildSchema(sdlString);
const initialIntrospection = introspectionFromSchema(serverSchema);
const clientSchema = buildClientSchema(initialIntrospection);
const secondIntrospection = introspectionFromSchema(clientSchema);
expect(secondIntrospection).to.deep.equal(initialIntrospection);
return printSchema(clientSchema);
}
describe('Type System: build schema from introspection', () => {
it('builds a simple schema', () => {
const sdl = dedent`
"""Simple schema"""
schema {
query: Simple
}
"""This is a simple type"""
type Simple {
"""This is a string field"""
string: String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema without the query type', () => {
const sdl = dedent`
type Query {
foo: String
}
`;
const schema = buildSchema(sdl);
const introspection = introspectionFromSchema(schema);
delete introspection.__schema.queryType;
const clientSchema = buildClientSchema(introspection);
expect(clientSchema.getQueryType()).to.equal(null);
expect(printSchema(clientSchema)).to.equal(sdl);
});
it('builds a simple schema with all operation types', () => {
const sdl = dedent`
schema {
query: QueryType
mutation: MutationType
subscription: SubscriptionType
}
"""This is a simple mutation type"""
type MutationType {
"""Set the string field"""
string: String
}
"""This is a simple query type"""
type QueryType {
"""This is a string field"""
string: String
}
"""This is a simple subscription type"""
type SubscriptionType {
"""This is a string field"""
string: String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('uses built-in scalars when possible', () => {
const sdl = dedent`
scalar CustomScalar
type Query {
int: Int
float: Float
string: String
boolean: Boolean
id: ID
custom: CustomScalar
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
const schema = buildSchema(sdl);
const introspection = introspectionFromSchema(schema);
const clientSchema = buildClientSchema(introspection);
expect(clientSchema.getType('Int')).to.equal(GraphQLInt);
expect(clientSchema.getType('Float')).to.equal(GraphQLFloat);
expect(clientSchema.getType('String')).to.equal(GraphQLString);
expect(clientSchema.getType('Boolean')).to.equal(GraphQLBoolean);
expect(clientSchema.getType('ID')).to.equal(GraphQLID);
const customScalar = schema.getType('CustomScalar');
expect(clientSchema.getType('CustomScalar')).to.not.equal(customScalar);
});
it('includes standard types only if they are used', () => {
const schema = buildSchema(`
type Query {
foo: String
}
`);
const introspection = introspectionFromSchema(schema);
const clientSchema = buildClientSchema(introspection);
expect(clientSchema.getType('Int')).to.equal(undefined);
expect(clientSchema.getType('Float')).to.equal(undefined);
expect(clientSchema.getType('ID')).to.equal(undefined);
});
it('builds a schema with a recursive type reference', () => {
const sdl = dedent`
schema {
query: Recur
}
type Recur {
recur: Recur
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with a circular type reference', () => {
const sdl = dedent`
type Dog {
bestFriend: Human
}
type Human {
bestFriend: Dog
}
type Query {
dog: Dog
human: Human
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with an interface', () => {
const sdl = dedent`
type Dog implements Friendly {
bestFriend: Friendly
}
interface Friendly {
"""The best friend of this friendly thing"""
bestFriend: Friendly
}
type Human implements Friendly {
bestFriend: Friendly
}
type Query {
friendly: Friendly
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with an interface hierarchy', () => {
const sdl = dedent`
type Dog implements Friendly & Named {
bestFriend: Friendly
name: String
}
interface Friendly implements Named {
"""The best friend of this friendly thing"""
bestFriend: Friendly
name: String
}
type Human implements Friendly & Named {
bestFriend: Friendly
name: String
}
interface Named {
name: String
}
type Query {
friendly: Friendly
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with an implicit interface', () => {
const sdl = dedent`
type Dog implements Friendly {
bestFriend: Friendly
}
interface Friendly {
"""The best friend of this friendly thing"""
bestFriend: Friendly
}
type Query {
dog: Dog
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with a union', () => {
const sdl = dedent`
type Dog {
bestFriend: Friendly
}
union Friendly = Dog | Human
type Human {
bestFriend: Friendly
}
type Query {
friendly: Friendly
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with complex field values', () => {
const sdl = dedent`
type Query {
string: String
listOfString: [String]
nonNullString: String!
nonNullListOfString: [String]!
nonNullListOfNonNullString: [String!]!
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with field arguments', () => {
const sdl = dedent`
type Query {
"""A field with a single arg"""
one(
"""This is an int arg"""
intArg: Int
): String
"""A field with a two args"""
two(
"""This is an list of int arg"""
listArg: [Int]
"""This is a required arg"""
requiredArg: Boolean!
): String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with default value on custom scalar field', () => {
const sdl = dedent`
scalar CustomScalar
type Query {
testField(testArg: CustomScalar = "default"): String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with an enum', () => {
const foodEnum = new GraphQLEnumType({
name: 'Food',
description: 'Varieties of food stuffs',
values: {
VEGETABLES: {
description: 'Foods that are vegetables.',
value: 1,
},
FRUITS: {
value: 2,
},
OILS: {
value: 3,
deprecationReason: 'Too fatty',
},
},
});
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'EnumFields',
fields: {
food: {
description: 'Repeats the arg you give it',
type: foodEnum,
args: {
kind: {
description: 'what kind of food?',
type: foodEnum,
},
},
},
},
}),
});
const introspection = introspectionFromSchema(schema);
const clientSchema = buildClientSchema(introspection);
const secondIntrospection = introspectionFromSchema(clientSchema);
expect(secondIntrospection).to.deep.equal(introspection);
const clientFoodEnum = assertEnumType(clientSchema.getType('Food'));
expect(clientFoodEnum.getValues()).to.deep.equal([
{
name: 'VEGETABLES',
description: 'Foods that are vegetables.',
value: 'VEGETABLES',
deprecationReason: null,
extensions: {},
astNode: undefined,
},
{
name: 'FRUITS',
description: null,
value: 'FRUITS',
deprecationReason: null,
extensions: {},
astNode: undefined,
},
{
name: 'OILS',
description: null,
value: 'OILS',
deprecationReason: 'Too fatty',
extensions: {},
astNode: undefined,
},
]);
});
it('builds a schema with an input object', () => {
const sdl = dedent`
"""An input address"""
input Address {
"""What street is this address?"""
street: String!
"""The city the address is within?"""
city: String!
"""The country (blank will assume USA)."""
country: String = "USA"
}
type Query {
"""Get a geocode from an address"""
geocode(
"""The address to lookup"""
address: Address
): String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with field arguments with default values', () => {
const sdl = dedent`
input Geo {
lat: Float
lon: Float
}
type Query {
defaultInt(intArg: Int = 30): String
defaultList(listArg: [Int] = [1, 2, 3]): String
defaultObject(objArg: Geo = {lat: 37.485, lon: -122.148}): String
defaultNull(intArg: Int = null): String
noDefault(intArg: Int): String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with custom directives', () => {
const sdl = dedent`
"""This is a custom directive"""
directive @customDirective repeatable on FIELD
type Query {
string: String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema without directives', () => {
const sdl = dedent`
type Query {
string: String
}
`;
const schema = buildSchema(sdl);
const introspection = introspectionFromSchema(schema);
delete introspection.__schema.directives;
const clientSchema = buildClientSchema(introspection);
expect(schema.getDirectives()).to.have.lengthOf.above(0);
expect(clientSchema.getDirectives()).to.deep.equal([]);
expect(printSchema(clientSchema)).to.equal(sdl);
});
it('builds a schema aware of deprecation', () => {
const sdl = dedent`
directive @someDirective(
"""This is a shiny new argument"""
shinyArg: SomeInputObject
"""This was our design mistake :("""
oldArg: String @deprecated(reason: "Use shinyArg")
) on QUERY
enum Color {
"""So rosy"""
RED
"""So grassy"""
GREEN
"""So calming"""
BLUE
"""So sickening"""
MAUVE @deprecated(reason: "No longer in fashion")
}
input SomeInputObject {
"""Nothing special about it, just deprecated for some unknown reason"""
oldField: String @deprecated(reason: "Don't use it, use newField instead!")
"""Same field but with a new name"""
newField: String
}
type Query {
"""This is a shiny string field"""
shinyString: String
"""This is a deprecated string field"""
deprecatedString: String @deprecated(reason: "Use shinyString")
"""Color of a week"""
color: Color
"""Some random field"""
someField(
"""This is a shiny new argument"""
shinyArg: SomeInputObject
"""This was our design mistake :("""
oldArg: String @deprecated(reason: "Use shinyArg")
): String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with empty deprecation reasons', () => {
const sdl = dedent`
directive @someDirective(someArg: SomeInputObject @deprecated(reason: "")) on QUERY
type Query {
someField(someArg: SomeInputObject @deprecated(reason: "")): SomeEnum @deprecated(reason: "")
}
input SomeInputObject {
someInputField: String @deprecated(reason: "")
}
enum SomeEnum {
SOME_VALUE @deprecated(reason: "")
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with specifiedBy url', () => {
const sdl = dedent`
scalar Foo @specifiedBy(url: "https://example.com/foo_spec")
type Query {
foo: Foo
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('builds a schema with @oneOf directive', () => {
const sdl = dedent`
type Query {
someField(someArg: SomeInputObject): String
}
input SomeInputObject @oneOf {
someInputField1: String
someInputField2: String
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
it('can use client schema for limited execution', () => {
const schema = buildSchema(`
scalar CustomScalar
type Query {
foo(custom1: CustomScalar, custom2: CustomScalar): String
}
`);
const introspection = introspectionFromSchema(schema);
const clientSchema = buildClientSchema(introspection);
const result = graphqlSync({
schema: clientSchema,
source:
'query Limited($v: CustomScalar) { foo(custom1: 123, custom2: $v) }',
rootValue: { foo: 'bar', unused: 'value' },
variableValues: { v: 'baz' },
});
expect(result.data).to.deep.equal({ foo: 'bar' });
});
it('can build invalid schema', () => {
const schema = buildSchema('type Query', { assumeValid: true });
const introspection = introspectionFromSchema(schema);
const clientSchema = buildClientSchema(introspection, {
assumeValid: true,
});
expect(clientSchema.toConfig().assumeValid).to.equal(true);
});
describe('throws when given invalid introspection', () => {
const dummySchema = buildSchema(`
type Query {
foo(bar: String): String
}
interface SomeInterface {
foo: String
}
union SomeUnion = Query
enum SomeEnum { FOO }
input SomeInputObject {
foo: String
}
directive @SomeDirective on QUERY
`);
it('throws when introspection is missing __schema property', () => {
expect(() => buildClientSchema(null)).to.throw(
'Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: null.',
);
expect(() => buildClientSchema({})).to.throw(
'Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: {}.',
);
});
it('throws when referenced unknown type', () => {
const introspection = introspectionFromSchema(dummySchema);
introspection.__schema.types = introspection.__schema.types.filter(
({ name }) => name !== 'Query',
);
expect(() => buildClientSchema(introspection)).to.throw(
'Invalid or incomplete schema, unknown type: Query. Ensure that a full introspection query is used in order to build a client schema.',
);
});
it('throws when missing definition for one of the standard scalars', () => {
const schema = buildSchema(`
type Query {
foo: Float
}
`);
const introspection = introspectionFromSchema(schema);
introspection.__schema.types = introspection.__schema.types.filter(
({ name }) => name !== 'Float',
);
expect(() => buildClientSchema(introspection)).to.throw(
'Invalid or incomplete schema, unknown type: Float. Ensure that a full introspection query is used in order to build a client schema.',
);
});
it('throws when type reference is missing name', () => {
const introspection = introspectionFromSchema(dummySchema);
expect(introspection).to.have.nested.property('__schema.queryType.name');
delete introspection.__schema.queryType.name;
expect(() => buildClientSchema(introspection)).to.throw(
'Unknown type reference: { kind: "OBJECT" }.',
);
});
it('throws when missing kind', () => {
const introspection = introspectionFromSchema(dummySchema);
const queryTypeIntrospection = introspection.__schema.types.find(
({ name }) => name === 'Query',
);
invariant(queryTypeIntrospection?.kind === 'OBJECT');
delete queryTypeIntrospection.kind;
expect(() => buildClientSchema(introspection)).to.throw(
/Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: { name: "Query", .* }\./,
);
});
it('throws when missing interfaces', () => {
const introspection = introspectionFromSchema(dummySchema);
const queryTypeIntrospection = introspection.__schema.types.find(
({ name }) => name === 'Query',
);
expect(queryTypeIntrospection).to.have.property('interfaces');
invariant(queryTypeIntrospection?.kind === 'OBJECT');
delete queryTypeIntrospection.interfaces;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing interfaces: { kind: "OBJECT", name: "Query", .* }\./,
);
});
it('Legacy support for interfaces with null as interfaces field', () => {
const introspection = introspectionFromSchema(dummySchema);
const someInterfaceIntrospection = introspection.__schema.types.find(
({ name }) => name === 'SomeInterface',
);
invariant(someInterfaceIntrospection?.kind === 'INTERFACE');
someInterfaceIntrospection.interfaces = null;
const clientSchema = buildClientSchema(introspection);
expect(printSchema(clientSchema)).to.equal(printSchema(dummySchema));
});
it('throws when missing fields', () => {
const introspection = introspectionFromSchema(dummySchema);
const queryTypeIntrospection = introspection.__schema.types.find(
({ name }) => name === 'Query',
);
invariant(queryTypeIntrospection?.kind === 'OBJECT');
delete queryTypeIntrospection.fields;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing fields: { kind: "OBJECT", name: "Query", .* }\./,
);
});
it('throws when missing field args', () => {
const introspection = introspectionFromSchema(dummySchema);
const queryTypeIntrospection = introspection.__schema.types.find(
({ name }) => name === 'Query',
);
invariant(queryTypeIntrospection?.kind === 'OBJECT');
delete queryTypeIntrospection.fields[0].args;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing field args: { name: "foo", .* }\./,
);
});
it('throws when output type is used as an arg type', () => {
const introspection = introspectionFromSchema(dummySchema);
const queryTypeIntrospection = introspection.__schema.types.find(
({ name }) => name === 'Query',
);
invariant(queryTypeIntrospection?.kind === 'OBJECT');
const argType = queryTypeIntrospection.fields[0].args[0].type;
invariant(argType.kind === 'SCALAR');
expect(argType).to.have.property('name', 'String');
argType.name = 'SomeUnion';
expect(() => buildClientSchema(introspection)).to.throw(
'Introspection must provide input type for arguments, but received: SomeUnion.',
);
});
it('throws when input type is used as a field type', () => {
const introspection = introspectionFromSchema(dummySchema);
const queryTypeIntrospection = introspection.__schema.types.find(
({ name }) => name === 'Query',
);
invariant(queryTypeIntrospection?.kind === 'OBJECT');
const fieldType = queryTypeIntrospection.fields[0].type;
invariant(fieldType.kind === 'SCALAR');
expect(fieldType).to.have.property('name', 'String');
fieldType.name = 'SomeInputObject';
expect(() => buildClientSchema(introspection)).to.throw(
'Introspection must provide output type for fields, but received: SomeInputObject.',
);
});
it('throws when missing possibleTypes', () => {
const introspection = introspectionFromSchema(dummySchema);
const someUnionIntrospection = introspection.__schema.types.find(
({ name }) => name === 'SomeUnion',
);
invariant(someUnionIntrospection?.kind === 'UNION');
delete someUnionIntrospection.possibleTypes;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing possibleTypes: { kind: "UNION", name: "SomeUnion",.* }\./,
);
});
it('throws when missing enumValues', () => {
const introspection = introspectionFromSchema(dummySchema);
const someEnumIntrospection = introspection.__schema.types.find(
({ name }) => name === 'SomeEnum',
);
invariant(someEnumIntrospection?.kind === 'ENUM');
delete someEnumIntrospection.enumValues;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing enumValues: { kind: "ENUM", name: "SomeEnum", .* }\./,
);
});
it('throws when missing inputFields', () => {
const introspection = introspectionFromSchema(dummySchema);
const someInputObjectIntrospection = introspection.__schema.types.find(
({ name }) => name === 'SomeInputObject',
);
invariant(someInputObjectIntrospection?.kind === 'INPUT_OBJECT');
delete someInputObjectIntrospection.inputFields;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing inputFields: { kind: "INPUT_OBJECT", name: "SomeInputObject", .* }\./,
);
});
it('throws when missing directive locations', () => {
const introspection = introspectionFromSchema(dummySchema);
const someDirectiveIntrospection = introspection.__schema.directives[0];
expect(someDirectiveIntrospection).to.deep.include({
name: 'SomeDirective',
locations: ['QUERY'],
});
delete someDirectiveIntrospection.locations;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing directive locations: { name: "SomeDirective", .* }\./,
);
});
it('throws when missing directive args', () => {
const introspection = introspectionFromSchema(dummySchema);
const someDirectiveIntrospection = introspection.__schema.directives[0];
expect(someDirectiveIntrospection).to.deep.include({
name: 'SomeDirective',
args: [],
});
delete someDirectiveIntrospection.args;
expect(() => buildClientSchema(introspection)).to.throw(
/Introspection result missing directive args: { name: "SomeDirective", .* }\./,
);
});
});
describe('very deep decorators are not supported', () => {
it('fails on very deep (> 8 levels) lists', () => {
const schema = buildSchema(`
type Query {
foo: [[[[[[[[[[String]]]]]]]]]]
}
`);
const introspection = introspectionFromSchema(schema);
expect(() => buildClientSchema(introspection)).to.throw(
'Decorated type deeper than introspection query.',
);
});
it('fails on a very deep (> 8 levels) non-null', () => {
const schema = buildSchema(`
type Query {
foo: [[[[[String!]!]!]!]!]
}
`);
const introspection = introspectionFromSchema(schema);
expect(() => buildClientSchema(introspection)).to.throw(
'Decorated type deeper than introspection query.',
);
});
it('succeeds on deep (<= 8 levels) types', () => {
const sdl = dedent`
type Query {
foo: [[[[String!]!]!]!]!
}
`;
expect(cycleIntrospection(sdl)).to.equal(sdl);
});
});
describe('prevents infinite recursion on invalid introspection', () => {
it('recursive interfaces', () => {
const sdl = `
type Query {
foo: Foo
}
type Foo implements Foo {
foo: String
}
`;
const schema = buildSchema(sdl, { assumeValid: true });
const introspection = introspectionFromSchema(schema);
const fooIntrospection = introspection.__schema.types.find(
(type) => type.name === 'Foo',
);
expect(fooIntrospection).to.deep.include({
name: 'Foo',
interfaces: [{ kind: 'OBJECT', name: 'Foo', ofType: null }],
});
expect(() => buildClientSchema(introspection)).to.throw(
'Expected Foo to be a GraphQL Interface type.',
);
});
it('recursive union', () => {
const sdl = `
type Query {
foo: Foo
}
union Foo = Foo
`;
const schema = buildSchema(sdl, { assumeValid: true });
const introspection = introspectionFromSchema(schema);
const fooIntrospection = introspection.__schema.types.find(
(type) => type.name === 'Foo',
);
expect(fooIntrospection).to.deep.include({
name: 'Foo',
possibleTypes: [{ kind: 'UNION', name: 'Foo', ofType: null }],
});
expect(() => buildClientSchema(introspection)).to.throw(
'Expected Foo to be a GraphQL Object type.',
);
});
});
});