From 48e158732c82ea1e877417ae41f4b39638ef98c8 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 4 Jan 2020 22:32:16 +0700 Subject: [PATCH] Improve coverage of the entire codebase --- src/__fixtures__/schema-kitchen-sink.graphql | 1 + src/__tests__/graphql-test.js | 18 ++ src/__tests__/starWarsQuery-test.js | 15 +- src/execution/__tests__/abstract-test.js | 40 +++++ src/execution/__tests__/executor-test.js | 60 ++++++- src/execution/__tests__/mutations-test.js | 9 + src/execution/__tests__/nonnull-test.js | 7 +- .../__tests__/union-interface-test.js | 41 +++++ src/execution/__tests__/variables-test.js | 18 +- src/jsutils/__tests__/invariant-test.js | 16 ++ src/language/__tests__/predicates-test.js | 134 ++++++++++++++ src/language/__tests__/schema-parser-test.js | 53 ++++-- src/language/__tests__/schema-printer-test.js | 5 +- src/language/__tests__/source-test.js | 20 +++ src/language/__tests__/visitor-test.js | 125 ++++++++++++- src/subscription/__tests__/subscribe-test.js | 11 ++ src/type/__tests__/definition-test.js | 49 ++++- src/type/__tests__/enumType-test.js | 7 +- src/type/__tests__/introspection-test.js | 2 +- src/type/__tests__/schema-test.js | 5 +- src/type/__tests__/serialization-test.js | 20 +-- src/type/__tests__/validation-test.js | 68 ++++++- src/utilities/__tests__/TypeInfo-test.js | 170 +++++++++++++++++- .../__tests__/assertValidName-test.js | 10 ++ .../__tests__/buildClientSchema-test.js | 11 ++ src/utilities/__tests__/extendSchema-test.js | 21 +++ src/utilities/__tests__/schemaPrinter-test.js | 40 ++++- .../__tests__/KnownDirectives-test.js | 16 +- .../OverlappingFieldsCanBeMerged-test.js | 20 ++- .../__tests__/PossibleFragmentSpreads-test.js | 6 + src/validation/__tests__/validation-test.js | 18 ++ 31 files changed, 958 insertions(+), 78 deletions(-) create mode 100644 src/__tests__/graphql-test.js create mode 100644 src/jsutils/__tests__/invariant-test.js create mode 100644 src/language/__tests__/predicates-test.js diff --git a/src/__fixtures__/schema-kitchen-sink.graphql b/src/__fixtures__/schema-kitchen-sink.graphql index a670f6b3cc..477e25d474 100644 --- a/src/__fixtures__/schema-kitchen-sink.graphql +++ b/src/__fixtures__/schema-kitchen-sink.graphql @@ -130,6 +130,7 @@ extend input InputType @onInputObject This is a description of the `@skip` directive """ directive @skip( + """This is a description of the `if` argument""" if: Boolean! @onArgumentDefinition ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/__tests__/graphql-test.js b/src/__tests__/graphql-test.js new file mode 100644 index 0000000000..764909dba2 --- /dev/null +++ b/src/__tests__/graphql-test.js @@ -0,0 +1,18 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { GraphQLSchema } from '../type/schema'; + +import { graphqlSync } from '../graphql'; + +describe('graphql', () => { + it('report errors raised during schema validation', () => { + const schema = new GraphQLSchema({}); + const result = graphqlSync({ schema, source: '{ __typename }' }); + expect(result).to.deep.equal({ + errors: [{ message: 'Query root type must be provided.' }], + }); + }); +}); diff --git a/src/__tests__/starWarsQuery-test.js b/src/__tests__/starWarsQuery-test.js index 64ecdf1368..fd14017293 100644 --- a/src/__tests__/starWarsQuery-test.js +++ b/src/__tests__/starWarsQuery-test.js @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { graphql } from '../graphql'; +import { graphql, graphqlSync } from '../graphql'; import { StarWarsSchema as schema } from './starWarsSchema'; @@ -45,6 +45,9 @@ describe('Star Wars Query Tests', () => { }, }, }); + + const syncResult = graphqlSync(schema, source); + expect(syncResult).to.deep.equal(result); }); it('Allows us to query for the ID and friends of R2-D2', async () => { @@ -165,12 +168,15 @@ describe('Star Wars Query Tests', () => { }); describe('Using IDs and query parameters to refetch objects', () => { - it('Allows us to query for Luke Skywalker directly, using his ID', async () => { + it('Allows us to query characters directly, using their IDs', async () => { const source = ` - query FetchLukeQuery { + query FetchLukeAndC3POQuery { human(id: "1000") { name } + droid(id: "2000") { + name + } } `; @@ -180,6 +186,9 @@ describe('Star Wars Query Tests', () => { human: { name: 'Luke Skywalker', }, + droid: { + name: 'C-3PO', + }, }, }); }); diff --git a/src/execution/__tests__/abstract-test.js b/src/execution/__tests__/abstract-test.js index 93fd2b00c2..7c6d527569 100644 --- a/src/execution/__tests__/abstract-test.js +++ b/src/execution/__tests__/abstract-test.js @@ -443,6 +443,46 @@ describe('Execute: Handles execution of abstract types', () => { }); }); + it('missing both resolveType and isTypeOf yields useful error', () => { + const fooInterface = new GraphQLInterfaceType({ + name: 'FooInterface', + fields: { bar: { type: GraphQLString } }, + }); + + const fooObject = new GraphQLObjectType({ + name: 'FooObject', + fields: { bar: { type: GraphQLString } }, + interfaces: [fooInterface], + }); + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + foo: { + type: fooInterface, + resolve: () => 'dummy', + }, + }, + }), + types: [fooObject], + }); + + const result = graphqlSync({ schema, source: '{ foo { bar } }' }); + + expect(result).to.deep.equal({ + data: { foo: null }, + errors: [ + { + message: + 'Abstract type "FooInterface" must resolve to an Object type at runtime for field "Query.foo" with value "dummy", received "undefined". Either the "FooInterface" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.', + locations: [{ line: 1, column: 3 }], + path: ['foo'], + }, + ], + }); + }); + it('resolveType allows resolving with type name', () => { const PetType = new GraphQLInterfaceType({ name: 'Pet', diff --git a/src/execution/__tests__/executor-test.js b/src/execution/__tests__/executor-test.js index fa293df123..134a4ad4d0 100644 --- a/src/execution/__tests__/executor-test.js +++ b/src/execution/__tests__/executor-test.js @@ -44,6 +44,31 @@ describe('Execute: Handles basic execution tasks', () => { ); }); + it('throws on invalid variables', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Type', + fields: { + fieldA: { + type: GraphQLString, + args: { argA: { type: GraphQLInt } }, + }, + }, + }), + }); + const document = parse(` + query ($a: Int) { + fieldA(argA: $a) + } + `); + const variableValues = '{ "a": 1 }'; + + // $DisableFlowOnNegativeTest + expect(() => execute({ schema, document, variableValues })).to.throw( + 'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.', + ); + }); + it('accepts positional arguments', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -77,7 +102,7 @@ describe('Execute: Handles basic execution tasks', () => { e: () => 'Egg', f: 'Fish', // Called only by DataType::pic static resolver - pic: size => 'Pic of size: ' + (size || 50), + pic: size => 'Pic of size: ' + size, deep: () => deepData, promise: promiseData, }; @@ -908,6 +933,30 @@ describe('Execute: Handles basic execution tasks', () => { }); }); + it('ignores missing sub selections on fields', () => { + const someType = new GraphQLObjectType({ + name: 'SomeType', + fields: { + b: { type: GraphQLString }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + a: { type: someType }, + }, + }), + }); + const document = parse('{ a }'); + const rootValue = { a: { b: 'c' } }; + + const result = execute({ schema, document, rootValue }); + expect(result).to.deep.equal({ + data: { a: {} }, + }); + }); + it('does not include illegal fields in output', () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -973,7 +1022,10 @@ describe('Execute: Handles basic execution tasks', () => { const SpecialType = new GraphQLObjectType({ name: 'SpecialType', - isTypeOf: obj => obj instanceof Special, + isTypeOf(obj, context) { + const result = obj instanceof Special; + return context && context.async ? Promise.resolve(result) : result; + }, fields: { value: { type: GraphQLString } }, }); @@ -1005,6 +1057,10 @@ describe('Execute: Handles basic execution tasks', () => { }, ], }); + + const contextValue = { async: true }; + const asyncResult = execute({ schema, document, rootValue, contextValue }); + expect(asyncResult).to.deep.equal(asyncResult); }); it('executes ignoring invalid non-executable definitions', () => { diff --git a/src/execution/__tests__/mutations-test.js b/src/execution/__tests__/mutations-test.js index 235fbbdc72..217b6e8873 100644 --- a/src/execution/__tests__/mutations-test.js +++ b/src/execution/__tests__/mutations-test.js @@ -137,6 +137,15 @@ describe('Execute: Handles mutation execution ordering', () => { }); }); + it('does not include illegal mutation fields in output', () => { + const document = parse('mutation { thisIsIllegalDoNotIncludeMe }'); + + const result = execute({ schema, document }); + expect(result).to.deep.equal({ + data: {}, + }); + }); + it('evaluates mutations correctly in the presence of a failed mutation', async () => { const document = parse(` mutation M { diff --git a/src/execution/__tests__/nonnull-test.js b/src/execution/__tests__/nonnull-test.js index 2829118c90..620c52f8c7 100644 --- a/src/execution/__tests__/nonnull-test.js +++ b/src/execution/__tests__/nonnull-test.js @@ -3,6 +3,8 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; +import invariant from '../../jsutils/invariant'; + import { parse } from '../../language/parser'; import { GraphQLSchema } from '../../type/schema'; @@ -566,9 +568,8 @@ describe('Execute: handles non-nullable types', () => { }, }, resolve: (_, args) => { - if (typeof args.cannotBeNull === 'string') { - return 'Passed: ' + args.cannotBeNull; - } + invariant(typeof args.cannotBeNull === 'string'); + return 'Passed: ' + args.cannotBeNull; }, }, }, diff --git a/src/execution/__tests__/union-interface-test.js b/src/execution/__tests__/union-interface-test.js index 4f74aca78b..d92befc813 100644 --- a/src/execution/__tests__/union-interface-test.js +++ b/src/execution/__tests__/union-interface-test.js @@ -381,6 +381,47 @@ describe('Execute: Union and intersection types', () => { }); }); + it('executes interface types with named fragments', () => { + const document = parse(` + { + __typename + name + friends { + __typename + name + ...DogBarks + ...CatMeows + } + } + + fragment DogBarks on Dog { + barks + } + + fragment CatMeows on Cat { + meows + } + `); + + expect(execute({ schema, document, rootValue: john })).to.deep.equal({ + data: { + __typename: 'Person', + name: 'John', + friends: [ + { + __typename: 'Person', + name: 'Liz', + }, + { + __typename: 'Dog', + name: 'Odie', + barks: true, + }, + ], + }, + }); + }); + it('allows fragment conditions to be abstract types', () => { const document = parse(` { diff --git a/src/execution/__tests__/variables-test.js b/src/execution/__tests__/variables-test.js index 9ca5812c0c..1963f6d59e 100644 --- a/src/execution/__tests__/variables-test.js +++ b/src/execution/__tests__/variables-test.js @@ -25,23 +25,13 @@ import { getVariableValues } from '../values'; const TestComplexScalar = new GraphQLScalarType({ name: 'ComplexScalar', - serialize(value) { - if (value === 'DeserializedValue') { - return 'SerializedValue'; - } - return null; - }, parseValue(value) { - if (value === 'SerializedValue') { - return 'DeserializedValue'; - } - return null; + invariant(value === 'SerializedValue'); + return 'DeserializedValue'; }, parseLiteral(ast) { - if (ast.value === 'SerializedValue') { - return 'DeserializedValue'; - } - return null; + invariant(ast.value === 'SerializedValue'); + return 'DeserializedValue'; }, }); diff --git a/src/jsutils/__tests__/invariant-test.js b/src/jsutils/__tests__/invariant-test.js new file mode 100644 index 0000000000..762143076a --- /dev/null +++ b/src/jsutils/__tests__/invariant-test.js @@ -0,0 +1,16 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import invariant from '../invariant'; + +describe('invariant', () => { + it('throws on false conditions', () => { + expect(() => invariant(false, 'Oops!')).to.throw('Oops!'); + }); + + it('use default error message', () => { + expect(() => invariant(false)).to.throw('Unexpected invariant triggered.'); + }); +}); diff --git a/src/language/__tests__/predicates-test.js b/src/language/__tests__/predicates-test.js new file mode 100644 index 0000000000..a79fb7b706 --- /dev/null +++ b/src/language/__tests__/predicates-test.js @@ -0,0 +1,134 @@ +// @flow strict + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { Kind } from '../kinds'; +import { type ASTNode } from '../ast'; +import { + isDefinitionNode, + isExecutableDefinitionNode, + isSelectionNode, + isValueNode, + isTypeNode, + isTypeSystemDefinitionNode, + isTypeDefinitionNode, + isTypeSystemExtensionNode, + isTypeExtensionNode, +} from '../predicates'; + +const allASTNodes: Array = Object.values(Kind).map( + kind => ({ kind }: any), +); + +function filterNodes(predicate: ASTNode => boolean): Array { + return allASTNodes.filter(predicate).map(({ kind }) => kind); +} + +describe('AST node predicates', () => { + it('isDefinitionNode', () => { + expect(filterNodes(isDefinitionNode)).to.deep.equal([ + 'OperationDefinition', + 'FragmentDefinition', + 'SchemaDefinition', + 'ScalarTypeDefinition', + 'ObjectTypeDefinition', + 'InterfaceTypeDefinition', + 'UnionTypeDefinition', + 'EnumTypeDefinition', + 'InputObjectTypeDefinition', + 'DirectiveDefinition', + 'SchemaExtension', + 'ScalarTypeExtension', + 'ObjectTypeExtension', + 'InterfaceTypeExtension', + 'UnionTypeExtension', + 'EnumTypeExtension', + 'InputObjectTypeExtension', + ]); + }); + + it('isExecutableDefinitionNode', () => { + expect(filterNodes(isExecutableDefinitionNode)).to.deep.equal([ + 'OperationDefinition', + 'FragmentDefinition', + ]); + }); + + it('isSelectionNode', () => { + expect(filterNodes(isSelectionNode)).to.deep.equal([ + 'Field', + 'FragmentSpread', + 'InlineFragment', + ]); + }); + + it('isValueNode', () => { + expect(filterNodes(isValueNode)).to.deep.equal([ + 'Variable', + 'IntValue', + 'FloatValue', + 'StringValue', + 'BooleanValue', + 'NullValue', + 'EnumValue', + 'ListValue', + 'ObjectValue', + ]); + }); + + it('isTypeNode', () => { + expect(filterNodes(isTypeNode)).to.deep.equal([ + 'NamedType', + 'ListType', + 'NonNullType', + ]); + }); + + it('isTypeSystemDefinitionNode', () => { + expect(filterNodes(isTypeSystemDefinitionNode)).to.deep.equal([ + 'SchemaDefinition', + 'ScalarTypeDefinition', + 'ObjectTypeDefinition', + 'InterfaceTypeDefinition', + 'UnionTypeDefinition', + 'EnumTypeDefinition', + 'InputObjectTypeDefinition', + 'DirectiveDefinition', + ]); + }); + + it('isTypeDefinitionNode', () => { + expect(filterNodes(isTypeDefinitionNode)).to.deep.equal([ + 'ScalarTypeDefinition', + 'ObjectTypeDefinition', + 'InterfaceTypeDefinition', + 'UnionTypeDefinition', + 'EnumTypeDefinition', + 'InputObjectTypeDefinition', + ]); + }); + + it('isTypeSystemExtensionNode', () => { + expect(filterNodes(isTypeSystemExtensionNode)).to.deep.equal([ + 'SchemaExtension', + 'ScalarTypeExtension', + 'ObjectTypeExtension', + 'InterfaceTypeExtension', + 'UnionTypeExtension', + 'EnumTypeExtension', + 'InputObjectTypeExtension', + ]); + }); + + it('isTypeExtensionNode', () => { + expect(filterNodes(isTypeExtensionNode)).to.deep.equal([ + 'ScalarTypeExtension', + 'ObjectTypeExtension', + 'InterfaceTypeExtension', + 'UnionTypeExtension', + 'EnumTypeExtension', + 'InputObjectTypeExtension', + ]); + }); +}); diff --git a/src/language/__tests__/schema-parser-test.js b/src/language/__tests__/schema-parser-test.js index 2a6a73a5bd..a6187e04d1 100644 --- a/src/language/__tests__/schema-parser-test.js +++ b/src/language/__tests__/schema-parser-test.js @@ -140,6 +140,13 @@ describe('Schema Parser', () => { ); }); + it('parses type with description multi-line string', () => { + expectSyntaxError('"Description" 1').to.deep.equal({ + message: 'Syntax Error: Unexpected Int "1".', + locations: [{ line: 1, column: 15 }], + }); + }); + it('Simple extension', () => { const doc = parse(dedent` extend type Hello { @@ -238,10 +245,35 @@ describe('Schema Parser', () => { }); it('Extension without anything throws', () => { + expectSyntaxError('extend scalar Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 20 }], + }); + expectSyntaxError('extend type Hello').to.deep.equal({ message: 'Syntax Error: Unexpected .', locations: [{ line: 1, column: 18 }], }); + + expectSyntaxError('extend interface Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 23 }], + }); + + expectSyntaxError('extend union Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 19 }], + }); + + expectSyntaxError('extend enum Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 18 }], + }); + + expectSyntaxError('extend input Hello').to.deep.equal({ + message: 'Syntax Error: Unexpected .', + locations: [{ line: 1, column: 19 }], + }); }); it('Interface extension without fields followed by extension', () => { @@ -274,20 +306,6 @@ describe('Schema Parser', () => { }); }); - it('Object extension without anything throws', () => { - expectSyntaxError('extend type Hello').to.deep.equal({ - message: 'Syntax Error: Unexpected .', - locations: [{ line: 1, column: 18 }], - }); - }); - - it('Interface extension without anything throws', () => { - expectSyntaxError('extend interface Hello').to.deep.equal({ - message: 'Syntax Error: Unexpected .', - locations: [{ line: 1, column: 23 }], - }); - }); - it('Object extension do not include descriptions', () => { expectSyntaxError(` "Description" @@ -388,6 +406,13 @@ describe('Schema Parser', () => { }); }); + it('Schema extension with invalid operation type throws', () => { + expectSyntaxError('extend schema { unknown: SomeType }').to.deep.equal({ + message: 'Syntax Error: Unexpected Name "unknown".', + locations: [{ line: 1, column: 17 }], + }); + }); + it('Simple non-null type', () => { const doc = parse(dedent` type Hello { diff --git a/src/language/__tests__/schema-printer-test.js b/src/language/__tests__/schema-printer-test.js index 3862ff53db..a96f8c3d98 100644 --- a/src/language/__tests__/schema-printer-test.js +++ b/src/language/__tests__/schema-printer-test.js @@ -156,7 +156,10 @@ describe('Printer: SDL document', () => { extend input InputType @onInputObject """This is a description of the \`@skip\` directive""" - directive @skip(if: Boolean! @onArgumentDefinition) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + directive @skip( + """This is a description of the \`if\` argument""" + if: Boolean! @onArgumentDefinition + ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/src/language/__tests__/source-test.js b/src/language/__tests__/source-test.js index a397ef5614..64b1a89d03 100644 --- a/src/language/__tests__/source-test.js +++ b/src/language/__tests__/source-test.js @@ -11,4 +11,24 @@ describe('Source', () => { expect(Object.prototype.toString.call(source)).to.equal('[object Source]'); }); + + it('rejects invalid locationOffset', () => { + function createSource(locationOffset) { + return new Source('', '', locationOffset); + } + + expect(() => createSource({ line: 0, column: 1 })).to.throw( + 'line in locationOffset is 1-indexed and must be positive.', + ); + expect(() => createSource({ line: -1, column: 1 })).to.throw( + 'line in locationOffset is 1-indexed and must be positive.', + ); + + expect(() => createSource({ line: 1, column: 0 })).to.throw( + 'column in locationOffset is 1-indexed and must be positive.', + ); + expect(() => createSource({ line: 1, column: -1 })).to.throw( + 'column in locationOffset is 1-indexed and must be positive.', + ); + }); }); diff --git a/src/language/__tests__/visitor-test.js b/src/language/__tests__/visitor-test.js index ac94769d5f..6b13efaa9c 100644 --- a/src/language/__tests__/visitor-test.js +++ b/src/language/__tests__/visitor-test.js @@ -3,9 +3,11 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; +import invariant from '../../jsutils/invariant'; + import { Kind } from '../kinds'; import { parse } from '../parser'; -import { visit, visitInParallel, BREAK } from '../visitor'; +import { visit, visitInParallel, BREAK, QueryDocumentKeys } from '../visitor'; import { kitchenSinkQuery } from '../../__fixtures__'; @@ -113,6 +115,29 @@ describe('Visitor', () => { }); }); + it('allows visiting only specified nodes', () => { + const ast = parse('{ a }', { noLocation: true }); + const visited = []; + + visit(ast, { + enter: { + Field(node) { + visited.push(['enter', node.kind]); + }, + }, + leave: { + Field(node) { + visited.push(['leave', node.kind]); + }, + }, + }); + + expect(visited).to.deep.equal([ + ['enter', 'Field'], + ['leave', 'Field'], + ]); + }); + it('allows editing a node both on enter and on leave', () => { const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true }); @@ -308,7 +333,6 @@ describe('Visitor', () => { return BREAK; } }, - leave(node) { checkVisitorFnArgs(ast, arguments); visited.push(['leave', node.kind, getValue(node)]); @@ -829,6 +853,97 @@ describe('Visitor', () => { ]); }); + describe('Support for custom AST nodes', () => { + const customAST: any = parse('{ a }'); + customAST.definitions[0].selectionSet.selections.push({ + kind: 'CustomField', + name: { + kind: 'Name', + value: 'b', + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'CustomField', + name: { + kind: 'Name', + value: 'c', + }, + }, + ], + }, + }); + + it('does not traverse unknown node kinds', () => { + const visited = []; + visit(customAST, { + enter(node) { + visited.push(['enter', node.kind, getValue(node)]); + }, + leave(node) { + visited.push(['leave', node.kind, getValue(node)]); + }, + }); + + expect(visited).to.deep.equal([ + ['enter', 'Document', undefined], + ['enter', 'OperationDefinition', undefined], + ['enter', 'SelectionSet', undefined], + ['enter', 'Field', undefined], + ['enter', 'Name', 'a'], + ['leave', 'Name', 'a'], + ['leave', 'Field', undefined], + ['enter', 'CustomField', undefined], + ['leave', 'CustomField', undefined], + ['leave', 'SelectionSet', undefined], + ['leave', 'OperationDefinition', undefined], + ['leave', 'Document', undefined], + ]); + }); + + it('does not traverse unknown node kinds', () => { + const customQueryDocumentKeys: any = { + ...QueryDocumentKeys, + CustomField: ['name', 'selectionSet'], + }; + + const visited = []; + const visitor = { + enter(node) { + visited.push(['enter', node.kind, getValue(node)]); + }, + leave(node) { + visited.push(['leave', node.kind, getValue(node)]); + }, + }; + visit(customAST, visitor, customQueryDocumentKeys); + + expect(visited).to.deep.equal([ + ['enter', 'Document', undefined], + ['enter', 'OperationDefinition', undefined], + ['enter', 'SelectionSet', undefined], + ['enter', 'Field', undefined], + ['enter', 'Name', 'a'], + ['leave', 'Name', 'a'], + ['leave', 'Field', undefined], + ['enter', 'CustomField', undefined], + ['enter', 'Name', 'b'], + ['leave', 'Name', 'b'], + ['enter', 'SelectionSet', undefined], + ['enter', 'CustomField', undefined], + ['enter', 'Name', 'c'], + ['leave', 'Name', 'c'], + ['leave', 'CustomField', undefined], + ['leave', 'SelectionSet', undefined], + ['leave', 'CustomField', undefined], + ['leave', 'SelectionSet', undefined], + ['leave', 'OperationDefinition', undefined], + ['leave', 'Document', undefined], + ]); + }); + }); + describe('visitInParallel', () => { // Note: nearly identical to the above test of the same test but // using visitInParallel. @@ -1006,9 +1121,9 @@ describe('Visitor', () => { return BREAK; } }, - leave(node) { - checkVisitorFnArgs(ast, arguments); - visited.push(['break-a', 'leave', node.kind, getValue(node)]); + /* istanbul ignore next */ + leave() { + invariant(false); }, }, { diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index a9952b3343..56f04711c3 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -368,6 +368,17 @@ describe('Subscription Initialization Phase', () => { }); }); + it('should pass through unexpected errors thrown in subscribe', async () => { + let expectedError; + try { + // $DisableFlowOnNegativeTest + await subscribe({ schema: emailSchema, document: {} }); + } catch (error) { + expectedError = error; + } + expect(expectedError).to.be.instanceOf(Error); + }); + it('throws an error if subscribe does not return an iterator', async () => { const invalidEmailSchema = new GraphQLSchema({ query: QueryType, diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index 62cee9b21f..8003d86883 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -39,6 +39,11 @@ const NonNullScalarType = GraphQLNonNull(ScalarType); const ListOfNonNullScalarsType = GraphQLList(NonNullScalarType); const NonNullListOfScalars = GraphQLNonNull(ListOfScalarsType); +/* istanbul ignore next */ +const dummyFunc = () => { + /* empty */ +}; + describe('Type System: Scalars', () => { it('accepts a Scalar type defining serialize', () => { expect(() => new GraphQLScalarType({ name: 'SomeScalar' })).to.not.throw(); @@ -49,8 +54,8 @@ describe('Type System: Scalars', () => { () => new GraphQLScalarType({ name: 'SomeScalar', - parseValue: () => null, - parseLiteral: () => null, + parseValue: dummyFunc, + parseLiteral: dummyFunc, }), ).to.not.throw(); }); @@ -79,6 +84,11 @@ describe('Type System: Scalars', () => { ); }); + it('rejects a Scalar type without name', () => { + // $DisableFlowOnNegativeTest + expect(() => new GraphQLScalarType({})).to.throw('Must provide name.'); + }); + it('rejects a Scalar type defining serialize with an incorrect type', () => { expect( () => @@ -97,7 +107,7 @@ describe('Type System: Scalars', () => { () => new GraphQLScalarType({ name: 'SomeScalar', - parseLiteral: () => null, + parseLiteral: dummyFunc, }), ).to.throw( 'SomeScalar must provide both "parseValue" and "parseLiteral" functions.', @@ -287,13 +297,18 @@ describe('Type System: Objects', () => { fields: { f: { type: ScalarType, - resolve: () => ({}), + resolve: dummyFunc, }, }, }); expect(() => objType.getFields()).to.not.throw(); }); + it('rejects an Object type without name', () => { + // $DisableFlowOnNegativeTest + expect(() => new GraphQLObjectType({})).to.throw('Must provide name.'); + }); + it('rejects an Object type field with undefined config', () => { const objType = new GraphQLObjectType({ name: 'SomeObject', @@ -459,6 +474,11 @@ describe('Type System: Interfaces', () => { expect(implementing.getInterfaces()).to.deep.equal([InterfaceType]); }); + it('rejects an Interface type without name', () => { + // $DisableFlowOnNegativeTest + expect(() => new GraphQLInterfaceType({})).to.throw('Must provide name.'); + }); + it('rejects an Interface type with incorrectly typed interfaces', () => { const objType = new GraphQLInterfaceType({ name: 'AnotherInterface', @@ -535,6 +555,11 @@ describe('Type System: Unions', () => { expect(unionType.getTypes()).to.deep.equal([]); }); + it('rejects an Union type without name', () => { + // $DisableFlowOnNegativeTest + expect(() => new GraphQLUnionType({})).to.throw('Must provide name.'); + }); + it('rejects an Union type with an incorrect type for resolveType', () => { expect( () => @@ -650,6 +675,13 @@ describe('Type System: Enums', () => { expect(enumType.getValue('BAR')).has.property('value', 20); }); + it('rejects an Enum type without name', () => { + // $DisableFlowOnNegativeTest + expect(() => new GraphQLEnumType({ values: {} })).to.throw( + 'Must provide name.', + ); + }); + it('rejects an Enum type with incorrectly typed values', () => { expect( () => @@ -743,6 +775,13 @@ describe('Type System: Input Objects', () => { }); }); + it('rejects an Input Object type without name', () => { + // $DisableFlowOnNegativeTest + expect(() => new GraphQLInputObjectType({})).to.throw( + 'Must provide name.', + ); + }); + it('rejects an Input Object type with incorrect fields', () => { const inputObjType = new GraphQLInputObjectType({ name: 'SomeInputObject', @@ -772,7 +811,7 @@ describe('Type System: Input Objects', () => { name: 'SomeInputObject', fields: { // $DisableFlowOnNegativeTest - f: { type: ScalarType, resolve: () => 0 }, + f: { type: ScalarType, resolve: dummyFunc }, }, }); expect(() => inputObjType.getFields()).to.throw( diff --git a/src/type/__tests__/enumType-test.js b/src/type/__tests__/enumType-test.js index d2d1acc041..e3c798966d 100644 --- a/src/type/__tests__/enumType-test.js +++ b/src/type/__tests__/enumType-test.js @@ -19,7 +19,7 @@ const ColorType = new GraphQLEnumType({ }, }); -const Complex1 = { someRandomFunction: () => null }; +const Complex1 = { someRandomObject: new Date() }; const Complex2 = { someRandomValue: 123 }; const ComplexEnum = new GraphQLEnumType({ @@ -52,10 +52,9 @@ const QueryType = new GraphQLObjectType({ type: GraphQLInt, args: { fromEnum: { type: ColorType }, - fromInt: { type: GraphQLInt }, }, - resolve(_source, { fromEnum, fromInt }) { - return fromInt !== undefined ? fromInt : fromEnum; + resolve(_source, { fromEnum }) { + return fromEnum; }, }, complexEnum: { diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 5a2d07bc78..7744ee976d 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -1406,6 +1406,6 @@ describe('Introspection', () => { invariant(false, `Called on ${info.parentType.name}::${info.fieldName}`); } - graphqlSync({ schema, source, fieldResolver }); + expect(() => graphqlSync({ schema, source, fieldResolver })).to.not.throw(); }); }); diff --git a/src/type/__tests__/schema-test.js b/src/type/__tests__/schema-test.js index c10ef8041f..4891aa4469 100644 --- a/src/type/__tests__/schema-test.js +++ b/src/type/__tests__/schema-test.js @@ -270,7 +270,7 @@ describe('Type System: Schema', () => { it('checks the configuration for mistakes', () => { // $DisableFlowOnNegativeTest - expect(() => new GraphQLSchema(() => null)).to.throw(); + expect(() => new GraphQLSchema(JSON.parse)).to.throw(); // $DisableFlowOnNegativeTest expect(() => new GraphQLSchema({ types: {} })).to.throw(); // $DisableFlowOnNegativeTest @@ -332,7 +332,8 @@ describe('Type System: Schema', () => { }); it('does not check the configuration for mistakes', () => { - const config = () => null; + const config = []; + // $DisableFlowOnNegativeTest config.assumeValid = true; // $DisableFlowOnNegativeTest expect(() => new GraphQLSchema(config)).to.not.throw(); diff --git a/src/type/__tests__/serialization-test.js b/src/type/__tests__/serialization-test.js index b9d6e29773..dd1bb8719c 100644 --- a/src/type/__tests__/serialization-test.js +++ b/src/type/__tests__/serialization-test.js @@ -104,15 +104,15 @@ describe('Type System: Scalar coercion', () => { expect(GraphQLString.serialize(true)).to.equal('true'); expect(GraphQLString.serialize(false)).to.equal('false'); - const valueOfAndToJSONValue = { - valueOf: () => 'valueOf string', - toJSON: () => 'toJSON string', - }; + const valueOf = () => 'valueOf string'; + const toJSON = () => 'toJSON string'; + + const valueOfAndToJSONValue = { valueOf, toJSON }; expect(GraphQLString.serialize(valueOfAndToJSONValue)).to.equal( 'valueOf string', ); - const onlyToJSONValue = { toJSON: () => 'toJSON string' }; + const onlyToJSONValue = { toJSON }; expect(GraphQLString.serialize(onlyToJSONValue)).to.equal('toJSON string'); expect(() => GraphQLString.serialize(NaN)).to.throw( @@ -165,13 +165,13 @@ describe('Type System: Scalar coercion', () => { expect(GraphQLID.serialize(0)).to.equal('0'); expect(GraphQLID.serialize(-1)).to.equal('-1'); - const valueOfAndToJSONValue = { - valueOf: () => 'valueOf ID', - toJSON: () => 'toJSON ID', - }; + const valueOf = () => 'valueOf ID'; + const toJSON = () => 'toJSON ID'; + + const valueOfAndToJSONValue = { valueOf, toJSON }; expect(GraphQLID.serialize(valueOfAndToJSONValue)).to.equal('valueOf ID'); - const onlyToJSONValue = { toJSON: () => 'toJSON ID' }; + const onlyToJSONValue = { toJSON }; expect(GraphQLID.serialize(onlyToJSONValue)).to.equal('toJSON ID'); const badObjValue = { diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index 3e7f4475bb..4ac82c5eac 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -3,12 +3,14 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; +import dedent from '../../jsutils/dedent'; import inspect from '../../jsutils/inspect'; import { parse } from '../../language/parser'; import { GraphQLSchema } from '../../type/schema'; import { GraphQLString } from '../../type/scalars'; +import { GraphQLDirective } from '../../type/directives'; import { type GraphQLNamedType, type GraphQLInputType, @@ -26,7 +28,7 @@ import { import { extendSchema } from '../../utilities/extendSchema'; import { buildSchema } from '../../utilities/buildASTSchema'; -import { validateSchema } from '../validate'; +import { validateSchema, assertValidSchema } from '../validate'; const SomeScalarType = new GraphQLScalarType({ name: 'SomeScalar' }); @@ -1390,7 +1392,7 @@ describe('Type System: Interface fields must have output types', () => { }); }); -describe('Type System: Field arguments must have input types', () => { +describe('Type System: Arguments must have input types', () => { function schemaWithArgOfType(argType: GraphQLInputType) { const BadObjectType = new GraphQLObjectType({ name: 'BadObject', @@ -1411,6 +1413,15 @@ describe('Type System: Field arguments must have input types', () => { f: { type: BadObjectType }, }, }), + directives: [ + new GraphQLDirective({ + name: 'BadDirective', + args: { + badArg: { type: argType }, + }, + locations: ['QUERY'], + }), + ], }); } @@ -1426,6 +1437,10 @@ describe('Type System: Field arguments must have input types', () => { // $DisableFlowOnNegativeTest const schema = schemaWithArgOfType(undefined); expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'The type of @BadDirective(badArg:) must be Input Type but got: undefined.', + }, { message: 'The type of BadObject.badField(badArg:) must be Input Type but got: undefined.', @@ -1439,6 +1454,9 @@ describe('Type System: Field arguments must have input types', () => { // $DisableFlowOnNegativeTest const schema = schemaWithArgOfType(type); expect(validateSchema(schema)).to.deep.equal([ + { + message: `The type of @BadDirective(badArg:) must be Input Type but got: ${typeStr}.`, + }, { message: `The type of BadObject.badField(badArg:) must be Input Type but got: ${typeStr}.`, }, @@ -1450,6 +1468,10 @@ describe('Type System: Field arguments must have input types', () => { // $DisableFlowOnNegativeTest const schema = schemaWithArgOfType(Number); expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'The type of @BadDirective(badArg:) must be Input Type but got: [function Number].', + }, { message: 'The type of BadObject.badField(badArg:) must be Input Type but got: [function Number].', @@ -2182,6 +2204,29 @@ describe('Interfaces must adhere to Interface they implement', () => { expect(validateSchema(schema)).to.deep.equal([]); }); + it('rejects an Interface implementing a non-Interface type', () => { + const schema = buildSchema(` + type Query { + field: String + } + + input SomeInputObject { + field: String + } + + interface BadInterface implements SomeInputObject { + field: String + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Type BadInterface must only implement Interface types, it cannot implement SomeInputObject.', + locations: [{ line: 10, column: 41 }], + }, + ]); + }); + it('rejects an Interface missing an Interface argument', () => { const schema = buildSchema(` type Query { @@ -2496,3 +2541,22 @@ describe('Interfaces must adhere to Interface they implement', () => { ]); }); }); + +describe('assertValidSchema', () => { + it('do not throw on valid schemas', () => { + const schema = buildSchema(` + type Query { + foo: String + } + `); + expect(() => assertValidSchema(schema)).to.not.throw(); + }); + + it('include multiple errors into a description', () => { + const schema = buildSchema('type SomeType'); + expect(() => assertValidSchema(schema)).to.throw(dedent` + Query root type must be provided. + + Type SomeType must define one or more fields.`); + }); +}); diff --git a/src/utilities/__tests__/TypeInfo-test.js b/src/utilities/__tests__/TypeInfo-test.js index 30ba9c4d19..bf7d5a853c 100644 --- a/src/utilities/__tests__/TypeInfo-test.js +++ b/src/utilities/__tests__/TypeInfo-test.js @@ -3,17 +3,80 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { parse } from '../../language/parser'; +import invariant from '../../jsutils/invariant'; + +import { parse, parseValue } from '../../language/parser'; import { print } from '../../language/printer'; import { visit } from '../../language/visitor'; import { getNamedType, isCompositeType } from '../../type/definition'; +import { buildSchema } from '../buildASTSchema'; import { TypeInfo, visitWithTypeInfo } from '../TypeInfo'; import { testSchema } from '../../validation/__tests__/harness'; +describe('TypeInfo', () => { + it('allow all methods to be called before entering any node', () => { + const typeInfo = new TypeInfo(testSchema); + + expect(typeInfo.getType()).to.equal(undefined); + expect(typeInfo.getParentType()).to.equal(undefined); + expect(typeInfo.getInputType()).to.equal(undefined); + expect(typeInfo.getParentInputType()).to.equal(undefined); + expect(typeInfo.getFieldDef()).to.equal(undefined); + expect(typeInfo.getDefaultValue()).to.equal(undefined); + expect(typeInfo.getDirective()).to.equal(null); + expect(typeInfo.getArgument()).to.equal(null); + expect(typeInfo.getEnumValue()).to.equal(null); + }); +}); + describe('visitWithTypeInfo', () => { + it('supports different operation types', () => { + const schema = buildSchema(` + schema { + query: QueryRoot + mutation: MutationRoot + subscription: SubscriptionRoot + } + + type QueryRoot { + foo: String + } + + type MutationRoot { + bar: String + } + + type SubscriptionRoot { + baz: String + } + `); + const ast = parse(` + query { foo } + mutation { bar } + subscription { baz } + `); + const typeInfo = new TypeInfo(schema); + + const rootTypes = {}; + visit( + ast, + visitWithTypeInfo(typeInfo, { + OperationDefinition(node) { + rootTypes[node.operation] = String(typeInfo.getType()); + }, + }), + ); + + expect(rootTypes).to.deep.equal({ + query: 'QueryRoot', + mutation: 'MutationRoot', + subscription: 'SubscriptionRoot', + }); + }); + it('provide exact same arguments to wrapped visitor', () => { const ast = parse( '{ human(id: 4) { name, pets { ... { name } }, unknown } }', @@ -245,4 +308,109 @@ describe('visitWithTypeInfo', () => { ['leave', 'Document', null, null, null, null], ]); }); + + it('support traversals of input values', () => { + const ast = parseValue('{ stringListField: ["foo"] }'); + const complexInputType = testSchema.getType('ComplexInput'); + invariant(complexInputType != null); + + const typeInfo = new TypeInfo(testSchema, undefined, complexInputType); + + const visited = []; + visit( + ast, + visitWithTypeInfo(typeInfo, { + enter(node) { + const type = typeInfo.getInputType(); + visited.push([ + 'enter', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + ]); + }, + leave(node) { + const type = typeInfo.getInputType(); + visited.push([ + 'leave', + node.kind, + node.kind === 'Name' ? node.value : null, + String(type), + ]); + }, + }), + ); + + expect(visited).to.deep.equal([ + ['enter', 'ObjectValue', null, 'ComplexInput'], + ['enter', 'ObjectField', null, '[String]'], + ['enter', 'Name', 'stringListField', '[String]'], + ['leave', 'Name', 'stringListField', '[String]'], + ['enter', 'ListValue', null, 'String'], + ['enter', 'StringValue', null, 'String'], + ['leave', 'StringValue', null, 'String'], + ['leave', 'ListValue', null, 'String'], + ['leave', 'ObjectField', null, '[String]'], + ['leave', 'ObjectValue', null, 'ComplexInput'], + ]); + }); + + it('support traversals of input values', () => { + const humanType = testSchema.getType('Human'); + invariant(humanType != null); + + const typeInfo = new TypeInfo(testSchema, undefined, humanType); + + const ast = parse('{ name, pets { name } }'); + const operationNode = ast.definitions[0]; + invariant(operationNode.kind === 'OperationDefinition'); + + const visited = []; + visit( + operationNode.selectionSet, + visitWithTypeInfo(typeInfo, { + enter(node) { + const parentType = typeInfo.getParentType(); + const type = typeInfo.getType(); + visited.push([ + 'enter', + node.kind, + node.kind === 'Name' ? node.value : null, + String(parentType), + String(type), + ]); + }, + leave(node) { + const parentType = typeInfo.getParentType(); + const type = typeInfo.getType(); + visited.push([ + 'leave', + node.kind, + node.kind === 'Name' ? node.value : null, + String(parentType), + String(type), + ]); + }, + }), + ); + + expect(visited).to.deep.equal([ + ['enter', 'SelectionSet', null, 'Human', 'Human'], + ['enter', 'Field', null, 'Human', 'String'], + ['enter', 'Name', 'name', 'Human', 'String'], + ['leave', 'Name', 'name', 'Human', 'String'], + ['leave', 'Field', null, 'Human', 'String'], + ['enter', 'Field', null, 'Human', '[Pet]'], + ['enter', 'Name', 'pets', 'Human', '[Pet]'], + ['leave', 'Name', 'pets', 'Human', '[Pet]'], + ['enter', 'SelectionSet', null, 'Pet', '[Pet]'], + ['enter', 'Field', null, 'Pet', 'String'], + ['enter', 'Name', 'name', 'Pet', 'String'], + ['leave', 'Name', 'name', 'Pet', 'String'], + ['leave', 'Field', null, 'Pet', 'String'], + ['leave', 'SelectionSet', null, 'Pet', '[Pet]'], + ['leave', 'Field', null, 'Human', '[Pet]'], + ['leave', 'SelectionSet', null, 'Human', 'Human'], + ]); + }); }); diff --git a/src/utilities/__tests__/assertValidName-test.js b/src/utilities/__tests__/assertValidName-test.js index 8eb60dbd6d..5cf713ab62 100644 --- a/src/utilities/__tests__/assertValidName-test.js +++ b/src/utilities/__tests__/assertValidName-test.js @@ -6,6 +6,16 @@ import { describe, it } from 'mocha'; import { assertValidName } from '../assertValidName'; describe('assertValidName()', () => { + it('passthrough valid name', () => { + expect(assertValidName('_ValidName123')).to.equal('_ValidName123'); + }); + + it('throws for use of leading double underscores', () => { + expect(() => assertValidName('__bad')).to.throw( + '"__bad" must not begin with "__", which is reserved by GraphQL introspection.', + ); + }); + it('throws for use of leading double underscores', () => { expect(() => assertValidName('__bad')).to.throw( '"__bad" must not begin with "__", which is reserved by GraphQL introspection.', diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 9ed311132e..74c7ce8f39 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -553,6 +553,17 @@ describe('Type System: build schema from introspection', () => { expect(result.data).to.deep.equal({ foo: 'bar' }); }); + describe('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 { diff --git a/src/utilities/__tests__/extendSchema-test.js b/src/utilities/__tests__/extendSchema-test.js index 7ca413a6a7..a9571130a5 100644 --- a/src/utilities/__tests__/extendSchema-test.js +++ b/src/utilities/__tests__/extendSchema-test.js @@ -171,6 +171,27 @@ describe('extendSchema', () => { `); }); + it('ignores comment description on extended fields if location is not provided', () => { + const schema = buildSchema('type Query'); + const extendSDL = ` + extend type Query { + # New field description. + newField: String + } + `; + const extendAST = parse(extendSDL, { noLocation: true }); + const extendedSchema = extendSchema(schema, extendAST, { + commentDescriptions: true, + }); + + expect(validateSchema(extendedSchema)).to.deep.equal([]); + expect(printSchemaChanges(schema, extendedSchema)).to.equal(dedent` + type Query { + newField: String + } + `); + }); + it('extends objects with standard type fields', () => { const schema = buildSchema('type Query'); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 14edfdca62..93ac1143cd 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -256,24 +256,48 @@ describe('Type System Printer', () => { `); }); - it('Prints custom query root type', () => { - const CustomQueryType = new GraphQLObjectType({ - name: 'CustomQueryType', - fields: { bar: { type: GraphQLString } }, + it('Prints custom query root types', () => { + const Schema = new GraphQLSchema({ + query: new GraphQLObjectType({ name: 'CustomType', fields: {} }), }); + const output = printForTest(Schema); + expect(output).to.equal(dedent` + schema { + query: CustomType + } + + type CustomType + `); + }); + + it('Prints custom mutation root types', () => { const Schema = new GraphQLSchema({ - query: CustomQueryType, + mutation: new GraphQLObjectType({ name: 'CustomType', fields: {} }), }); + const output = printForTest(Schema); expect(output).to.equal(dedent` schema { - query: CustomQueryType + mutation: CustomType } - type CustomQueryType { - bar: String + type CustomType + `); + }); + + it('Prints custom subscription root types', () => { + const Schema = new GraphQLSchema({ + subscription: new GraphQLObjectType({ name: 'CustomType', fields: {} }), + }); + + const output = printForTest(Schema); + expect(output).to.equal(dedent` + schema { + subscription: CustomType } + + type CustomType `); }); diff --git a/src/validation/__tests__/KnownDirectives-test.js b/src/validation/__tests__/KnownDirectives-test.js index d875cbc4a1..1f422da1c8 100644 --- a/src/validation/__tests__/KnownDirectives-test.js +++ b/src/validation/__tests__/KnownDirectives-test.js @@ -111,14 +111,26 @@ describe('Validate: Known directives', () => { it('with well placed directives', () => { expectValid(` - query Foo($var: Boolean) @onQuery { + query ($var: Boolean) @onQuery { name @include(if: $var) ...Frag @include(if: true) skippedField @skip(if: true) ...SkippedFrag @skip(if: true) + + ... @skip(if: true) { + skippedField + } + } + + mutation @onMutation { + someField + } + + subscription @onSubscription { + someField } - mutation Bar @onMutation { + fragment Frag on SomeType @onFragmentDefinition { someField } `); diff --git a/src/validation/__tests__/OverlappingFieldsCanBeMerged-test.js b/src/validation/__tests__/OverlappingFieldsCanBeMerged-test.js index dd40d0e130..e6b48d15c7 100644 --- a/src/validation/__tests__/OverlappingFieldsCanBeMerged-test.js +++ b/src/validation/__tests__/OverlappingFieldsCanBeMerged-test.js @@ -185,7 +185,7 @@ describe('Validate: Overlapping fields can be merged', () => { ]); }); - it('conflicting args', () => { + it('conflicting arg values', () => { expectErrors(` fragment conflictingArgs on Dog { doesKnowCommand(dogCommand: SIT) @@ -203,6 +203,24 @@ describe('Validate: Overlapping fields can be merged', () => { ]); }); + it('conflicting arg names', () => { + expectErrors(` + fragment conflictingArgs on Dog { + isAtLocation(x: 0) + isAtLocation(y: 0) + } + `).to.deep.equal([ + { + message: + 'Fields "isAtLocation" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.', + locations: [ + { line: 3, column: 9 }, + { line: 4, column: 9 }, + ], + }, + ]); + }); + it('allows different args where no conflict is possible', () => { // This is valid since no object can be both a "Dog" and a "Cat", thus // these fields can never overlap. diff --git a/src/validation/__tests__/PossibleFragmentSpreads-test.js b/src/validation/__tests__/PossibleFragmentSpreads-test.js index 2ee75b87b3..e0934ea1a1 100644 --- a/src/validation/__tests__/PossibleFragmentSpreads-test.js +++ b/src/validation/__tests__/PossibleFragmentSpreads-test.js @@ -97,6 +97,12 @@ describe('Validate: Possible fragment spreads', () => { `); }); + it('ignores unknown fragments (caught by KnownFragmentNames)', () => { + expectValid(` + fragment petFragment on Pet { ...UnknownFragment } + `); + }); + it('different object into object', () => { expectErrors(` fragment invalidObjectWithinObject on Cat { ...dogFragment } diff --git a/src/validation/__tests__/validation-test.js b/src/validation/__tests__/validation-test.js index 0a510d79ab..831edc7b85 100644 --- a/src/validation/__tests__/validation-test.js +++ b/src/validation/__tests__/validation-test.js @@ -11,6 +11,11 @@ import { validate } from '../validate'; import { testSchema } from './harness'; describe('Validate: Supports full validation', () => { + it('rejects invalid documents', () => { + // $DisableFlowOnNegativeTest + expect(() => validate(testSchema, null)).to.throw('Must provide document.'); + }); + it('validates queries', () => { const doc = parse(` query { @@ -116,4 +121,17 @@ describe('Validate: Limit maximum number of validation errors', () => { }, ]); }); + + it('passthrough exceptions from rules', () => { + function customRule() { + return { + Field() { + throw new Error('Error from custom rule!'); + }, + }; + } + expect(() => + validate(testSchema, doc, [customRule], undefined, { maxErrors: 1 }), + ).to.throw(/^Error from custom rule!$/); + }); });