From 87c6a274fb24e08a2c85a93a9294e5711eed874b Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Fri, 16 Oct 2015 16:55:00 -0400 Subject: [PATCH] first pass at tests for subscriptions --- src/execution/__tests__/executor.js | 32 ++++++++- src/language/__tests__/kitchen-sink.graphql | 13 ++++ src/language/__tests__/parser.js | 25 +++++-- src/language/__tests__/printer.js | 13 ++++ src/language/__tests__/visitor.js | 67 +++++++++++++++++-- src/type/__tests__/definition.js | 35 ++++++++++ src/type/__tests__/enumType.js | 32 ++++++++- src/type/__tests__/introspection.js | 15 ++++- src/type/__tests__/validation.js | 25 +++++++ src/type/introspection.js | 2 +- src/utilities/__tests__/buildASTSchema.js | 35 +++++++++- src/utilities/__tests__/buildClientSchema.js | 16 ++++- src/utilities/__tests__/getOperationAST.js | 28 ++++++-- .../__tests__/LoneAnonymousOperation.js | 15 ++++- .../__tests__/UniqueOperationNames.js | 19 +++++- 15 files changed, 347 insertions(+), 25 deletions(-) diff --git a/src/execution/__tests__/executor.js b/src/execution/__tests__/executor.js index 84e1e9843b..f7492ab3a3 100644 --- a/src/execution/__tests__/executor.js +++ b/src/execution/__tests__/executor.js @@ -433,7 +433,7 @@ describe('Execute: Handles basic execution tasks', () => { }); it('uses the query schema for queries', async () => { - var doc = `query Q { a } mutation M { c }`; + var doc = `query Q { a } mutation M { c } subscription S { a }`; var data = { a: 'b', c: 'd' }; var ast = parse(doc); var schema = new GraphQLSchema({ @@ -448,6 +448,12 @@ describe('Execute: Handles basic execution tasks', () => { fields: { c: { type: GraphQLString }, } + }), + subscription: new GraphQLObjectType({ + name: 'S', + fields: { + a: { type: GraphQLString }, + } }) }); @@ -480,6 +486,30 @@ describe('Execute: Handles basic execution tasks', () => { expect(mutationResult).to.deep.equal({ data: { c: 'd' } }); }); + it('uses the subscription schema for subscriptions', async () => { + var doc = `query Q { a } subscription S { a }`; + var data = { a: 'b', c: 'd' }; + var ast = parse(doc); + var schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Q', + fields: { + a: { type: GraphQLString }, + } + }), + subscription: new GraphQLObjectType({ + name: 'S', + fields: { + a: { type: GraphQLString }, + } + }) + }); + + var subscriptionResult = await execute(schema, ast, data, {}, 'S'); + + expect(subscriptionResult).to.deep.equal({ data: { a: 'b' } }); + }); + it('correct field ordering despite execution order', async () => { var doc = `{ a, diff --git a/src/language/__tests__/kitchen-sink.graphql b/src/language/__tests__/kitchen-sink.graphql index 0d2ceff043..0e04e2e42d 100644 --- a/src/language/__tests__/kitchen-sink.graphql +++ b/src/language/__tests__/kitchen-sink.graphql @@ -34,6 +34,19 @@ mutation likeStory { } } +subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + fragment frag on Friend { foo(size: $size, bar: $b, obj: {key: "value"}) } diff --git a/src/language/__tests__/parser.js b/src/language/__tests__/parser.js index 6cc6af056a..8e9e3ce2d8 100644 --- a/src/language/__tests__/parser.js +++ b/src/language/__tests__/parser.js @@ -163,6 +163,7 @@ fragment MissingOn Type 'fragment', 'query', 'mutation', + 'subscription', 'true', 'false' ]; @@ -185,22 +186,38 @@ fragment ${fragmentName} on Type { }); }); - it('parses experimental subscription feature', () => { + it('parses anonymous mutation operations', () => { expect(() => parse(` - subscription Foo { + mutation { + mutationField + } + `)).to.not.throw(); + }); + + it('parses anonymous subscription operations', () => { + expect(() => parse(` + subscription { subscriptionField } `)).to.not.throw(); }); - it('parses anonymous operations', () => { + it('parses named mutation operations', () => { expect(() => parse(` - mutation { + mutation Foo { mutationField } `)).to.not.throw(); }); + it('parses named subscription operations', () => { + expect(() => parse(` + subscription Foo { + subscriptionField + } + `)).to.not.throw(); + }); + it('parse creates ast', () => { var source = new Source(`{ diff --git a/src/language/__tests__/printer.js b/src/language/__tests__/printer.js index 1d25bd3791..c6216ddc4c 100644 --- a/src/language/__tests__/printer.js +++ b/src/language/__tests__/printer.js @@ -76,6 +76,19 @@ mutation likeStory { } } +subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + fragment frag on Friend { foo(size: $size, bar: $b, obj: {key: "value"}) } diff --git a/src/language/__tests__/visitor.js b/src/language/__tests__/visitor.js index b191384bad..97de10b3d9 100644 --- a/src/language/__tests__/visitor.js +++ b/src/language/__tests__/visitor.js @@ -385,7 +385,63 @@ describe('Visitor', () => { [ 'leave', 'Field', 0, undefined ], [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], [ 'leave', 'OperationDefinition', 1, undefined ], - [ 'enter', 'FragmentDefinition', 2, undefined ], + [ 'enter', 'OperationDefinition', 2, undefined ], + [ 'enter', 'Name', 'name', 'OperationDefinition' ], + [ 'leave', 'Name', 'name', 'OperationDefinition' ], + [ 'enter', 'VariableDefinition', 0, undefined ], + [ 'enter', 'Variable', 'variable', 'VariableDefinition' ], + [ 'enter', 'Name', 'name', 'Variable' ], + [ 'leave', 'Name', 'name', 'Variable' ], + [ 'leave', 'Variable', 'variable', 'VariableDefinition' ], + [ 'enter', 'NamedType', 'type', 'VariableDefinition' ], + [ 'enter', 'Name', 'name', 'NamedType' ], + [ 'leave', 'Name', 'name', 'NamedType' ], + [ 'leave', 'NamedType', 'type', 'VariableDefinition' ], + [ 'leave', 'VariableDefinition', 0, undefined ], + [ 'enter', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'Argument', 0, undefined ], + [ 'enter', 'Name', 'name', 'Argument' ], + [ 'leave', 'Name', 'name', 'Argument' ], + [ 'enter', 'Variable', 'value', 'Argument' ], + [ 'enter', 'Name', 'name', 'Variable' ], + [ 'leave', 'Name', 'name', 'Variable' ], + [ 'leave', 'Variable', 'value', 'Argument' ], + [ 'leave', 'Argument', 0, undefined ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'enter', 'Field', 1, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'enter', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'enter', 'Field', 0, undefined ], + [ 'enter', 'Name', 'name', 'Field' ], + [ 'leave', 'Name', 'name', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 1, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'Field' ], + [ 'leave', 'Field', 0, undefined ], + [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], + [ 'leave', 'OperationDefinition', 2, undefined ], + [ 'enter', 'FragmentDefinition', 3, undefined ], [ 'enter', 'Name', 'name', 'FragmentDefinition' ], [ 'leave', 'Name', 'name', 'FragmentDefinition' ], [ 'enter', 'NamedType', 'typeCondition', 'FragmentDefinition' ], @@ -426,8 +482,8 @@ describe('Visitor', () => { [ 'leave', 'Argument', 2, undefined ], [ 'leave', 'Field', 0, undefined ], [ 'leave', 'SelectionSet', 'selectionSet', 'FragmentDefinition' ], - [ 'leave', 'FragmentDefinition', 2, undefined ], - [ 'enter', 'OperationDefinition', 3, undefined ], + [ 'leave', 'FragmentDefinition', 3, undefined ], + [ 'enter', 'OperationDefinition', 4, undefined ], [ 'enter', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], [ 'enter', 'Field', 0, undefined ], [ 'enter', 'Name', 'name', 'Field' ], @@ -450,8 +506,7 @@ describe('Visitor', () => { [ 'leave', 'Name', 'name', 'Field' ], [ 'leave', 'Field', 1, undefined ], [ 'leave', 'SelectionSet', 'selectionSet', 'OperationDefinition' ], - [ 'leave', 'OperationDefinition', 3, undefined ], - [ 'leave', 'Document', undefined, undefined ] - ]); + [ 'leave', 'OperationDefinition', 4, undefined ], + [ 'leave', 'Document', undefined, undefined ] ]); }); }); diff --git a/src/type/__tests__/definition.js b/src/type/__tests__/definition.js index 0e6dbc7d9e..5db43a210c 100644 --- a/src/type/__tests__/definition.js +++ b/src/type/__tests__/definition.js @@ -81,6 +81,16 @@ var BlogMutation = new GraphQLObjectType({ } }); +var BlogSubscription = new GraphQLObjectType({ + name: 'Subscription', + fields: { + articleSubscribe: { + args: { id: { type: GraphQLString } }, + type: BlogArticle + } + } +}); + var ObjectType = new GraphQLObjectType({ name: 'Object', isTypeOf: () => true @@ -143,6 +153,21 @@ describe('Type System: Example', () => { }); + it('defines a subscription schema', () => { + var BlogSchema = new GraphQLSchema({ + query: BlogQuery, + subscription: BlogSubscription + }); + + expect(BlogSchema.getSubscriptionType()).to.equal(BlogSubscription); + + var sub = BlogSubscription.getFields()[('articleSubscribe' : string)]; + expect(sub && sub.type).to.equal(BlogArticle); + expect(sub && sub.type.name).to.equal('Article'); + expect(sub && sub.name).to.equal('articleSubscribe'); + + }); + it('includes nested input objects in the map', () => { var NestedInputObject = new GraphQLInputObjectType({ name: 'NestedInputObject', @@ -161,9 +186,19 @@ describe('Type System: Example', () => { } } }); + var SomeSubscription = new GraphQLObjectType({ + name: 'SomeSubscription', + fields: { + subscribeToSomething: { + type: BlogArticle, + args: { input: { type: SomeInputObject } } + } + } + }); var schema = new GraphQLSchema({ query: BlogQuery, mutation: SomeMutation, + subscription: SomeSubscription }); expect(schema.getTypeMap().NestedInputObject).to.equal(NestedInputObject); }); diff --git a/src/type/__tests__/enumType.js b/src/type/__tests__/enumType.js index f8393a5736..d0a3ed5f2a 100644 --- a/src/type/__tests__/enumType.js +++ b/src/type/__tests__/enumType.js @@ -70,7 +70,22 @@ describe('Type System: Enum Values', () => { } }); - var schema = new GraphQLSchema({ query: QueryType, mutation: MutationType }); + var SubscriptionType = new GraphQLObjectType({ + name: 'Subscription', + fields: { + subscribeToEnum: { + type: ColorType, + args: { color: { type: ColorType } }, + resolve(value, { color }) { return color; } + } + } + }); + + var schema = new GraphQLSchema({ + query: QueryType, + mutation: MutationType, + subscription: SubscriptionType + }); it('accepts enum literals as input', async () => { expect( @@ -173,6 +188,21 @@ describe('Type System: Enum Values', () => { }); }); + it('accepts enum literals as input arguments to subscriptions', async () => { + expect( + await graphql( + schema, + 'subscription x($color: Color!) { subscribeToEnum(color: $color) }', + null, + { color: 'GREEN' } + ) + ).to.deep.equal({ + data: { + subscribeToEnum: 'GREEN' + } + }); + }); + it('does not accept internal value as enum variable', async () => { expect( await graphql( diff --git a/src/type/__tests__/introspection.js b/src/type/__tests__/introspection.js index 070d884bdd..52ef9dae32 100644 --- a/src/type/__tests__/introspection.js +++ b/src/type/__tests__/introspection.js @@ -42,6 +42,7 @@ describe('Introspection', () => { data: { __schema: { mutationType: null, + subscriptionType: null, queryType: { name: 'QueryRoot', }, @@ -106,6 +107,17 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null }, + { + name: 'subscriptionType', + args: [], + type: { + kind: 'OBJECT', + name: '__Type', + ofType: null + }, + isDeprecated: false, + deprecationReason: null + }, { name: 'directives', args: [], @@ -1175,7 +1187,8 @@ describe('Introspection', () => { description: 'A GraphQL Schema defines the capabilities of a ' + 'GraphQL server. It exposes all available types and ' + 'directives on the server, as well as the entry ' + - 'points for query and mutation operations.', + 'points for query, mutation, ' + + 'and subscription operations.', fields: [ { name: 'types', diff --git a/src/type/__tests__/validation.js b/src/type/__tests__/validation.js index c9b06100b9..f46da17fd9 100644 --- a/src/type/__tests__/validation.js +++ b/src/type/__tests__/validation.js @@ -137,6 +137,20 @@ describe('Type System: A Schema must have Object root types', () => { }).not.to.throw(); }); + it('accepts a Schema whose query and subscription types are object types', () => { + expect(() => { + var SubscriptionType = new GraphQLObjectType({ + name: 'Subscription', + fields: { subscribe: { type: GraphQLString } } + }); + + return new GraphQLSchema({ + query: SomeObjectType, + subscription: SubscriptionType + }); + }).not.to.throw(); + }); + it('rejects a Schema without a query type', () => { expect( () => new GraphQLSchema({ }) @@ -164,6 +178,17 @@ describe('Type System: A Schema must have Object root types', () => { ); }); + it('rejects a Schema whose subscription type is an input type', () => { + expect( + () => new GraphQLSchema({ + query: SomeObjectType, + subscription: SomeInputObjectType + }) + ).to.throw( + 'Schema subscription must be Object Type if provided but got: SomeInputObject.' + ); + }); + }); describe('Type System: A Schema must contain uniquely named types', () => { diff --git a/src/type/introspection.js b/src/type/introspection.js index ee433be8fa..b3910a76f0 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -30,7 +30,7 @@ export var __Schema = new GraphQLObjectType({ description: 'A GraphQL Schema defines the capabilities of a GraphQL server. It ' + 'exposes all available types and directives on the server, as well as ' + - 'the entry points for query and mutation operations.', + 'the entry points for query, mutation, and subscription operations.', fields: () => ({ types: { description: 'A list of all types supported by this server.', diff --git a/src/utilities/__tests__/buildASTSchema.js b/src/utilities/__tests__/buildASTSchema.js index 14786d10cf..a49f79677a 100644 --- a/src/utilities/__tests__/buildASTSchema.js +++ b/src/utilities/__tests__/buildASTSchema.js @@ -20,9 +20,9 @@ import { buildASTSchema } from '../buildASTSchema'; * into an in-memory GraphQLSchema, and then finally * printing that GraphQL into the DSL */ -function cycleOutput(body, queryType, mutationType) { +function cycleOutput(body, queryType, mutationType, subscriptionType) { var ast = parse(body); - var schema = buildASTSchema(ast, queryType, mutationType); + var schema = buildASTSchema(ast, queryType, mutationType, subscriptionType); return '\n' + printSchema(schema); } @@ -254,6 +254,22 @@ type Mutation { expect(output).to.equal(body); }); + it('Simple type with subscription', () => { + var body = ` +type HelloScalars { + str: String + int: Int + bool: Boolean +} + +type Subscription { + subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars +} +`; + var output = cycleOutput(body, 'HelloScalars', null, 'Subscription'); + expect(output).to.equal(body); + }); + it('Unreferenced type implementing referenced interface', () => { var body = ` type Concrete implements Iface { @@ -342,6 +358,21 @@ type Hello { .to.throw('Specified mutation type Wat not found in document'); }); + it('Unknown subscription type', () => { + var body = ` +type Hello { + str: String +} + +type Wat { + str: String +} +`; + var doc = parse(body); + expect(() => buildASTSchema(doc, 'Hello', 'Wat', 'Awesome')) + .to.throw('Specified subscription type Awesome not found in document'); + }); + it('Rejects query names', () => { var body = `query Foo { field }`; var doc = parse(body); diff --git a/src/utilities/__tests__/buildClientSchema.js b/src/utilities/__tests__/buildClientSchema.js index 5b8baa28d4..2473d8b6b7 100644 --- a/src/utilities/__tests__/buildClientSchema.js +++ b/src/utilities/__tests__/buildClientSchema.js @@ -62,7 +62,7 @@ describe('Type System: build schema from introspection', () => { await testSchema(schema); }); - it('builds a simple schema with a mutation type', async () => { + it('builds a simple schema with both operation types', async () => { var queryType = new GraphQLObjectType({ name: 'QueryType', description: 'This is a simple query type', @@ -88,9 +88,21 @@ describe('Type System: build schema from introspection', () => { } }); + var subscriptionType = new GraphQLObjectType({ + name: 'SubscriptionType', + description: 'This is a simple subscription type', + fields: { + string: { + type: GraphQLString, + description: 'This is a string field' + } + } + }); + var schema = new GraphQLSchema({ query: queryType, - mutation: mutationType + mutation: mutationType, + subscription: subscriptionType }); await testSchema(schema); diff --git a/src/utilities/__tests__/getOperationAST.js b/src/utilities/__tests__/getOperationAST.js index 0c1f149499..3c17551129 100644 --- a/src/utilities/__tests__/getOperationAST.js +++ b/src/utilities/__tests__/getOperationAST.js @@ -19,35 +19,53 @@ describe('getOperationAST', () => { expect(getOperationAST(doc)).to.equal(doc.definitions[0]); }); - it('Gets an operation from a document with named operation', () => { + it('Gets an operation from a document with named op (mutation)', () => { var doc = parse(`mutation Test { field }`); expect(getOperationAST(doc)).to.equal(doc.definitions[0]); }); + it('Gets an operation from a document with named op (subscription)', () => { + var doc = parse(`subscription Test { field }`); + expect(getOperationAST(doc)).to.equal(doc.definitions[0]); + }); + it('Does not get missing operation', () => { var doc = parse(`type Foo { field: String }`); expect(getOperationAST(doc)).to.equal(null); }); it('Does not get ambiguous unnamed operation', () => { - var doc = parse(`{ field } mutation Test { field }`); + var doc = parse(` + { field } + mutation Test { field } + subscription TestSub { field }`); expect(getOperationAST(doc)).to.equal(null); }); it('Does not get ambiguous named operation', () => { - var doc = parse(`query TestQ { field } mutation TestM { field }`); + var doc = parse(` + query TestQ { field } + mutation TestM { field } + subscription TestS { field }`); expect(getOperationAST(doc)).to.equal(null); }); it('Does not get misnamed operation', () => { - var doc = parse(`query TestQ { field } mutation TestM { field }`); + var doc = parse(` + query TestQ { field } + mutation TestM { field } + subscription TestS { field }`); expect(getOperationAST(doc, 'Unknown')).to.equal(null); }); it('Gets named operation', () => { - var doc = parse(`query TestQ { field } mutation TestM { field }`); + var doc = parse(` + query TestQ { field } + mutation TestM { field } + subscription TestS { field }`); expect(getOperationAST(doc, 'TestQ')).to.equal(doc.definitions[0]); expect(getOperationAST(doc, 'TestM')).to.equal(doc.definitions[1]); + expect(getOperationAST(doc, 'TestS')).to.equal(doc.definitions[2]); }); }); diff --git a/src/validation/__tests__/LoneAnonymousOperation.js b/src/validation/__tests__/LoneAnonymousOperation.js index ff86b839a6..9f66c721f0 100644 --- a/src/validation/__tests__/LoneAnonymousOperation.js +++ b/src/validation/__tests__/LoneAnonymousOperation.js @@ -77,7 +77,7 @@ describe('Validate: Anonymous operation must be alone', () => { ]); }); - it('anon operation with another operation', () => { + it('anon operation with a mutation', () => { expectFailsRule(LoneAnonymousOperation, ` { fieldA @@ -90,4 +90,17 @@ describe('Validate: Anonymous operation must be alone', () => { ]); }); + it('anon operation with a subscription', () => { + expectFailsRule(LoneAnonymousOperation, ` + { + fieldA + } + subscription Foo { + fieldB + } + `, [ + anonNotAlone(2, 7) + ]); + }); + }); diff --git a/src/validation/__tests__/UniqueOperationNames.js b/src/validation/__tests__/UniqueOperationNames.js index 0faa286f79..6a9a1a1c9f 100644 --- a/src/validation/__tests__/UniqueOperationNames.js +++ b/src/validation/__tests__/UniqueOperationNames.js @@ -69,6 +69,10 @@ describe('Validate: Unique operation names', () => { mutation Bar { field } + + subscription Baz { + field + } `); }); @@ -96,7 +100,7 @@ describe('Validate: Unique operation names', () => { ]); }); - it('multiple operations of same name of different types', () => { + it('multiple ops of same name of different types (mutation)', () => { expectFailsRule(UniqueOperationNames, ` query Foo { fieldA @@ -109,4 +113,17 @@ describe('Validate: Unique operation names', () => { ]); }); + it('multiple ops of same name of different types (subscription)', () => { + expectFailsRule(UniqueOperationNames, ` + query Foo { + fieldA + } + subscription Foo { + fieldB + } + `, [ + duplicateOp('Foo', 2, 13, 5, 20) + ]); + }); + });