diff --git a/pom.xml b/pom.xml index 0e14abc8..1528cd35 100644 --- a/pom.xml +++ b/pom.xml @@ -155,9 +155,9 @@ - org.spockframework - spock-core - 1.0-groovy-2.4 + junit + junit + 4.12 test @@ -209,7 +209,6 @@ src/test/kotlin - src/test/groovy @@ -240,23 +239,6 @@ - - - org.codehaus.gmavenplus - gmavenplus-plugin - 1.5 - - - - generateStubs - testGenerateStubs - addTestSources - testCompile - - - - - org.apache.maven.plugins maven-compiler-plugin @@ -279,7 +261,6 @@ - **/*Spec.* **/*Test.* diff --git a/src/test/groovy/graphql/kickstart/tools/BuiltInIdSpec.groovy b/src/test/groovy/graphql/kickstart/tools/BuiltInIdSpec.groovy deleted file mode 100644 index a9bfb62f..00000000 --- a/src/test/groovy/graphql/kickstart/tools/BuiltInIdSpec.groovy +++ /dev/null @@ -1,134 +0,0 @@ -package graphql.kickstart.tools - -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import spock.lang.Shared -import spock.lang.Specification - -class BuiltInIdSpec extends Specification { - - @Shared - GraphQL gql - - def setupSpec() { - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - itemByLongId(id: ID!): Item1! - itemsByLongIds(ids: [ID!]!): [Item1!]! - itemByUuidId(id: ID!): Item2! - itemsByUuidIds(ids: [ID!]!): [Item2!]! - } - - type Item1 { - id: ID! - } - - type Item2 { - id: ID! - } - '''.stripIndent()) - .resolvers(new QueryWithLongItemResolver()) - .build() - .makeExecutableSchema() - gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - } - - def "supports Long as ID as input"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemByLongId(id: 1) { - id - } - } - ''' - } - - then: - data.itemByLongId != null - data.itemByLongId.id == "1" - } - - def "supports list of Long as ID as input"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemsByLongIds(ids: [1,2,3]) { - id - } - } - ''' - } - - then: - data.itemsByLongIds != null - data.itemsByLongIds.size == 3 - data.itemsByLongIds[0].id == "1" - } - - def "supports UUID as ID as input"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemByUuidId(id: "00000000-0000-0000-0000-000000000000") { - id - } - } - ''' - } - - then: - data.itemByUuidId != null - data.itemByUuidId.id == "00000000-0000-0000-0000-000000000000" - } - - def "supports list of UUID as ID as input"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemsByUuidIds(ids: ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111","22222222-2222-2222-2222-222222222222"]) { - id - } - } - ''' - } - - then: - data.itemsByUuidIds != null - data.itemsByUuidIds.size == 3 - data.itemsByUuidIds[0].id == "00000000-0000-0000-0000-000000000000" - } - - class QueryWithLongItemResolver implements GraphQLQueryResolver { - Item1 itemByLongId(Long id) { - new Item1(id: id) - } - - List itemsByLongIds(List ids) { - ids.collect { new Item1(id: it) } - } - - Item2 itemByUuidId(UUID id) { - new Item2(id: id) - } - - List itemsByUuidIds(List ids) { - ids.collect { new Item2(id: it) } - } - } - - class Item1 { - Long id - } - - class Item2 { - UUID id - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy b/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy deleted file mode 100644 index e654d455..00000000 --- a/src/test/groovy/graphql/kickstart/tools/EndToEndSpec.groovy +++ /dev/null @@ -1,653 +0,0 @@ -package graphql.kickstart.tools - -import graphql.ExecutionInput -import graphql.ExecutionResult -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import org.reactivestreams.Publisher -import org.reactivestreams.Subscriber -import org.reactivestreams.tck.TestEnvironment -import spock.lang.Shared -import spock.lang.Specification - -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -/** - * @author Andrew Potter - */ -class EndToEndSpec extends Specification { - - @Shared - GraphQL gql - - def setupSpec() { - GraphQLSchema schema = EndToEndSpecHelperKt.createSchema() - - gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - } - - def "schema comments are used as descriptions"() { - expect: - gql.graphQLSchema.allTypesAsList.find { it.name == 'Type' }?.valueDefinitionMap?.TYPE_1?.description == "Item type 1" - } - - def "generated schema should respond to simple queries"() { - when: - Utils.assertNoGraphQlErrors(gql) { - ''' - { - items(itemsInput: {name: "item1"}) { - id - type - } - } - ''' - } - - then: - noExceptionThrown() - } - - def "generated schema should respond to simple mutations"() { - when: - def data = Utils.assertNoGraphQlErrors(gql, [name: "new1", type: Type.TYPE_2.toString()]) { - ''' - mutation addNewItem($name: String!, $type: Type!) { - addItem(newItem: {name: $name, type: $type}) { - id - name - type - } - } - ''' - } - - then: - data.addItem - } - - def "generated schema should execute the subscription query"() { - when: - def newItem = new Item(1, "item", Type.TYPE_1, UUID.randomUUID(), []) - def returnedItem = null - def data = Utils.assertNoGraphQlErrors(gql, [:], new OnItemCreatedContext(newItem)) { - ''' - subscription { - onItemCreated { - id - } - } - ''' - } - CountDownLatch latch = new CountDownLatch(1) - (data as Publisher).subscribe(new Subscriber() { - @Override - void onSubscribe(org.reactivestreams.Subscription s) { - - } - - @Override - void onNext(ExecutionResult executionResult) { - returnedItem = executionResult.data - latch.countDown() - } - - @Override - void onError(Throwable t) { - } - - @Override - void onComplete() { - } - }) - latch.await(3, TimeUnit.SECONDS) - - then: - returnedItem.get("onItemCreated").id == 1 - } - - def "generated schema should handle interface types"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemsByInterface { - name - type - } - } - ''' - } - - then: - data.itemsByInterface - } - - def "generated schema should handle union types"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - allItems { - ... on Item { - id - name - } - ... on OtherItem { - name - type - } - } - } - ''' - } - - then: - data.allItems - } - - def "generated schema should handle nested union types"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - nestedUnionItems { - ... on Item { - itemId: id - } - ... on OtherItem { - otherItemId: id - } - ... on ThirdItem { - thirdItemId: id - } - } - } - ''' - } - - then: - data.nestedUnionItems == [[itemId: 0], [itemId: 1], [otherItemId: 0], [otherItemId: 1], [thirdItemId: 100]] - } - - def "generated schema should handle scalar types"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemByUUID(uuid: "38f685f1-b460-4a54-a17f-7fd69e8cf3f8") { - uuid - } - } - ''' - } - - then: - data.itemByUUID - } - - def "generated schema should handle non nullable scalar types"() { - when: - def fileParts = [new MockPart("test.doc", "Hello"), new MockPart("test.doc", "World")] - def args = ["fileParts": fileParts] - def data = Utils.assertNoGraphQlErrors(gql, args) { - ''' - mutation ($fileParts: [Upload!]!) { echoFiles(fileParts: $fileParts)} - ''' - } - then: - (data["echoFiles"] as ArrayList).join(",") == "Hello,World" - } - - def "generated schema should handle any java.util.Map (using HashMap) types as property maps"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - propertyHashMapItems { - name - age - } - } - ''' - } - - then: - data.propertyHashMapItems == [[name: "bob", age: 55]] - } - - def "generated schema should handle any java.util.Map (using SortedMap) types as property maps"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - propertySortedMapItems { - name - age - } - } - ''' - } - - then: - data.propertySortedMapItems == [[name: "Arthur", age: 76], [name: "Jane", age: 28]] - } - - // In this test a dictionary entry for the schema type ComplexMapItem is defined - // so that it is possible for a POJO mapping to be known since the ComplexMapItem is contained - // in a property map (i.e. Map) and so the normal resolver and schema traversal code - // will not be able to find the POJO since it does not exist as a strongly typed object in - // resolver/POJO graph. - def "generated schema should handle java.util.Map types as property maps when containing complex data"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - propertyMapWithComplexItems { - nameId { - id - } - age - } - } - ''' - } - - then: - data.propertyMapWithComplexItems == [[nameId: [id: 150], age: 72]] - } - - // This behavior is consistent with PropertyDataFetcher - def "property map returns null when a property is not defined."() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - propertyMapMissingNamePropItems { - name - age - } - } - ''' - } - - then: - data.propertyMapMissingNamePropItems == [[name: null, age: 55]] - } - - // In this test a dictonary entry for the schema type NestedComplexMapItem is defined - // however we expect to not be required to define one for the transitive UndiscoveredItem object since - // the schema resolver discovery code should still be able to automatically determine the POJO that - // maps to this schema type. - def "generated schema should continue to associate resolvers for transitive types of a java.util.Map complex data type"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - propertyMapWithNestedComplexItems { - nested { - item { - id - } - } - age - } - } - ''' - } - - then: - data.propertyMapWithNestedComplexItems == [[nested: [item: [id: 63]], age: 72]] - } - - - def "generated schema should handle optional arguments"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - missing: itemsWithOptionalInput { - id - } - - present: itemsWithOptionalInput(itemsInput: {name: "item1"}) { - id - } - } - ''' - } - - then: - data.missing?.size > 1 - data.present?.size == 1 - } - - def "generated schema should handle optional arguments using java.util.Optional"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - missing: itemsWithOptionalInputExplicit { - id - } - - present: itemsWithOptionalInputExplicit(itemsInput: {name: "item1"}) { - id - } - } - ''' - } - - then: - data.missing?.size > 1 - data.present?.size == 1 - } - - def "generated schema should handle optional return types using java.util.Optional"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - missing: optionalItem(itemsInput: {name: "item?"}) { - id - } - - present: optionalItem(itemsInput: {name: "item1"}) { - id - } - } - ''' - } - - then: - data.missing == null - data.present - } - - def "generated schema should pass default arguments"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - defaultArgument - } - ''' - } - - then: - data.defaultArgument == true - } - - def "introspection shouldn't fail for arguments of type list with a default value (defaultEnumListArgument)"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - __type(name: "Query") { - name - fields { - name - args { - name - defaultValue - } - } - } - } - ''' - } - - then: - data.__type - } - - def "generated schema should return null without errors for null value with nested fields"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - complexNullableType { - first - second - third - } - } - ''' - } - - then: - data.containsKey('complexNullableType') - data.complexNullableType == null - } - - def "generated schema handles nested lists in input type fields"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - complexInputType(complexInput: [[{first: "foo", second: [[{first: "bar"}]]}]]) - } - ''' - } - - then: - data.complexInputType - } - - def "generated schema should use type extensions"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - extendedType { - first - second - } - } - ''' - } - - then: - data.extendedType - data.extendedType.first - data.extendedType.second - } - - def "generated schema uses properties if no methods are found"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - propertyField - } - ''' - } - - then: - data.propertyField - } - - def "generated schema allows enums in input types"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - enumInputType(type: TYPE_2) - } - ''' - } - - then: - data.enumInputType == "TYPE_2" - } - - def "generated schema works with custom scalars as input values"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - customScalarMapInputType(customScalarMap: { test: "me" }) - } - ''' - } - - then: - data.customScalarMapInputType == [ - test: "me" - ] - } - - def "generated schema should handle extended input types"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - mutation { - saveUser(input: {name: "John", password: "secret"}) - } - ''' - } - - then: - data.saveUser == "John/secret" - } - - def "generated schema supports generic properties"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemWithGenericProperties { - keys - } - } - ''' - } - - then: - data.itemWithGenericProperties == [ - keys: ["A", "B"] - ] - } - - def "generated schema supports overriding built-in scalars"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - itemByBuiltInId(id: "38f685f1-b460-4a54-a17f-7fd69e8cf3f8") { - name - } - } - ''' - } - - then: - noExceptionThrown() - data.itemByBuiltInId != null - } - - def "generated schema supports DataFetcherResult"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - dataFetcherResult { - name - } - } - ''' - } - - then: - data.dataFetcherResult.name == "item1" - } - - def "generated schema supports Kotlin suspend functions"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - coroutineItems { - id - name - } - } - ''' - } - - then: - data.coroutineItems == [[id: 0, name: "item1"], [id: 1, name: "item2"]] - } - - def "generated schema supports Kotlin coroutine channels for the subscription query"() { - when: - def newItem = new Item(1, "item", Type.TYPE_1, UUID.randomUUID(), []) - def data = Utils.assertNoGraphQlErrors(gql, [:], new OnItemCreatedContext(newItem)) { - ''' - subscription { - onItemCreatedCoroutineChannel { - id - } - } - ''' - } - def subscriber = new TestEnvironment().newManualSubscriber(data as Publisher) - - then: - subscriber.requestNextElement().data.get("onItemCreatedCoroutineChannel").id == 1 - subscriber.expectCompletion() - } - - def "generated schema supports Kotlin coroutine channels with suspend function for the subscription query"() { - when: - def newItem = new Item(1, "item", Type.TYPE_1, UUID.randomUUID(), []) - def data = Utils.assertNoGraphQlErrors(gql, [:], new OnItemCreatedContext(newItem)) { - ''' - subscription { - onItemCreatedCoroutineChannelAndSuspendFunction { - id - } - } - ''' - } - def subscriber = new TestEnvironment().newManualSubscriber(data as Publisher) - - then: - subscriber.requestNextElement().data.get("onItemCreatedCoroutineChannelAndSuspendFunction").id == 1 - subscriber.expectCompletion() - } - - def "generated schema supports arrays"() { - when: - def data = Utils.assertNoGraphQlErrors(gql) { - ''' - { - arrayItems { - name - } - } - ''' - } - - then: - data.arrayItems.collect { it.name } == ['item1', 'item2'] - } - - def "generated schema should re-throw original runtime exception when executing a resolver method"() { - when: - - gql.execute(ExecutionInput.newExecutionInput().query(''' - { - throwsIllegalArgumentException - } - ''' - )) - - then: - IllegalArgumentException - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/EnumDefaultValueSpec.groovy b/src/test/groovy/graphql/kickstart/tools/EnumDefaultValueSpec.groovy deleted file mode 100644 index e9813bc2..00000000 --- a/src/test/groovy/graphql/kickstart/tools/EnumDefaultValueSpec.groovy +++ /dev/null @@ -1,59 +0,0 @@ -package graphql.kickstart.tools - -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import spock.lang.Specification - -class EnumDefaultValueSpec extends Specification { - - def "enumvalue is not passed down to graphql-java"() { - when: - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - test(input: MySortSpecifier): SortBy - } - input MySortSpecifier { - sortBy: SortBy = createdOn - value: Int = 10 - } - enum SortBy { - createdOn - updatedOn - } - ''').resolvers(new GraphQLQueryResolver() { - - SortBy test(MySortSpecifier input) { - return input.sortBy - } - - }) - .build() - .makeExecutableSchema() - GraphQL gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - def data = Utils.assertNoGraphQlErrors(gql, [input: [value: 1]]) { - ''' - query test($input: MySortSpecifier) { - test(input: $input) - } - ''' - } - - then: - noExceptionThrown() - data.test == 'createdOn' - } - - static class MySortSpecifier { - SortBy sortBy - int value - } - - enum SortBy { - createdOn, - updatedOn - } - -} diff --git a/src/test/groovy/graphql/kickstart/tools/EnumListParameterSpec.groovy b/src/test/groovy/graphql/kickstart/tools/EnumListParameterSpec.groovy deleted file mode 100644 index 0166f84c..00000000 --- a/src/test/groovy/graphql/kickstart/tools/EnumListParameterSpec.groovy +++ /dev/null @@ -1,76 +0,0 @@ -package graphql.kickstart.tools - -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import spock.lang.Shared -import spock.lang.Specification - -class EnumListParameterSpec extends Specification { - - @Shared - GraphQL gql - - def setupSpec() { - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - countries(regions: [Region!]!): [Country!]! - } - - enum Region { - EUROPE - ASIA - } - - type Country { - code: String! - name: String! - regions: [Region!] - } - '''.stripIndent()) - .resolvers(new QueryResolver()) - .build() - .makeExecutableSchema() - gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - } - - def "query with parameter type list of enums should resolve correctly"() { - when: - def data = Utils.assertNoGraphQlErrors(gql, [regions: ["EUROPE", "ASIA"]]) { - ''' - query getCountries($regions: [Region!]!) { - countries(regions: $regions){ - code - name - regions - } - } - ''' - } - - then: - data.countries == [] - } - - class QueryResolver implements GraphQLQueryResolver { - Set getCountries(Set regions) { - return Collections.emptySet() - } - } - - class Country { - String code - String name - List regions - } - - enum Region { - EUROPE, - ASIA - } - -} - - diff --git a/src/test/groovy/graphql/kickstart/tools/FieldResolverScannerSpec.groovy b/src/test/groovy/graphql/kickstart/tools/FieldResolverScannerSpec.groovy deleted file mode 100644 index e2a14b89..00000000 --- a/src/test/groovy/graphql/kickstart/tools/FieldResolverScannerSpec.groovy +++ /dev/null @@ -1,141 +0,0 @@ -package graphql.kickstart.tools - -import graphql.kickstart.tools.resolver.FieldResolverError -import graphql.kickstart.tools.resolver.FieldResolverScanner -import graphql.kickstart.tools.resolver.MethodFieldResolver -import graphql.kickstart.tools.resolver.PropertyFieldResolver -import graphql.language.FieldDefinition -import graphql.language.TypeName -import graphql.relay.Connection -import spock.lang.Specification - -/** - * @author Andrew Potter - */ -class FieldResolverScannerSpec extends Specification { - - private static final SchemaParserOptions options = SchemaParserOptions.defaultOptions() - private static final FieldResolverScanner scanner = new FieldResolverScanner(options) - - def "scanner finds fields on multiple root types"() { - setup: - def resolver = new RootResolverInfo([new RootQuery1(), new RootQuery2()], options) - - when: - def result1 = scanner.findFieldResolver(new FieldDefinition("field1", new TypeName("String")), resolver) - def result2 = scanner.findFieldResolver(new FieldDefinition("field2", new TypeName("String")), resolver) - - then: - result1.search.source != result2.search.source - } - - def "scanner throws exception when more than one resolver method is found"() { - setup: - def resolver = new RootResolverInfo([new RootQuery1(), new DuplicateQuery()], options) - - when: - scanner.findFieldResolver(new FieldDefinition("field1", new TypeName("String")), resolver) - - then: - thrown(FieldResolverError) - } - - def "scanner throws exception when no resolver methods are found"() { - setup: - def resolver = new RootResolverInfo([], options) - - when: - scanner.findFieldResolver(new FieldDefinition("field1", new TypeName("String")), resolver) - - then: - thrown(FieldResolverError) - } - - def "scanner finds properties when no method is found"() { - setup: - def resolver = new RootResolverInfo([new PropertyQuery()], options) - - when: - def name = scanner.findFieldResolver(new FieldDefinition("name", new TypeName("String")), resolver) - def version = scanner.findFieldResolver(new FieldDefinition("version", new TypeName("Integer")), resolver) - - then: - name instanceof PropertyFieldResolver - version instanceof PropertyFieldResolver - } - - def "scanner finds generic return type"() { - setup: - def resolver = new RootResolverInfo([new GenericQuery()], options) - - when: - def users = scanner.findFieldResolver(new FieldDefinition("users", new TypeName("UserConnection")), resolver) - - then: - users instanceof MethodFieldResolver - } - - def "scanner prefers concrete resolver"() { - setup: - def resolver = new DataClassResolverInfo(Kayak.class) - - when: - def meta = scanner.findFieldResolver(new FieldDefinition("information", new TypeName("VehicleInformation")), resolver) - - then: - meta instanceof MethodFieldResolver - ((MethodFieldResolver) meta).getMethod().getReturnType() == BoatInformation.class - } - - def "scanner finds field resolver method using camelCase for snake_cased field_name"() { - setup: - def resolver = new RootResolverInfo([new CamelCaseQuery1()], options) - - when: - def meta = scanner.findFieldResolver(new FieldDefinition("hull_type", new TypeName("HullType")), resolver) - - then: - meta instanceof MethodFieldResolver - ((MethodFieldResolver) meta).getMethod().getReturnType() == HullType.class - } - - class RootQuery1 implements GraphQLQueryResolver { - def field1() {} - } - - class RootQuery2 implements GraphQLQueryResolver { - def field2() {} - } - - class DuplicateQuery implements GraphQLQueryResolver { - def field1() {} - } - - class CamelCaseQuery1 implements GraphQLQueryResolver { - HullType getHullType(){} - } - - class HullType {} - - class ParentPropertyQuery { - private Integer version = 1 - } - - class PropertyQuery extends ParentPropertyQuery implements GraphQLQueryResolver { - private String name = "name" - } - - class User {} - - class GenericQuery implements GraphQLQueryResolver { - Connection getUsers() {} - } - - abstract class Boat implements Vehicle { - BoatInformation getInformation() { return this.information; } - } - - class BoatInformation implements VehicleInformation {} - - class Kayak extends Boat {} -} diff --git a/src/test/groovy/graphql/kickstart/tools/GenericResolverSpec.groovy b/src/test/groovy/graphql/kickstart/tools/GenericResolverSpec.groovy deleted file mode 100644 index ea67500a..00000000 --- a/src/test/groovy/graphql/kickstart/tools/GenericResolverSpec.groovy +++ /dev/null @@ -1,84 +0,0 @@ -package graphql.kickstart.tools - - -import spock.lang.Specification - -class GenericResolverSpec extends Specification { - - def "methods from generic resolvers are resolved"() { - when: - SchemaParser.newParser().schemaString('''\ - type Query { - bar: Bar! - } - - type Bar { - value: String - } - ''') - .resolvers(new QueryResolver1(), new BarResolver()) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - class QueryResolver1 implements GraphQLQueryResolver { - Bar getBar() { - return new Bar() - } - } - - class Bar { - } - - abstract class FooResolver implements GraphQLResolver { - String getValue(T foo) { - return "value" - } - } - - class BarResolver extends FooResolver implements GraphQLResolver { - - } - - - def "methods from generic inherited resolvers are resolved"() { - when: - SchemaParser.newParser().schemaString('''\ - type Query { - car: Car! - } - type Car { - value: String - } - ''') - .resolvers(new QueryResolver2(), new CarResolver()) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - - class QueryResolver2 implements GraphQLQueryResolver { - Car getCar() { - return new Car() - } - } - - abstract class FooGraphQLResolver implements GraphQLResolver { - String getValue(T foo) { - return "value" - } - } - - class Car { - } - - class CarResolver extends FooGraphQLResolver { - - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/InterfaceImplementation.groovy b/src/test/groovy/graphql/kickstart/tools/InterfaceImplementation.groovy deleted file mode 100644 index a453f703..00000000 --- a/src/test/groovy/graphql/kickstart/tools/InterfaceImplementation.groovy +++ /dev/null @@ -1,11 +0,0 @@ -package graphql.kickstart.tools - -class InterfaceImplementation implements GraphQLQueryResolver { - NamedResource query1() { null } - - NamedResourceImpl query2() { null } - - static class NamedResourceImpl implements NamedResource { - String name() {} - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/MethodFieldResolverDataFetcherSpec.groovy b/src/test/groovy/graphql/kickstart/tools/MethodFieldResolverDataFetcherSpec.groovy deleted file mode 100644 index c7ca1c34..00000000 --- a/src/test/groovy/graphql/kickstart/tools/MethodFieldResolverDataFetcherSpec.groovy +++ /dev/null @@ -1,254 +0,0 @@ -package graphql.kickstart.tools - -import graphql.Assert -import graphql.ExecutionResult -import graphql.execution.* -import graphql.execution.instrumentation.SimpleInstrumentation -import graphql.kickstart.tools.resolver.FieldResolverError -import graphql.kickstart.tools.resolver.FieldResolverScanner -import graphql.language.FieldDefinition -import graphql.language.InputValueDefinition -import graphql.language.NonNullType -import graphql.language.TypeName -import graphql.schema.DataFetcher -import graphql.schema.DataFetchingEnvironment -import graphql.schema.DataFetchingEnvironmentImpl -import spock.lang.Specification - -import java.util.concurrent.CompletableFuture - -/** - * @author Andrew Potter - */ -class MethodFieldResolverDataFetcherSpec extends Specification { - - def "data fetcher throws exception if resolver has too many arguments"() { - when: - createFetcher("active", new GraphQLQueryResolver() { - boolean active(def arg1, def arg2) { true } - }) - - then: - thrown(FieldResolverError) - } - - def "data fetcher throws exception if resolver has too few arguments"() { - when: - createFetcher("active", [new InputValueDefinition("doesNotExist", new TypeName("Boolean"))], new GraphQLQueryResolver() { - boolean active() { true } - }) - - then: - thrown(FieldResolverError) - } - - def "data fetcher prioritizes methods on the resolver"() { - setup: - def name = "Resolver Name" - def resolver = createFetcher("name", new GraphQLResolver() { - String getName(DataClass dataClass) { name } - }) - - expect: - resolver.get(createEnvironment(new DataClass())) == name - } - - def "data fetcher uses data class methods if no resolver method is given"() { - setup: - def resolver = createFetcher("name", new GraphQLResolver() {}) - - expect: - resolver.get(createEnvironment(new DataClass())) == DataClass.name - } - - def "data fetcher prioritizes methods without a prefix"() { - setup: - def name = "correct name" - def resolver = createFetcher("name", new GraphQLResolver() { - String name(DataClass dataClass) { name } - - String getName(DataClass dataClass) { "in" + name } - }) - - expect: - resolver.get(createEnvironment(new DataClass())) == name - } - - def "data fetcher uses 'is' prefix for booleans (primitive type)"() { - setup: - def resolver = createFetcher("active", new GraphQLResolver() { - boolean isActive(DataClass dataClass) { true } - - boolean getActive(DataClass dataClass) { true } - }) - - expect: - resolver.get(createEnvironment(new DataClass())) - } - - def "data fetcher uses 'is' prefix for Booleans (Object type)"() { - setup: - def resolver = createFetcher("active", new GraphQLResolver() { - Boolean isActive(DataClass dataClass) { Boolean.TRUE } - - Boolean getActive(DataClass dataClass) { Boolean.TRUE } - }) - - expect: - resolver.get(createEnvironment(new DataClass())) - } - - def "data fetcher passes environment if method has extra argument"() { - setup: - def resolver = createFetcher("active", new GraphQLResolver() { - boolean isActive(DataClass dataClass, DataFetchingEnvironment env) { - env instanceof DataFetchingEnvironment - } - }) - - expect: - resolver.get(createEnvironment(new DataClass())) - } - - def "data fetcher passes environment if method has extra argument even if context is specified"() { - setup: - def options = SchemaParserOptions.newOptions().contextClass(ContextClass).build() - def resolver = createFetcher(options, "active", new GraphQLResolver() { - boolean isActive(DataClass dataClass, DataFetchingEnvironment env) { - env instanceof DataFetchingEnvironment - } - }) - - expect: - resolver.get(createEnvironment(new ContextClass(), new DataClass())) - } - - def "data fetcher passes context if method has extra argument and context is specified"() { - setup: - def context = new ContextClass() - def options = SchemaParserOptions.newOptions().contextClass(ContextClass).build() - def resolver = createFetcher(options, "active", new GraphQLResolver() { - boolean isActive(DataClass dataClass, ContextClass ctx) { - ctx == context - } - }) - - expect: - resolver.get(createEnvironment(context, new DataClass())) - } - - def "data fetcher marshalls input object if required"() { - setup: - def name = "correct name" - def resolver = createFetcher("active", [new InputValueDefinition("input", new TypeName("InputClass"))], new GraphQLQueryResolver() { - boolean active(InputClass input) { - input instanceof InputClass && input.name == name - } - }) - - expect: - resolver.get(createEnvironment([input: [name: name]])) - } - - def "data fetcher doesn't marshall input object if not required"() { - setup: - def name = "correct name" - def resolver = createFetcher("active", [new InputValueDefinition("input", new TypeName("Map"))], new GraphQLQueryResolver() { - boolean active(Map input) { - input instanceof Map && input.name == name - } - }) - - expect: - resolver.get(createEnvironment([input: [name: name]])) - } - - def "data fetcher returns null if nullable argument is passed null"() { - setup: - def resolver = createFetcher("echo", [new InputValueDefinition("message", new TypeName("String"))], new GraphQLQueryResolver() { - String echo(String message) { - return message - } - }) - - expect: - resolver.get(createEnvironment()) == null - } - - def "data fetcher throws exception if non-null argument is passed null"() { - setup: - def resolver = createFetcher("echo", [new InputValueDefinition("message", new NonNullType(new TypeName("String")))], new GraphQLQueryResolver() { - String echo(String message) { - return message - } - }) - - when: - resolver.get(createEnvironment()) - - then: - thrown(ResolverError) - } - - private static DataFetcher createFetcher(String methodName, List arguments = [], GraphQLResolver resolver) { - return createFetcher(SchemaParserOptions.defaultOptions(), methodName, arguments, resolver) - } - - private static DataFetcher createFetcher(SchemaParserOptions options, String methodName, List arguments = [], GraphQLResolver resolver) { - def field = FieldDefinition.newFieldDefinition() - .name(methodName) - .type(new TypeName('Boolean')) - .inputValueDefinitions(arguments) - .build() - - new FieldResolverScanner(options).findFieldResolver(field, resolver instanceof GraphQLQueryResolver ? new RootResolverInfo([resolver], options) : new NormalResolverInfo(resolver, options)).createDataFetcher() - } - - private static DataFetchingEnvironment createEnvironment(Map arguments = [:]) { - createEnvironment(new Object(), arguments) - } - - private static DataFetchingEnvironment createEnvironment(Object source, Map arguments = [:]) { - createEnvironment(null, source, arguments) - } - - private static DataFetchingEnvironment createEnvironment(Object context, Object source, Map arguments = [:]) { - DataFetchingEnvironmentImpl.newDataFetchingEnvironment(buildExecutionContext()) - .source(source) - .arguments(arguments) - .context(context) - .build() - } - - private static ExecutionContext buildExecutionContext() { - ExecutionStrategy executionStrategy = new ExecutionStrategy() { - @Override - CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { - return Assert.assertShouldNeverHappen("should not be called") - } - } - ExecutionId executionId = ExecutionId.from("executionId123") - ExecutionContextBuilder.newExecutionContextBuilder() - .instrumentation(SimpleInstrumentation.INSTANCE) - .executionId(executionId) - .queryStrategy(executionStrategy) - .mutationStrategy(executionStrategy) - .subscriptionStrategy(executionStrategy) - .build() - } - - class DataClass { - private static final String name = "Data Class Name" - - String getName() { - name - } - } - - static class InputClass { - String name - } - - class ContextClass { - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/MultiResolverSpec.groovy b/src/test/groovy/graphql/kickstart/tools/MultiResolverSpec.groovy deleted file mode 100644 index 00451ac1..00000000 --- a/src/test/groovy/graphql/kickstart/tools/MultiResolverSpec.groovy +++ /dev/null @@ -1,80 +0,0 @@ -package graphql.kickstart.tools - -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import spock.lang.Shared -import spock.lang.Specification - -class MultiResolverSpec extends Specification { - - @Shared - GraphQL gql - - def setupSpec() { - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - person: Person - } - - type Person { - name: String! - friends(friendName: String!): [Friend!]! - } - - type Friend { - name: String! - } - '''.stripIndent()) - .resolvers(new QueryWithPersonResolver(), new PersonFriendResolver(), new PersonNameResolver()) - .build() - .makeExecutableSchema() - gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - } - - def "multiple resolvers for one data class should resolve methods with arguments"() { - when: - def data = Utils.assertNoGraphQlErrors(gql, [friendName: "name"]) { - ''' - query friendOfPerson($friendName: String!) { - person { - friends(friendName: $friendName) { - name - } - } - } - ''' - } - - then: - data.person - } - - class QueryWithPersonResolver implements GraphQLQueryResolver { - Person getPerson() { - new Person() - } - } - - class Person { - - } - - class Friend { - String name - } - - class PersonFriendResolver implements GraphQLResolver { - List friends(Person person, String friendName) { - Collections.emptyList() - } - } - - class PersonNameResolver implements GraphQLResolver { - String name(Person person) { - "name" - } - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/MultipleInterfaces.groovy b/src/test/groovy/graphql/kickstart/tools/MultipleInterfaces.groovy deleted file mode 100644 index 8c8783f0..00000000 --- a/src/test/groovy/graphql/kickstart/tools/MultipleInterfaces.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package graphql.kickstart.tools - -class MultipleInterfaces implements GraphQLQueryResolver { - NamedResourceImpl query1() { null } - - VersionedResourceImpl query2() { null } - - static class NamedResourceImpl implements NamedResource { - String name() {} - } - - static class VersionedResourceImpl implements VersionedResource { - int version() {} - } -} - - diff --git a/src/test/groovy/graphql/kickstart/tools/NamedResource.groovy b/src/test/groovy/graphql/kickstart/tools/NamedResource.groovy deleted file mode 100644 index 0f871caf..00000000 --- a/src/test/groovy/graphql/kickstart/tools/NamedResource.groovy +++ /dev/null @@ -1,5 +0,0 @@ -package graphql.kickstart.tools - -interface NamedResource { - String name() -} diff --git a/src/test/groovy/graphql/kickstart/tools/NestedInputTypesSpec.groovy b/src/test/groovy/graphql/kickstart/tools/NestedInputTypesSpec.groovy deleted file mode 100644 index d0ce1a68..00000000 --- a/src/test/groovy/graphql/kickstart/tools/NestedInputTypesSpec.groovy +++ /dev/null @@ -1,127 +0,0 @@ -package graphql.kickstart.tools - -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import spock.lang.Specification - -class NestedInputTypesSpec extends Specification { - - def "nested input types are parsed"() { - when: - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - materials(filter: MaterialFilter): [Material!]! - } - - input MaterialFilter { - title: String - requestFilter: RequestFilter - } - - input RequestFilter { - and: [RequestFilter!] - or: [RequestFilter!] - discountTypeFilter: DiscountTypeFilter - } - - input DiscountTypeFilter { - name: String - } - - type Material { - id: ID! - } - ''').resolvers(new QueryResolver()) - .build() - .makeExecutableSchema() - GraphQL gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - def data = Utils.assertNoGraphQlErrors(gql, [filter: [title: "title", requestFilter: [discountTypeFilter: [name: "discount"]]]]) { - ''' - query materials($filter: MaterialFilter!) { - materials(filter: $filter) { - id - } - } - ''' - } - - then: - noExceptionThrown() - data.materials == [] - } - - def "nested input in extensions are parsed"() { - when: - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - materials(filter: MaterialFilter): [Material!]! - } - - input MaterialFilter { - title: String - } - - extend input MaterialFilter { - requestFilter: RequestFilter - } - - input RequestFilter { - and: [RequestFilter!] - or: [RequestFilter!] - discountTypeFilter: DiscountTypeFilter - } - - input DiscountTypeFilter { - name: String - } - - type Material { - id: ID! - } - ''').resolvers(new QueryResolver()) - .build() - .makeExecutableSchema() - GraphQL gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - def data = Utils.assertNoGraphQlErrors(gql, [filter: [title: "title", requestFilter: [discountTypeFilter: [name: "discount"]]]]) { - ''' - query materials($filter: MaterialFilter!) { - materials(filter: $filter) { - id - } - } - ''' - } - - then: - noExceptionThrown() - data.materials == [] - } - - class QueryResolver implements GraphQLQueryResolver { - List materials(MaterialFilter filter) { Collections.emptyList() } - } - - class Material { - Long id - } - - static class MaterialFilter { - String title - RequestFilter requestFilter - } - - static class RequestFilter { - List and - List or - DiscountTypeFilter discountTypeFilter - } - - static class DiscountTypeFilter { - String name - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/ParameterizedGetterSpec.groovy b/src/test/groovy/graphql/kickstart/tools/ParameterizedGetterSpec.groovy deleted file mode 100644 index 0d5ccade..00000000 --- a/src/test/groovy/graphql/kickstart/tools/ParameterizedGetterSpec.groovy +++ /dev/null @@ -1,70 +0,0 @@ -package graphql.kickstart.tools - -import graphql.GraphQL -import graphql.execution.AsyncExecutionStrategy -import graphql.schema.GraphQLSchema -import spock.lang.Shared -import spock.lang.Specification - -class ParameterizedGetterSpec extends Specification { - - @Shared - GraphQL gql - - def setupSpec() { - GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ - type Query { - human: Human - } - - type Human { - bestFriends: [Character!]! - allFriends(limit: Int!): [Character!]! - } - - type Character { - name: String! - } - '''.stripIndent()) - .resolvers(new QueryResolver(), new HumanResolver()) - .build() - .makeExecutableSchema() - gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(new AsyncExecutionStrategy()) - .build() - } - - def "parameterized query is resolved on data type instead of on its resolver"() { - when: - def data = Utils.assertNoGraphQlErrors(gql, [limit: 10]) { - ''' - query allFriends($limit: Int!) { - human { - allFriends(limit: $limit) { - name - } - } - } - ''' - } - - then: - data.human - } - - class QueryResolver implements GraphQLQueryResolver { - Human human() { new Human() } - } - - class Human { - List allFriends(int limit) { Collections.emptyList() } - } - - class HumanResolver implements GraphQLResolver { - List bestFriends(Human human) { Collections.emptyList() } - } - - class Character { - String name - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy deleted file mode 100644 index 876ba3c6..00000000 --- a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy +++ /dev/null @@ -1,512 +0,0 @@ -package graphql.kickstart.tools - -import graphql.language.* -import graphql.schema.Coercing -import graphql.schema.GraphQLScalarType -import spock.lang.Specification - -import java.util.concurrent.CompletableFuture - -/** - * @author Andrew Potter - */ -class SchemaClassScannerSpec extends Specification { - - def "scanner handles futures and immediate return types"() { - when: - SchemaParser.newParser() - .resolvers(new FutureImmediateQuery()) - .schemaString(""" - type Query { - future: Int! - immediate: Int! - } - """) - .scan() - then: - noExceptionThrown() - } - - private class FutureImmediateQuery implements GraphQLQueryResolver { - CompletableFuture future() { - CompletableFuture.completedFuture(1) - } - - Integer immediate() { - 1 - } - } - - def "scanner handles primitive and boxed return types"() { - when: - SchemaParser.newParser() - .resolvers(new PrimitiveBoxedQuery()) - .schemaString(""" - type Query { - primitive: Int! - boxed: Int! - } - """) - .scan() - then: - noExceptionThrown() - } - - private class PrimitiveBoxedQuery implements GraphQLQueryResolver { - int primitive() { - 1 - } - - Integer boxed() { - 1 - } - } - - def "scanner handles different scalars with same java class"() { - when: - SchemaParser.newParser() - .resolvers(new ScalarDuplicateQuery()) - .schemaString(""" - type Query { - string: String! - id: ID! - } - """) - .scan() - - then: - noExceptionThrown() - } - - private class ScalarDuplicateQuery implements GraphQLQueryResolver { - String string() { "" } - - String id() { "" } - } - - def "scanner handles interfaces referenced by objects that aren't explicitly used"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .resolvers(new InterfaceMissingQuery()) - .schemaString(""" - interface Interface { - id: ID! - } - - type Query implements Interface { - id: ID! - } - """) - .scan() - - then: - objects.definitions.find { it instanceof InterfaceTypeDefinition } != null - } - - private class InterfaceMissingQuery implements GraphQLQueryResolver { - String id() { "" } - } - - def "scanner handles input types that reference other input types"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .resolvers(new MultipleInputTypeQuery()) - .schemaString(""" - input FirstInput { - id: String! - second: SecondInput! - third: ThirdInput! - } - input SecondInput { - id: String! - } - input ThirdInput { - id: String! - } - - type Query { - test(input: FirstInput): String! - } - """) - .scan() - - then: - objects.definitions.findAll { it instanceof InputObjectTypeDefinition }.size() == 3 - - } - - private class MultipleInputTypeQuery implements GraphQLQueryResolver { - - String test(FirstInput input) { "" } - - class FirstInput { - String id - - SecondInput second() { new SecondInput() } - ThirdInput third - } - - class SecondInput { - String id - } - - class ThirdInput { - String id - } - } - - def "scanner handles input types extensions"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .schemaString(''' - type Query { } - - type Mutation { - save(input: UserInput!): Boolean - } - - input UserInput { - name: String - } - - extend input UserInput { - password: String - } - ''') - .resolvers( - new GraphQLMutationResolver() { - boolean save(Map map) { true } - }, - new GraphQLQueryResolver() {} - ) - .scan() - - then: - objects.definitions.findAll { (it.class == InputObjectTypeExtensionDefinition.class) }.size() == 1 - objects.definitions.findAll { (it.class == InputObjectTypeDefinition.class) }.size() == 1 - } - - def "scanner allows multiple return types for custom scalars"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .resolvers(new ScalarsWithMultipleTypes()) - .scalars(new GraphQLScalarType("UUID", "Test scalars with duplicate types", new Coercing() { - @Override - Object serialize(Object input) { - return null - } - - @Override - Object parseValue(Object input) { - return null - } - - @Override - Object parseLiteral(Object input) { - return null - } - })) - .schemaString(""" - scalar UUID - - type Query { - first: UUID - second: UUID - } - """) - .scan() - - then: - objects.definitions.findAll { it instanceof ScalarTypeDefinition }.size() == 1 - } - - class ScalarsWithMultipleTypes implements GraphQLQueryResolver { - Integer first() { null } - - String second() { null } - } - - def "scanner handles multiple interfaces that are not used as field types"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .resolvers(new MultipleInterfaces()) - .schemaString(""" - type Query { - query1: NamedResourceImpl - query2: VersionedResourceImpl - } - - interface NamedResource { - name: String! - } - - interface VersionedResource { - version: Int! - } - - type NamedResourceImpl implements NamedResource { - name: String! - } - - type VersionedResourceImpl implements VersionedResource { - version: Int! - } - """) - .scan() - - then: - objects.definitions.findAll { it instanceof InterfaceTypeDefinition }.size() == 2 - } - - def "scanner handles interface implementation that is not used as field type"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - // uncommenting the line below makes the test succeed - .dictionary(InterfaceImplementation.NamedResourceImpl.class) - .resolvers(new InterfaceImplementation()) - .schemaString(""" - type Query { - query1: NamedResource - } - - interface NamedResource { - name: String! - } - - type NamedResourceImpl implements NamedResource { - name: String! - } - """) - .scan() - - then: - objects.definitions.findAll { it instanceof InterfaceTypeDefinition }.size() == 1 - } - - def "scanner handles custom scalars when matching input types"() { - when: - GraphQLScalarType customMap = new GraphQLScalarType('customMap', '', new Coercing, Map>() { - @Override - Map serialize(Object dataFetcherResult) { - return [:] - } - - @Override - Map parseValue(Object input) { - return [:] - } - - @Override - Map parseLiteral(Object input) { - return [:] - } - }) - - ScannedSchemaObjects objects = SchemaParser.newParser() - .resolvers(new GraphQLQueryResolver() { - boolean hasRawScalar(Map rawScalar) { true } - - boolean hasMapField(HasMapField mapField) { true } - }) - .scalars(customMap) - .schemaString(""" - type Query { - hasRawScalar(customMap: customMap): Boolean - hasMapField(mapField: HasMapField): Boolean - } - - input HasMapField { - map: customMap - } - - scalar customMap - """) - .scan() - - then: - objects.definitions.findAll { it instanceof ScalarTypeDefinition }.size() == 2 // Boolean and customMap - } - - class HasMapField { - Map map - } - - def "scanner allows class to be used for object type and input object type"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .resolvers(new GraphQLQueryResolver() { - Pojo test(Pojo pojo) { pojo } - }) - .schemaString(""" - type Query { - test(inPojo: InPojo): OutPojo - } - - input InPojo { - name: String - } - - type OutPojo { - name: String - } - """) - .scan() - - then: - objects.definitions - } - - class Pojo { - String name - } - - def "scanner should handle nested types in input types"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .schemaString(''' - schema { - query: Query - } - - type Query { - animal: Animal - } - - interface Animal { - type: ComplexType - } - - type Dog implements Animal { - type: ComplexType - } - - type ComplexType { - id: String - } - ''') - .resolvers(new NestedInterfaceTypeQuery()) - .dictionary(NestedInterfaceTypeQuery.Dog) - .scan() - - then: - objects.definitions.findAll { it instanceof ObjectTypeDefinition }.size() == 3 - - } - - class NestedInterfaceTypeQuery implements GraphQLQueryResolver { - Animal animal() { null } - - class Dog implements Animal { - @Override - ComplexType type() { null } - } - - class ComplexType { - String id - } - } - - def "scanner should handle unused types when option is true"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .schemaString(''' - # Let's say this is the Products service from Apollo Federation Introduction - - type Query { - allProducts: [Product] - } - - type Product { - name: String - } - - # these directives are defined in the Apollo Federation Specification: - # https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ - type User @key(fields: "id") @extends { - id: ID! @external - recentPurchasedProducts: [Product] - address: Address - } - - type Address { - street: String - } - ''') - .resolvers(new GraphQLQueryResolver() { - List allProducts() { null } - }) - .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) - .dictionary(User) - .scan() - - then: - objects.definitions.find { it.name == "User" } != null - objects.definitions.find { it.name == "Address" } != null - } - - class Product { - String name - } - - class User { - String id - List recentPurchasedProducts - Address address - } - - class Address { - String street - } - - def "scanner should handle unused types with interfaces when option is true"() { - when: - ScannedSchemaObjects objects = SchemaParser.newParser() - .schemaString(''' - type Query { - whatever: Whatever - } - - type Whatever { - value: String - } - - type Unused { - someInterface: SomeInterface - } - - interface SomeInterface { - value: String - } - - type Implementation implements SomeInterface { - value: String - } - ''') - .resolvers(new GraphQLQueryResolver() { - Whatever whatever() { null } - }) - .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) - .dictionary(Unused, Implementation) - .scan() - - then: - objects.definitions.find { it.name == "Unused" } != null - objects.definitions.find { it.name == "SomeInterface" } != null - objects.definitions.find { it.name == "Implementation" } != null - } - - class Whatever { - String value - } - - class Unused { - SomeInterface someInterface - } - - class Implementation implements SomeInterface { - @Override - String getValue() { - return null - } - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaParserBuilderSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaParserBuilderSpec.groovy deleted file mode 100644 index 4b60e63b..00000000 --- a/src/test/groovy/graphql/kickstart/tools/SchemaParserBuilderSpec.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package graphql.kickstart.tools - -import graphql.parser.InvalidSyntaxException -import spock.lang.Specification -import spock.lang.Unroll - -/** - * @author Andrew Potter - */ -class SchemaParserBuilderSpec extends Specification { - - @Unroll - def "parser errors should be returned in full"() { - when: - SchemaParser.newParser() - .schemaString(schema) - .build() - - then: - def e = thrown(InvalidSyntaxException) - e.toString().contains(error) - - where: - schema | error - "invalid" | "offending token 'invalid' at line 1 column 1" - "type Query {\ninvalid!\n}" | "offending token '!' at line 2 column 8" - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy deleted file mode 100644 index d141a903..00000000 --- a/src/test/groovy/graphql/kickstart/tools/SchemaParserSpec.groovy +++ /dev/null @@ -1,459 +0,0 @@ -package graphql.kickstart.tools - -import graphql.kickstart.tools.resolver.FieldResolverError -import graphql.language.SourceLocation -import graphql.schema.GraphQLSchema -import org.springframework.aop.framework.ProxyFactory -import spock.lang.Specification - -import java.util.concurrent.Future - -/** - * @author Andrew Potter - */ -class SchemaParserSpec extends Specification { - - SchemaParserBuilder builder - - def setup() { - builder = SchemaParser.newParser() - .schemaString(''' - type Query { - get(int: Int!): Int! - } - ''') - } - - def "builder throws FileNotFound exception when file is missing"() { - when: - builder.file("/404").build() - - then: - thrown(FileNotFoundException) - } - - def "builder doesn't throw FileNotFound exception when file is present"() { - when: - SchemaParser.newParser().file("Test.graphqls") - .resolvers(new GraphQLQueryResolver() { - String getId() { "1" } - }) - .build() - - then: - noExceptionThrown() - } - - def "parser throws SchemaError when Query resolver is missing"() { - when: - builder.build().makeExecutableSchema() - - then: - thrown(SchemaClassScannerError) - } - - def "parser throws ResolverError when Query resolver is given without correct method"() { - when: - SchemaParser.newParser() - .schemaString(''' - type Query { - get(int: Int!): Int! - } - ''') - .resolvers(new GraphQLQueryResolver() {}) - .build() - .makeExecutableSchema() - - then: - thrown(FieldResolverError) - } - - def "parser should parse correctly when Query resolver is given"() { - when: - SchemaParser.newParser() - .schemaString(''' - type Query { - get(int: Int!): Int! - } - ''') - .resolvers(new GraphQLQueryResolver() { - int get(int i) { return i } - }) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - def "parser should parse correctly when multiple query resolvers are given"() { - when: - SchemaParser.newParser() - .schemaString(''' - type Obj { - name: String - } - - type AnotherObj { - key: String - } - - type Query { - obj: Obj - anotherObj: AnotherObj - } - ''') - .resolvers(new GraphQLQueryResolver() { - Obj getObj() { return new Obj() } - }, new GraphQLQueryResolver() { - AnotherObj getAnotherObj() { return new AnotherObj() } - }) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - def "parser should parse correctly when multiple resolvers for the same data type are given"() { - when: - SchemaParser.newParser() - .schemaString(''' - type RootObj { - obj: Obj - anotherObj: AnotherObj - } - - type Obj { - name: String - } - - type AnotherObj { - key: String - } - - type Query { - rootObj: RootObj - } - ''') - .resolvers(new GraphQLQueryResolver() { - RootObj getRootObj() { return new RootObj() } - }, new GraphQLResolver() { - Obj getObj(RootObj rootObj) { return new Obj() } - }, new GraphQLResolver() { - AnotherObj getAnotherObj(RootObj rootObj) { return new AnotherObj() } - }) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - def "parser should allow setting custom generic wrappers"() { - when: - SchemaParser.newParser() - .schemaString(''' - type Query { - one: Object! - two: Object! - } - - type Object { - name: String! - } - ''') - .resolvers(new GraphQLQueryResolver() { - CustomGenericWrapper one() { null } - - Obj two() { null } - }) - .options(SchemaParserOptions.newOptions().genericWrappers(new SchemaParserOptions.GenericWrapper(CustomGenericWrapper, 1)).build()) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - def "parser should allow turning off default generic wrappers"() { - when: - SchemaParser.newParser() - .schemaString(''' - type Query { - one: Object! - two: Object! - } - - type Object { - toString: String! - } - ''') - .resolvers(new GraphQLQueryResolver() { - Future one() { null } - - Obj two() { null } - }) - .options(SchemaParserOptions.newOptions().useDefaultGenericWrappers(false).build()) - .build() - .makeExecutableSchema() - - then: - thrown(SchemaClassScannerError) - } - - def "parser should throw descriptive exception when object is used as input type incorrectly"() { - when: - SchemaParser.newParser() - .schemaString(''' - type Query { - name(filter: Filter): [String] - } - - type Filter { - filter: String - } - ''') - .resolvers(new GraphQLQueryResolver() { - List name(Filter filter) { null } - }) - .build() - .makeExecutableSchema() - - then: - def t = thrown(SchemaError) - t.message.contains("Was a type only permitted for object types incorrectly used as an input type, or vice-versa") - } - - def "parser handles spring AOP proxied resolvers by default"() { - when: - def resolver = new ProxyFactory(new ProxiedResolver()).getProxy() as GraphQLQueryResolver - - SchemaParser.newParser() - .schemaString(''' - type Query { - test: [String] - } - ''') - .resolvers(resolver) - .build() - then: - noExceptionThrown() - } - - def "parser handles enums with overridden toString method"() { - when: - SchemaParser.newParser() - .schemaString(''' - enum CustomEnum { - FOO - } - - type Query { - customEnum: CustomEnum - } - ''') - .resolvers(new GraphQLQueryResolver() { - CustomEnum customEnum() { null } - }) - .build() - .makeExecutableSchema() - then: - noExceptionThrown() - } - - def "parser should include source location for field definition"() { - when: - GraphQLSchema schema = SchemaParser.newParser() - .schemaString('''\ - |type Query { - | id: ID! - |} - '''.stripMargin()) - .resolvers(new QueryWithIdResolver()) - .build() - .makeExecutableSchema() - - then: - SourceLocation sourceLocation = schema.getObjectType("Query") - .getFieldDefinition("id") - .definition.sourceLocation - sourceLocation != null - sourceLocation.line == 2 - sourceLocation.column == 5 - sourceLocation.sourceName == null - } - - def "parser should include source location for field definition when loaded from single classpath file"() { - when: - GraphQLSchema schema = SchemaParser.newParser() - .file("Test.graphqls") - .resolvers(new QueryWithIdResolver()) - .build() - .makeExecutableSchema() - - then: - SourceLocation sourceLocation = schema.getObjectType("Query") - .getFieldDefinition("id") - .definition.sourceLocation - sourceLocation != null - sourceLocation.line == 2 - sourceLocation.column == 3 - sourceLocation.sourceName == "Test.graphqls" - } - - def "support enum types if only used as input type"() { - when: - SchemaParser.newParser().schemaString('''\ - type Query { test: Boolean } - - type Mutation { - save(input: SaveInput!): Boolean - } - - input SaveInput { - type: EnumType! - } - - enum EnumType { - TEST - } - '''.stripIndent()) - .resolvers(new GraphQLMutationResolver() { - boolean save(SaveInput input) { false } - - class SaveInput { - EnumType type; - } - - }, new GraphQLQueryResolver() { - boolean test() { false } - }) - .dictionary(EnumType.class) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - def "support enum types if only used in input Map"() { - when: - SchemaParser.newParser().schemaString('''\ - type Query { test: Boolean } - - type Mutation { - save(input: SaveInput!): Boolean - } - - input SaveInput { - age: Int - type: EnumType! - } - - enum EnumType { - TEST - } - '''.stripIndent()) - .resolvers(new GraphQLMutationResolver() { - boolean save(Map input) { false } - }, new GraphQLQueryResolver() { - boolean test() { false } - }) - .dictionary(EnumType.class) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - def "allow circular relations in input objects"() { - when: - SchemaParser.newParser().schemaString('''\ - input A { - id: ID! - b: B - } - input B { - id: ID! - a: A - } - input C { - id: ID! - c: C - } - type Query { test: Boolean } - type Mutation { - test(input: A!): Boolean - testC(input: C!): Boolean - } - '''.stripIndent()) - .resolvers(new GraphQLMutationResolver() { - static class A { - String id; - B b; - } - - static class B { - String id; - A a; - } - - static class C { - String id; - C c; - } - - boolean test(A a) { return true } - - boolean testC(C c) { return true } - }, new GraphQLQueryResolver() { - boolean test() { false } - }) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - enum EnumType { - TEST - } - - class QueryWithIdResolver implements GraphQLQueryResolver { - String getId() { null } - } - - class Filter { - String filter() { null } - } - - class CustomGenericWrapper {} - - class Obj { - def name() { null } - } - - class AnotherObj { - def key() { null } - } - - class RootObj { - } - - class ProxiedResolver implements GraphQLQueryResolver { - List test() { [] } - } - - enum CustomEnum { - FOO{ - @Override - String toString() { - return "Bar" - } - } - } - -} diff --git a/src/test/groovy/graphql/kickstart/tools/SuperclassResolverSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SuperclassResolverSpec.groovy deleted file mode 100644 index 5332562b..00000000 --- a/src/test/groovy/graphql/kickstart/tools/SuperclassResolverSpec.groovy +++ /dev/null @@ -1,58 +0,0 @@ -package graphql.kickstart.tools - - -import spock.lang.Specification - -class SuperclassResolverSpec extends Specification { - - def "methods from generic resolvers are resolved"() { - when: - SchemaParser.newParser().schemaString('''\ - type Query { - bar: Bar! - } - - type Bar implements Foo{ - value: String - getValueWithSeveralParameters(arg1: Boolean!, arg2: String): String! - } - - interface Foo { - getValueWithSeveralParameters(arg1: Boolean!, arg2: String): String! - } - ''') - .resolvers(new QueryResolver(), new BarResolver()) - .build() - .makeExecutableSchema() - - then: - noExceptionThrown() - } - - class QueryResolver implements GraphQLQueryResolver { - Bar getBar() { - return new Bar() - } - } - - class Bar { - } - - abstract class FooResolver implements GraphQLResolver { - String getValue(T foo) { - return "value" - } - - String getValueWithSeveralParameters(T foo, boolean arg1, String arg2) { - if (arg1) { - return "value" - } else { - return arg2 - } - } - } - - class BarResolver extends FooResolver { - - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy b/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy deleted file mode 100644 index 496dab49..00000000 --- a/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy +++ /dev/null @@ -1,13 +0,0 @@ -package graphql.kickstart.tools - -interface Animal { - SchemaClassScannerSpec.NestedInterfaceTypeQuery.ComplexType type() -} - -interface Vehicle { - VehicleInformation getInformation(); -} - -interface VehicleInformation {} - -interface SomeInterface { String getValue() } diff --git a/src/test/groovy/graphql/kickstart/tools/TypeClassMatcherSpec.groovy b/src/test/groovy/graphql/kickstart/tools/TypeClassMatcherSpec.groovy deleted file mode 100644 index 0a3e905f..00000000 --- a/src/test/groovy/graphql/kickstart/tools/TypeClassMatcherSpec.groovy +++ /dev/null @@ -1,167 +0,0 @@ -package graphql.kickstart.tools - -import graphql.kickstart.tools.resolver.FieldResolverScanner -import graphql.language.* -import spock.lang.Specification -import spock.lang.Unroll - -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Future - -/** - * @author Andrew Potter - */ -class TypeClassMatcherSpec extends Specification { - - private static final graphql.language.Type unwrappedCustomType = new TypeName("UnwrappedGenericCustomType") - private static final graphql.language.Type customType = new TypeName("CustomType") - private static final TypeDefinition customDefinition = new ObjectTypeDefinition("CustomType") - private static final TypeDefinition unwrappedCustomDefinition = new ObjectTypeDefinition("UnwrappedGenericCustomType") - - private static final TypeClassMatcher matcher = new TypeClassMatcher([ - CustomType : customDefinition, - UnwrappedGenericCustomType: unwrappedCustomDefinition - ]) - private static final SchemaParserOptions options = SchemaParserOptions.newOptions().genericWrappers( - new SchemaParserOptions.GenericWrapper( - GenericCustomType.class, - 0 - ), - SchemaParserOptions.GenericWrapper.listCollectionWithTransformer( - GenericCustomListType.class, - 0, - { x -> x } - ) - ).build() - private static final FieldResolverScanner scanner = new FieldResolverScanner(options) - - private final resolver = new RootResolverInfo([new QueryMethods()], options) - - private TypeClassMatcher.PotentialMatch createPotentialMatch(String methodName, graphql.language.Type graphQLType) { - scanner.findFieldResolver(new FieldDefinition(methodName, graphQLType), resolver) - .scanForMatches() - .find { it.location == TypeClassMatcher.Location.RETURN_TYPE } - } - - private graphql.language.Type list(graphql.language.Type other = customType) { - new ListType(other) - } - - private graphql.language.Type nonNull(graphql.language.Type other = customType) { - new NonNullType(other) - } - - @Unroll - def "matcher verifies that nested return type matches graphql definition for method #methodName"() { - when: - def match = matcher.match(createPotentialMatch(methodName, type)) - - then: - noExceptionThrown() - match.type == customDefinition - match.javaType == CustomType - - where: - methodName | type - "type" | customType - "futureType" | customType - "listType" | list() - "listListType" | list(list()) - "futureListType" | list() - "listFutureType" | list() - "listListFutureType" | list(list()) - "futureListListType" | list(list()) - "superType" | customType - "superListFutureType" | list() - "nullableType" | customType - "nullableListType" | list(nonNull(customType)) - "genericCustomType" | customType - "genericListType" | list() - } - - @Unroll - def "matcher verifies that nested return type doesn't match graphql definition for method #methodName"() { - when: - matcher.match(createPotentialMatch(methodName, type)) - - then: - thrown(SchemaClassScannerError) - - where: - methodName | type - "type" | list() - "futureType" | list() - } - - @Unroll - def "matcher verifies return value optionals are used incorrectly for method #methodName"() { - when: - matcher.match(createPotentialMatch(methodName, type)) - - then: - thrown(SchemaClassScannerError) - - where: - methodName | type - "nullableType" | nonNull(customType) - "nullableNullableType" | customType - "listNullableType" | list(customType) - } - - def "matcher allows unwrapped parameterized types as root types"() { - when: - def match = matcher.match(createPotentialMatch("genericCustomUnwrappedType", unwrappedCustomType)) - - then: - noExceptionThrown() - match.type == unwrappedCustomDefinition - match.javaType.getRawType() == UnwrappedGenericCustomType - } - - private class Super implements GraphQLQueryResolver { - Type superType() { null } - - ListFutureType superListFutureType() { null } - } - - private class QueryMethods extends Super>> { - CustomType type() { null } - - Future futureType() { null } - - List listType() { null } - - List> listListType() { null } - - CompletableFuture> futureListType() { null } - - List> listFutureType() { null } - - List>> listListFutureType() { null } - - CompletableFuture>> futureListListType() { null } - - Optional nullableType() { null } - - Optional> nullableListType() { null } - - Optional> nullableNullableType() { null } - - List> listNullableType() { null } - - GenericCustomType genericCustomType() { null } - - GenericCustomListType genericListType() { null } - - UnwrappedGenericCustomType genericCustomUnwrappedType() { null } - } - - private class CustomType {} - - private static class GenericCustomType {} - - private static class GenericCustomListType {} - - private static class UnwrappedGenericCustomType {} - -} diff --git a/src/test/groovy/graphql/kickstart/tools/Utils.groovy b/src/test/groovy/graphql/kickstart/tools/Utils.groovy deleted file mode 100644 index 075da225..00000000 --- a/src/test/groovy/graphql/kickstart/tools/Utils.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package graphql.kickstart.tools - -import com.fasterxml.jackson.databind.ObjectMapper -import graphql.ExecutionInput -import graphql.GraphQL -import groovy.transform.CompileStatic - -/** - * @author Andrew Potter - */ -@CompileStatic -class Utils { - private static ObjectMapper mapper = new ObjectMapper() - - static Map assertNoGraphQlErrors(GraphQL gql, Map args = [:], Object context = new Object(), Closure closure) { - def result = gql.execute(ExecutionInput.newExecutionInput() - .query(closure()) - .context(context) - .root(context) - .variables(args)) - - if (!result.errors.isEmpty()) { - throw new AssertionError("GraphQL result contained errors!\n${result.errors.collect { mapper.writeValueAsString(it) }.join("\n")}") - } - - return result.data as Map - } -} diff --git a/src/test/groovy/graphql/kickstart/tools/VersionedResource.groovy b/src/test/groovy/graphql/kickstart/tools/VersionedResource.groovy deleted file mode 100644 index de3c9c41..00000000 --- a/src/test/groovy/graphql/kickstart/tools/VersionedResource.groovy +++ /dev/null @@ -1,5 +0,0 @@ -package graphql.kickstart.tools - -interface VersionedResource { - int version() -} diff --git a/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt b/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt new file mode 100644 index 00000000..38f1d150 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt @@ -0,0 +1,113 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import graphql.schema.GraphQLSchema +import org.junit.Test +import java.util.* + +class BuiltInIdTest { + + private val schema: GraphQLSchema = SchemaParser.newParser() + .schemaString( + """ + type Query { + itemByLongId(id: ID!): Item1! + itemsByLongIds(ids: [ID!]!): [Item1!]! + itemByUuidId(id: ID!): Item2! + itemsByUuidIds(ids: [ID!]!): [Item2!]! + } + + type Item1 { + id: ID! + } + + type Item2 { + id: ID! + } + """) + .resolvers(QueryWithLongItemResolver()) + .build() + .makeExecutableSchema() + private val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + @Test + fun `supports Long as ID as input`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemByLongId(id: 1) { + id + } + } + """ + } + + assertEquals(data["itemByLongId"], mapOf("id" to "1")) + } + + @Test + fun `supports list of Long as ID as input`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemsByLongIds(ids: [1,2,3]) { + id + } + } + """ + } + + assertEquals(data["itemsByLongIds"], listOf( + mapOf("id" to "1"), + mapOf("id" to "2"), + mapOf("id" to "3") + )) + } + + @Test + fun `supports UUID as ID as input`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemByUuidId(id: "00000000-0000-0000-0000-000000000000") { + id + } + } + """ + } + + assertEquals(data["itemByUuidId"], mapOf("id" to "00000000-0000-0000-0000-000000000000")) + } + + @Test + fun `supports list of UUID as ID as input`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemsByUuidIds(ids: ["00000000-0000-0000-0000-000000000000","11111111-1111-1111-1111-111111111111","22222222-2222-2222-2222-222222222222"]) { + id + } + } + """ + } + + assertEquals(data["itemsByUuidIds"], listOf( + mapOf("id" to "00000000-0000-0000-0000-000000000000"), + mapOf("id" to "11111111-1111-1111-1111-111111111111"), + mapOf("id" to "22222222-2222-2222-2222-222222222222") + )) + } + + class QueryWithLongItemResolver : GraphQLQueryResolver { + fun itemByLongId(id: Long): Item1 = Item1(id) + fun itemsByLongIds(ids: List): List = ids.map { Item1(it) } + fun itemByUuidId(id: UUID): Item2 = Item2(id) + fun itemsByUuidIds(ids: List): List = ids.map { Item2(it) } + } + + class Item1(var id: Long? = null) + class Item2(var id: UUID? = null) +} diff --git a/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt index 300f00c5..cb47e09d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt @@ -9,89 +9,92 @@ import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLFieldDefinition import graphql.schema.idl.SchemaDirectiveWiring import graphql.schema.idl.SchemaDirectiveWiringEnvironment -import org.junit.Assert import org.junit.Ignore import org.junit.Test -import java.util.function.BiFunction class DirectiveTest { @Test fun `should apply correctly the @uppercase directive`() { - val schema = SchemaParser.newParser().schemaString(""" - directive @uppercase on FIELD_DEFINITION - - type Query { - users: UserConnection - } - - type UserConnection { - edges: [UserEdge!]! - } - - type UserEdge { - node: User! - } - - type User { - id: ID! - name: String @uppercase - } - """) - .resolvers(UsersQueryResolver()) - .directive("uppercase", UppercaseDirective()) - .build() - .makeExecutableSchema() + val schema = SchemaParser.newParser() + .schemaString( + """ + directive @uppercase on FIELD_DEFINITION + + type Query { + users: UserConnection + } + + type UserConnection { + edges: [UserEdge!]! + } + + type UserEdge { + node: User! + } + + type User { + id: ID! + name: String @uppercase + } + """) + .resolvers(UsersQueryResolver()) + .directive("uppercase", UppercaseDirective()) + .build() + .makeExecutableSchema() val gql = GraphQL.newGraphQL(schema) - .queryExecutionStrategy(AsyncExecutionStrategy()) - .build() - - val result = gql.execute(""" - query { - users { - edges { - node { - id - name + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + val result = gql.execute( + """ + query { + users { + edges { + node { + id + name + } + } } - } } - } - """) + """) val expected = mapOf( - "users" to mapOf( - "edges" to listOf( - mapOf("node" to - mapOf("id" to "1", "name" to "LUKE") - ) - ) + "users" to mapOf( + "edges" to listOf( + mapOf("node" to + mapOf("id" to "1", "name" to "LUKE") + ) ) + ) ) - Assert.assertEquals(expected, result.getData>>()) + assertEquals(result.getData(), expected) } @Test @Ignore("Ignore until enums work in directives") fun `should compile schema with directive that has enum parameter`() { - val schema = SchemaParser.newParser().schemaString(""" - directive @allowed(state: [AllowedState!]) on FIELD_DEFINITION - - enum AllowedState { - ALLOWED - DISALLOWED - } - - type Book { - id: Int! - name: String! @allowed(state: [ALLOWED]) - } - - type Query { - books: [Book!] - } - """) + val schema = SchemaParser.newParser() + .schemaString( + """ + directive @allowed(state: [AllowedState!]) on FIELD_DEFINITION + + enum AllowedState { + ALLOWED + DISALLOWED + } + + type Book { + id: Int! + name: String! @allowed(state: [ALLOWED]) + } + + type Query { + books: [Book!] + } + """) .resolvers(QueryResolver()) .directive("allowed", AllowedDirective()) .build() @@ -135,7 +138,7 @@ class DirectiveTest { val parentType = environment.fieldsContainer val originalDataFetcher = environment.codeRegistry.getDataFetcher(parentType, field) - val wrappedDataFetcher = DataFetcherFactories.wrapDataFetcher(originalDataFetcher, BiFunction { env, value -> + val wrappedDataFetcher = DataFetcherFactories.wrapDataFetcher(originalDataFetcher, { _, value -> (value as? String)?.toUpperCase() }) @@ -151,8 +154,8 @@ class DirectiveTest { } private data class User( - val id: Long, - val name: String + val id: Long, + val name: String ) } } diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt new file mode 100644 index 00000000..b67d6973 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt @@ -0,0 +1,642 @@ +package graphql.kickstart.tools + +import graphql.* +import graphql.execution.AsyncExecutionStrategy +import graphql.schema.GraphQLEnumType +import graphql.schema.GraphQLSchema +import org.junit.Test +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import org.reactivestreams.tck.TestEnvironment +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class EndToEndTest { + + private val schema: GraphQLSchema = createSchema() + private val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + @Test + fun `schema comments are used as descriptions`() { + val type = schema.allTypesAsList.find { it.name == "Type" } as GraphQLEnumType + assert(type.values[0].description == "Item type 1") + assert(type.values[1].description == "Item type 2") + } + + @Test + fun `generated schema should respond to simple queries`() { + assertNoGraphQlErrors(gql) { + """ + { + items(itemsInput: {name: "item1"}) { + id + type + } + } + """ + } + } + + @Test + fun `generated schema should respond to simple mutations`() { + val data = assertNoGraphQlErrors(gql, mapOf("name" to "new1", "type" to Type.TYPE_2.toString())) { + """ + mutation addNewItem(${'$'}name: String!, ${'$'}type: Type!) { + addItem(newItem: {name: ${'$'}name, type: ${'$'}type}) { + id + name + type + } + } + """ + } + + assertNotNull(data["addItem"]) + } + + @Test + fun `generated schema should execute the subscription query`() { + val newItem = Item(1, "item", Type.TYPE_1, UUID.randomUUID(), listOf()) + var returnedItem: Map>? = null + + val closure = { + """ + subscription { + onItemCreated { + id + } + } + """ + } + + val result = gql.execute(ExecutionInput.newExecutionInput() + .query(closure.invoke()) + .context(OnItemCreatedContext(newItem)) + .variables(mapOf())) + + val data = result.getData() as Publisher + val latch = CountDownLatch(1) + data.subscribe(object : Subscriber { + override fun onNext(item: ExecutionResult?) { + returnedItem = item?.getData() + latch.countDown() + } + + override fun onError(throwable: Throwable?) {} + override fun onComplete() {} + override fun onSubscribe(p0: Subscription?) {} + }) + latch.await(3, TimeUnit.SECONDS) + + assert(result.errors.isEmpty()) + assertEquals(returnedItem?.get("onItemCreated"), mapOf("id" to 1)) + } + + @Test + fun `generated schema should handle interface types`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemsByInterface { + name + type + } + } + """ + } + + assertNotNull(data["itemsByInterface"]) + } + + @Test + fun `generated schema should handle union types`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + allItems { + ... on Item { + id + name + } + ... on OtherItem { + name + type + } + } + } + """ + } + + assertNotNull(data["allItems"]) + } + + @Test + fun `generated schema should handle nested union types`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + nestedUnionItems { + ... on Item { + itemId: id + } + ... on OtherItem { + otherItemId: id + } + ... on ThirdItem { + thirdItemId: id + } + } + } + """ + } + + assertEquals(data["nestedUnionItems"], listOf( + mapOf("itemId" to 0), + mapOf("itemId" to 1), + mapOf("otherItemId" to 0), + mapOf("otherItemId" to 1), + mapOf("thirdItemId" to 100) + )) + } + + @Test + fun `generated schema should handle scalar types`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemByUUID(uuid: "38f685f1-b460-4a54-a17f-7fd69e8cf3f8") { + uuid + } + } + """ + } + + assertNotNull(data["itemByUUID"]) + } + + @Test + fun `generated schema should handle non nullable scalar types`() { + val fileParts = listOf(MockPart("test.doc", "Hello"), MockPart("test.doc", "World")) + val args = mapOf("fileParts" to fileParts) + val data = assertNoGraphQlErrors(gql, args) { + """ + mutation (${'$'}fileParts: [Upload!]!) { echoFiles(fileParts: ${'$'}fileParts) } + """ + } + + assertEquals(data["echoFiles"], listOf("Hello", "World")) + } + + @Test + fun `generated schema should handle any Map (using HashMap) types as property maps`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + propertyHashMapItems { + name + age + } + } + """ + } + + assertEquals(data["propertyHashMapItems"], listOf(mapOf("name" to "bob", "age" to 55))) + } + + @Test + fun `generated schema should handle any Map (using SortedMap) types as property maps`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + propertySortedMapItems { + name + age + } + } + """ + } + + assertEquals(data["propertySortedMapItems"], listOf( + mapOf("name" to "Arthur", "age" to 76), + mapOf("name" to "Jane", "age" to 28) + )) + } + + // In this test a dictionary entry for the schema type ComplexMapItem is defined + // so that it is possible for a POJO mapping to be known since the ComplexMapItem is contained + // in a property map (i.e. Map) and so the normal resolver and schema traversal code + // will not be able to find the POJO since it does not exist as a strongly typed object in + // resolver/POJO graph. + @Test + fun `generated schema should handle Map types as property maps when containing complex data`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + propertyMapWithComplexItems { + nameId { + id + } + age + } + } + """ + } + + assertEquals(data["propertyMapWithComplexItems"], listOf(mapOf("nameId" to mapOf("id" to 150), "age" to 72))) + } + + // This behavior is consistent with PropertyDataFetcher + @Test + fun `property map returns null when a property is not defined`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + propertyMapMissingNamePropItems { + name + age + } + } + """ + } + + assertEquals(data["propertyMapMissingNamePropItems"], listOf(mapOf("name" to null, "age" to 55))) + } + + // In this test a dictonary entry for the schema type NestedComplexMapItem is defined + // however we expect to not be required to define one for the transitive UndiscoveredItem object since + // the schema resolver discovery code should still be able to automatically determine the POJO that + // maps to this schema type. + @Test + fun `generated schema should continue to associate resolvers for transitive types of a Map complex data type`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + propertyMapWithNestedComplexItems { + nested { + item { + id + } + } + age + } + } + """ + } + + assertEquals(data["propertyMapWithNestedComplexItems"], listOf(mapOf("nested" to mapOf("item" to mapOf("id" to 63)), "age" to 72))) + } + + @Test + fun `generated schema should handle optional arguments`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + missing: itemsWithOptionalInput { + id + } + + present: itemsWithOptionalInput(itemsInput: {name: "item1"}) { + id + } + } + """ + } + + assertEquals(data["missing"], listOf(mapOf("id" to 0), mapOf("id" to 1))) + assertEquals(data["present"], listOf(mapOf("id" to 0))) + } + + @Test + fun `generated schema should handle optional arguments using Optional`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + missing: itemsWithOptionalInputExplicit { + id + } + + present: itemsWithOptionalInputExplicit(itemsInput: {name: "item1"}) { + id + } + } + """ + } + + assertEquals(data["missing"], listOf(mapOf("id" to 0), mapOf("id" to 1))) + assertEquals(data["present"], listOf(mapOf("id" to 0))) + } + + @Test + fun `generated schema should handle optional return types using Optional`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + missing: optionalItem(itemsInput: {name: "item?"}) { + id + } + + present: optionalItem(itemsInput: {name: "item1"}) { + id + } + } + """ + } + + assertNull(data["missing"]) + assertNotNull(data["present"]) + } + + @Test + fun `generated schema should pass default arguments`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + defaultArgument + } + """ + } + + assert(data["defaultArgument"] == true) + } + + @Test + fun `introspection shouldn't fail for arguments of type list with a default value (defaultEnumListArgument)`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + __type(name: "Query") { + name + fields { + name + args { + name + defaultValue + } + } + } + } + """ + } + + assertNotNull(data["__type"]) + } + + @Test + fun `generated schema should return null without errors for null value with nested fields`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + complexNullableType { + first + second + third + } + } + """ + } + + assert(data.containsKey("complexNullableType")) + assertNull(data["complexNullableType"]) + } + + @Test + fun `generated schema handles nested lists in input type fields`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + complexInputType(complexInput: [[{first: "foo", second: [[{first: "bar"}]]}]]) + } + """ + } + + assertNotNull(data["complexInputType"]) + } + + @Test + fun `generated schema should use type extensions`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + extendedType { + first + second + } + } + """ + } + + assertEquals(data["extendedType"], mapOf( + "first" to "test", + "second" to "test" + )) + } + + @Test + fun `generated schema uses properties if no methods are found`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + propertyField + } + """ + } + + assertNotNull(data["propertyField"]) + } + + @Test + fun `generated schema allows enums in input types`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + enumInputType(type: TYPE_2) + } + """ + } + + assertEquals(data["enumInputType"], "TYPE_2") + } + + @Test + fun `generated schema works with custom scalars as input values`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + customScalarMapInputType(customScalarMap: { test: "me" }) + } + """ + } + + assertEquals(data["customScalarMapInputType"], mapOf("test" to "me")) + } + + @Test + fun `generated schema should handle extended input types`() { + val data = assertNoGraphQlErrors(gql) { + """ + mutation { + saveUser(input: {name: "John", password: "secret"}) + } + """ + } + + assertEquals(data["saveUser"], "John/secret") + } + + @Test + fun `generated schema supports generic properties`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemWithGenericProperties { + keys + } + } + """ + } + + assertEquals(data["itemWithGenericProperties"], mapOf("keys" to listOf("A", "B"))) + } + + @Test + fun `generated schema supports overriding built-in scalars`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + itemByBuiltInId(id: "38f685f1-b460-4a54-a17f-7fd69e8cf3f8") { + name + } + } + """ + } + + assertNotNull(data["itemByBuiltInId"]) + } + + @Test + fun `generated schema supports DataFetcherResult`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + dataFetcherResult { + name + } + } + """ + } + + assertEquals(data["dataFetcherResult"], mapOf("name" to "item1")) + } + + @Test + fun `generated schema supports Kotlin suspend functions`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + coroutineItems { + id + name + } + } + """ + } + + assertEquals(data["coroutineItems"], listOf( + mapOf("id" to 0, "name" to "item1"), + mapOf("id" to 1, "name" to "item2") + )) + } + + @Test + fun `generated schema supports Kotlin coroutine channels for the subscription query`() { + val newItem = Item(1, "item", Type.TYPE_1, UUID.randomUUID(), listOf()) + val closure = { + """ + subscription { + onItemCreatedCoroutineChannel { + id + } + } + """ + } + + val result = gql.execute(ExecutionInput.newExecutionInput() + .query(closure.invoke()) + .context(OnItemCreatedContext(newItem)) + .variables(mapOf())) + + val data = result.getData() as Publisher + val subscriber = TestEnvironment().newManualSubscriber(data) + + val subscriberResult = subscriber.requestNextElement() as ExecutionResultImpl + val subscriberData = subscriberResult.getData() as Map>? + assert(result.errors.isEmpty()) + assertEquals(subscriberData?.get("onItemCreatedCoroutineChannel"), mapOf("id" to 1)) + subscriber.expectCompletion() + } + + @Test + fun `generated schema supports Kotlin coroutine channels with suspend function for the subscription query`() { + val newItem = Item(1, "item", Type.TYPE_1, UUID.randomUUID(), listOf()) + val closure = { + """ + subscription { + onItemCreatedCoroutineChannelAndSuspendFunction { + id + } + } + """ + } + + val result = gql.execute(ExecutionInput.newExecutionInput() + .query(closure.invoke()) + .context(OnItemCreatedContext(newItem)) + .variables(mapOf())) + + val data = result.getData() as Publisher + val subscriber = TestEnvironment().newManualSubscriber(data) + + val subscriberResult = subscriber.requestNextElement() as ExecutionResultImpl + val subscriberData = subscriberResult.getData() as Map>? + assert(result.errors.isEmpty()) + assertEquals(subscriberData?.get("onItemCreatedCoroutineChannelAndSuspendFunction"), mapOf("id" to 1)) + subscriber.expectCompletion() + } + + @Test + fun `generated schema supports arrays`() { + val data = assertNoGraphQlErrors(gql) { + """ + { + arrayItems { + name + } + } + """ + } + + assertEquals(data["arrayItems"], listOf( + mapOf("name" to "item1"), + mapOf("name" to "item2") + )) + } + + @Test + fun `generated schema should re-throw original runtime exception when executing a resolver method`() { + val result = gql.execute(ExecutionInput.newExecutionInput().query( + """ + { + throwsIllegalArgumentException + } + """ + )) + + assertEquals(result.errors.size, 1) + val exceptionWhileDataFetching = result.errors[0] as ExceptionWhileDataFetching + assert(exceptionWhileDataFetching.exception is IllegalArgumentException) + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt b/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt new file mode 100644 index 00000000..32b1781c --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt @@ -0,0 +1,56 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import org.junit.Test + +class EnumDefaultValueTest { + + @Test + fun `enum value is not passed down to graphql-java`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + test(input: MySortSpecifier): SortBy + } + input MySortSpecifier { + sortBy: SortBy = createdOn + value: Int = 10 + } + enum SortBy { + createdOn + updatedOn + } + """) + .resolvers(object : GraphQLQueryResolver { + fun test(input: MySortSpecifier): SortBy? = input.sortBy + }) + .build() + .makeExecutableSchema() + + val ggl = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + val data = assertNoGraphQlErrors(ggl, mapOf("input" to mapOf("value" to 1))) { + """ + query test(${'$'}input: MySortSpecifier) { + test(input: ${'$'}input) + } + """ + } + + assertEquals(data["test"], "createdOn") + } + + class MySortSpecifier { + var sortBy: SortBy? = null + var value: Int? = null + } + + enum class SortBy { + createdOn, + updatedOn + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt b/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt new file mode 100644 index 00000000..d0b0c5cd --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt @@ -0,0 +1,67 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import graphql.schema.GraphQLSchema +import org.junit.Test + +class EnumListParameterTest { + private val schema: GraphQLSchema = SchemaParser.newParser() + .schemaString( + """ + type Query { + countries(regions: [Region!]!): [Country!]! + } + + enum Region { + EUROPE + ASIA + } + + type Country { + code: String! + name: String! + regions: [Region!] + } + """) + .resolvers(QueryResolver()) + .build() + .makeExecutableSchema() + private val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + @Test + fun `query with parameter type list of enums should resolve correctly`() { + val data = assertNoGraphQlErrors(gql, mapOf("regions" to setOf("EUROPE", "ASIA"))) { + """ + query getCountries(${'$'}regions: [Region!]!) { + countries(regions: ${'$'}regions){ + code + name + regions + } + } + """ + } + + assertEquals((data["countries"]), emptyList()) + } + + class QueryResolver : GraphQLQueryResolver { + fun getCountries(regions: Set): Set { + return setOf() + } + } + + class Country { + var code: String? = null + var name: String? = null + var regions: List? = null + } + + enum class Region { + EUROPE, + ASIA + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt new file mode 100644 index 00000000..4646ddf4 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt @@ -0,0 +1,125 @@ +package graphql.kickstart.tools + +import graphql.kickstart.tools.SchemaParserOptions.Companion.defaultOptions +import graphql.kickstart.tools.resolver.FieldResolverError +import graphql.kickstart.tools.resolver.FieldResolverScanner +import graphql.kickstart.tools.resolver.MethodFieldResolver +import graphql.kickstart.tools.resolver.PropertyFieldResolver +import graphql.language.FieldDefinition +import graphql.language.TypeName +import graphql.relay.Connection +import graphql.relay.DefaultConnection +import graphql.relay.DefaultPageInfo +import org.junit.Test + +class FieldResolverScannerTest { + + private val options = defaultOptions() + private val scanner = FieldResolverScanner(options) + + @Test + fun `scanner finds fields on multiple root types`() { + val resolver = RootResolverInfo(listOf(RootQuery1(), RootQuery2()), options) + + val result1 = scanner.findFieldResolver(FieldDefinition("field1", TypeName("String")), resolver) + val result2 = scanner.findFieldResolver(FieldDefinition("field2", TypeName("String")), resolver) + + assertNotEquals(result1.search.source, result2.search.source) + } + + @Test(expected = FieldResolverError::class) + fun `scanner throws exception when more than one resolver method is found`() { + val resolver = RootResolverInfo(listOf(RootQuery1(), DuplicateQuery()), options) + + scanner.findFieldResolver(FieldDefinition("field1", TypeName("String")), resolver) + } + + @Test(expected = FieldResolverError::class) + fun `scanner throws exception when no resolver methods are found`() { + val resolver = RootResolverInfo(listOf(), options) + + scanner.findFieldResolver(FieldDefinition("field1", TypeName("String")), resolver) + } + + @Test + fun `scanner finds properties when no method is found`() { + val resolver = RootResolverInfo(listOf(PropertyQuery()), options) + + val name = scanner.findFieldResolver(FieldDefinition("name", TypeName("String")), resolver) + val version = scanner.findFieldResolver(FieldDefinition("version", TypeName("Integer")), resolver) + + assert(name is PropertyFieldResolver) + assert(version is PropertyFieldResolver) + } + + @Test + fun `scanner finds generic return type`() { + val resolver = RootResolverInfo(listOf(GenericQuery()), options) + + val users = scanner.findFieldResolver(FieldDefinition("users", TypeName("UserConnection")), resolver) + + assert(users is MethodFieldResolver) + } + + @Test + fun `scanner prefers concrete resolver`() { + val resolver = DataClassResolverInfo(Kayak::class.java) + + val meta = scanner.findFieldResolver(FieldDefinition("information", TypeName("VehicleInformation")), resolver) + + assert(meta is MethodFieldResolver) + assertEquals((meta as MethodFieldResolver).method.returnType, BoatInformation::class.java) + } + + @Test + fun `scanner finds field resolver method using camelCase for snake_cased field_name`() { + val resolver = RootResolverInfo(listOf(CamelCaseQuery1()), options) + + val meta = scanner.findFieldResolver(FieldDefinition("hull_type", TypeName("HullType")), resolver) + + assert(meta is MethodFieldResolver) + assertEquals((meta as MethodFieldResolver).method.returnType, HullType::class.java) + } + + class RootQuery1 : GraphQLQueryResolver { + fun field1() {} + } + + class RootQuery2 : GraphQLQueryResolver { + fun field2() {} + } + + class DuplicateQuery : GraphQLQueryResolver { + fun field1() {} + } + + class CamelCaseQuery1 : GraphQLQueryResolver { + fun getHullType(): HullType = HullType() + } + + class HullType + + open class ParentPropertyQuery { + private var version: Int = 1 + } + + class PropertyQuery : ParentPropertyQuery(), GraphQLQueryResolver { + private var name: String = "name" + } + + class User + + class GenericQuery : GraphQLQueryResolver { + fun getUsers(): Connection { + return DefaultConnection(listOf(), DefaultPageInfo(null, null, false, false)) + } + } + + abstract class Boat : Vehicle { + override fun getInformation(): BoatInformation = this.getInformation() + } + + class BoatInformation : VehicleInformation + + class Kayak : Boat() +} diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt new file mode 100644 index 00000000..cc8444df --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt @@ -0,0 +1,67 @@ +package graphql.kickstart.tools + +import org.junit.Test + +class GenericResolverTest { + + @Test + fun `methods from generic resolvers are resolved`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + bar: Bar + } + + type Bar { + value: String + } + """) + .resolvers(QueryResolver1(), BarResolver()) + .build() + .makeExecutableSchema() + } + + class QueryResolver1 : GraphQLQueryResolver { + fun getBar(): Bar { + return Bar() + } + } + + class Bar + + abstract class FooResolver : GraphQLResolver { + fun getValue(foo: T): String = "value" + } + + class BarResolver : FooResolver(), GraphQLResolver + + @Test + fun `methods from generic inherited resolvers are resolved`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + car: Car + } + type Car { + value: String + } + """) + .resolvers(QueryResolver2(), CarResolver()) + .build() + .makeExecutableSchema() + } + + class QueryResolver2 : GraphQLQueryResolver { + fun getCar(): Car = Car() + } + + abstract class FooGraphQLResolver : GraphQLResolver { + fun getValue(foo: T): String = "value" + } + + class Car + + class CarResolver : FooGraphQLResolver() +} diff --git a/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverDataFetcherTest.kt b/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverDataFetcherTest.kt index 20966215..564c7e3e 100644 --- a/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverDataFetcherTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverDataFetcherTest.kt @@ -3,9 +3,11 @@ package graphql.kickstart.tools import graphql.ExecutionResult import graphql.execution.* import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.kickstart.tools.resolver.FieldResolverError import graphql.kickstart.tools.resolver.FieldResolverScanner import graphql.language.FieldDefinition import graphql.language.InputValueDefinition +import graphql.language.NonNullType import graphql.language.TypeName import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment @@ -15,7 +17,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel -import org.junit.Assert import org.junit.Test import org.reactivestreams.Publisher import org.reactivestreams.tck.TestEnvironment @@ -35,12 +36,12 @@ class MethodFieldResolverDataFetcherTest { // expect @Suppress("UNCHECKED_CAST") val future = resolver.get(createEnvironment(DataClass())) as CompletableFuture - Assert.assertTrue(future.get()) + assert(future.get()) } class SuspendClass : GraphQLResolver { - val dispatcher = Dispatchers.IO - val job = Job() + private val dispatcher = Dispatchers.IO + private val job = Job() @ExperimentalCoroutinesApi val options = SchemaParserOptions.Builder() @@ -67,11 +68,11 @@ class MethodFieldResolverDataFetcherTest { val publisher = resolver.get(createEnvironment(DataClass())) as Publisher val subscriber = TestEnvironment().newManualSubscriber(publisher) - Assert.assertEquals("A", subscriber.requestNextElement()) + assertEquals(subscriber.requestNextElement(), "A") subscriber.cancel() Thread.sleep(100) - Assert.assertTrue(doubleChannel.channel.isClosedForReceive) + assert(doubleChannel.channel.isClosedForReceive) } class DoubleChannel : GraphQLResolver { @@ -98,12 +99,148 @@ class MethodFieldResolverDataFetcherTest { val publisher = resolver.get(createEnvironment(DataClass())) as Publisher val subscriber = TestEnvironment().newManualSubscriber(publisher) - Assert.assertEquals("A", subscriber.requestNextElement()) + assertEquals(subscriber.requestNextElement(), "A") subscriber.expectErrorWithMessage(IllegalStateException::class.java, "Channel error") } + @Test(expected = FieldResolverError::class) + fun `data fetcher throws exception if resolver has too many arguments`() { + createFetcher("active", object : GraphQLQueryResolver { + fun active(arg1: Any, arg2: Any): Boolean = true + }) + } + + @Test(expected = FieldResolverError::class) + fun `data fetcher throws exception if resolver has too few arguments`() { + createFetcher("active", listOf(InputValueDefinition("doesNotExist", TypeName("Boolean"))), object : GraphQLQueryResolver { + fun active(): Boolean = true + }) + } + + @Test + fun `data fetcher prioritizes methods on the resolver`() { + val name = "Resolver Name" + val resolver = createFetcher("name", object : GraphQLResolver { + fun getName(dataClass: DataClass): String = name + }) + + assertEquals(resolver.get(createEnvironment(DataClass())), name) + } + + @Test + fun `data fetcher uses data class methods if no resolver method is given`() { + val resolver = createFetcher("name", object : GraphQLResolver {}) + + assertEquals(resolver.get(createEnvironment(DataClass())), DataClass().name) + } + + @Test + fun `data fetcher prioritizes methods without a prefix`() { + val name = "correct name" + val resolver = createFetcher("name", object : GraphQLResolver { + fun getName(dataClass: DataClass): String = "in$name" + fun name(dataClass: DataClass): String = name + }) + + assertEquals(resolver.get(createEnvironment(DataClass())), name) + } + + @Test + fun `data fetcher uses 'is' prefix for booleans (primitive type)`() { + val resolver = createFetcher("active", object : GraphQLResolver { + fun isActive(dataClass: DataClass): Boolean = true + fun getActive(dataClass: DataClass): Boolean = true + }) + + assertEquals(resolver.get(createEnvironment(DataClass())), true) + } + + @Test + fun `data fetcher uses 'is' prefix for Booleans (Object type)`() { + val resolver = createFetcher("active", object : GraphQLResolver { + fun isActive(dataClass: DataClass): Boolean? = null + fun getActive(dataClass: DataClass): Boolean? = null + }) + + assertEquals(resolver.get(createEnvironment(DataClass())), null) + } + + @Test + fun `data fetcher passes environment if method has extra argument`() { + val resolver = createFetcher("active", object : GraphQLResolver { + fun isActive(dataClass: DataClass, env: DataFetchingEnvironment): Boolean = env is DataFetchingEnvironment + }) + + assertEquals(resolver.get(createEnvironment(DataClass())), true) + } + + @Test + fun `data fetcher passes environment if method has extra argument even if context is specified`() { + val options = SchemaParserOptions.newOptions().contextClass(ContextClass::class).build() + val resolver = createFetcher("active", options = options, resolver = object : GraphQLResolver { + fun isActive(dataClass: DataClass, env: DataFetchingEnvironment): Boolean = env is DataFetchingEnvironment + }) + + assertEquals(resolver.get(createEnvironment(DataClass(), context = ContextClass())), true) + } + + @Test + fun `data fetcher passes context if method has extra argument and context is specified`() { + val context = ContextClass() + val options = SchemaParserOptions.newOptions().contextClass(ContextClass::class).build() + val resolver = createFetcher("active", options = options, resolver = object : GraphQLResolver { + fun isActive(dataClass: DataClass, ctx: ContextClass): Boolean { + return ctx == context + } + }) + + assertEquals(resolver.get(createEnvironment(DataClass(), context = context)), true) + } + + @Test + fun `data fetcher marshalls input object if required`() { + val name = "correct name" + val resolver = createFetcher("active", listOf(InputValueDefinition("input", TypeName("InputClass"))), object : GraphQLQueryResolver { + fun active(input: InputClass): Boolean = + input.name == name + }) + + assertEquals(resolver.get(createEnvironment(arguments = mapOf("input" to mapOf("name" to name)))), true) + } + + @Test + fun `data fetcher doesn't marshall input object if not required`() { + val name = "correct name" + val resolver = createFetcher("active", listOf(InputValueDefinition("input", TypeName("Map"))), object : GraphQLQueryResolver { + fun active(input: Map<*, *>): Boolean = + input["name"] == name + }) + + assertEquals(resolver.get(createEnvironment(arguments = mapOf("input" to mapOf("name" to name)))), true) + } + + @Test + fun `data fetcher returns null if nullable argument is passed null`() { + val resolver = createFetcher("echo", listOf(InputValueDefinition("message", TypeName("String"))), object : GraphQLQueryResolver { + fun echo(message: String?): String? = + message + }) + + assertEquals(resolver.get(createEnvironment()), null) + } + + @Test(expected = ResolverError::class) + fun `data fetcher throws exception if non-null argument is passed null`() { + val resolver = createFetcher("echo", listOf(InputValueDefinition("message", NonNullType(TypeName("String")))), object : GraphQLQueryResolver { + fun echo(message: String): String = + message + }) + + resolver.get(createEnvironment()) + } + class OnDataNameChanged : GraphQLResolver { - val channel = Channel(10) + private val channel = Channel(10) init { channel.offer("A") @@ -116,6 +253,14 @@ class MethodFieldResolverDataFetcherTest { } } + private fun createFetcher( + methodName: String, + arguments: List = emptyList(), + resolver: GraphQLResolver<*> + ): DataFetcher<*> { + return createFetcher(methodName, resolver, arguments) + } + private fun createFetcher( methodName: String, resolver: GraphQLResolver<*>, @@ -123,10 +268,10 @@ class MethodFieldResolverDataFetcherTest { options: SchemaParserOptions = SchemaParserOptions.defaultOptions() ): DataFetcher<*> { val field = FieldDefinition.newFieldDefinition() - .name(methodName) - .type(TypeName("Boolean")) - .inputValueDefinitions(arguments) - .build() + .name(methodName) + .type(TypeName("Boolean")) + .inputValueDefinitions(arguments) + .build() val resolverInfo = if (resolver is GraphQLQueryResolver) { RootResolverInfo(listOf(resolver), options) @@ -136,7 +281,7 @@ class MethodFieldResolverDataFetcherTest { return FieldResolverScanner(options).findFieldResolver(field, resolverInfo).createDataFetcher() } - private fun createEnvironment(source: Any, arguments: Map = emptyMap(), context: Any? = null): DataFetchingEnvironment { + private fun createEnvironment(source: Any = Object(), arguments: Map = emptyMap(), context: Any? = null): DataFetchingEnvironment { return DataFetchingEnvironmentImpl.newDataFetchingEnvironment(buildExecutionContext()) .source(source) .arguments(arguments) @@ -161,4 +306,10 @@ class MethodFieldResolverDataFetcherTest { } data class DataClass(val name: String = "TestName") + + class InputClass { + var name: String? = null + } + + class ContextClass } diff --git a/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt index d2d69bd7..d6ed8549 100644 --- a/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt @@ -5,7 +5,6 @@ import graphql.GraphQL import graphql.language.StringValue import graphql.schema.Coercing import graphql.schema.GraphQLScalarType -import org.junit.Assert import org.junit.Test import java.lang.reflect.InvocationHandler import java.lang.reflect.Method @@ -17,14 +16,14 @@ class MethodFieldResolverTest { @Test fun `should handle Optional type as method input argument`() { val schema = SchemaParser.newParser() - .schemaString(""" - type Query { - testValue(input: String): String - testOmitted(input: String): String - testNull(input: String): String - } - """ - ) + .schemaString( + """ + type Query { + testValue(input: String): String + testOmitted(input: String): String + testNull(input: String): String + } + """) .scalars(customScalarType) .resolvers(object : GraphQLQueryResolver { fun testValue(input: Optional) = input.toString() @@ -36,38 +35,35 @@ class MethodFieldResolverTest { val gql = GraphQL.newGraphQL(schema).build() - val result = gql - .execute(ExecutionInput.newExecutionInput() - .query(""" - query { - testValue(input: "test-value") - testOmitted - testNull(input: null) - } - """) - .context(Object()) - .root(Object())) + val result = gql.execute(ExecutionInput.newExecutionInput().query( + """ + query { + testValue(input: "test-value") + testOmitted + testNull(input: null) + } + """) + .context(Object()) + .root(Object())) - val expected = mapOf( + assertEquals(result.getData(), mapOf( "testValue" to "Optional[test-value]", "testOmitted" to "Optional.empty", "testNull" to "Optional.empty" - ) - - Assert.assertEquals(expected, result.getData()) + )) } @Test fun `should handle Optional type as method input argument with omission detection`() { val schema = SchemaParser.newParser() - .schemaString(""" - type Query { - testValue(input: String): String - testOmitted(input: String): String - testNull(input: String): String - } - """ - ) + .schemaString( + """ + type Query { + testValue(input: String): String + testOmitted(input: String): String + testNull(input: String): String + } + """) .scalars(customScalarType) .resolvers(object : GraphQLQueryResolver { fun testValue(input: Optional) = input.toString() @@ -82,37 +78,34 @@ class MethodFieldResolverTest { val gql = GraphQL.newGraphQL(schema).build() - val result = gql - .execute(ExecutionInput.newExecutionInput() - .query(""" - query { - testValue(input: "test-value") - testOmitted - testNull(input: null) - } - """) - .context(Object()) - .root(Object())) + val result = gql.execute(ExecutionInput.newExecutionInput().query( + """ + query { + testValue(input: "test-value") + testOmitted + testNull(input: null) + } + """) + .context(Object()) + .root(Object())) - val expected = mapOf( + assertEquals(result.getData(), mapOf( "testValue" to "Optional[test-value]", "testOmitted" to "null", "testNull" to "Optional.empty" - ) - - Assert.assertEquals(expected, result.getData()) + )) } @Test fun `should handle scalar types as method input argument`() { val schema = SchemaParser.newParser() - .schemaString(""" - scalar CustomScalar - type Query { - test(input: CustomScalar): Int - } - """.trimIndent() - ) + .schemaString( + """ + scalar CustomScalar + type Query { + test(input: CustomScalar): Int + } + """) .scalars(customScalarType) .resolvers(object : GraphQLQueryResolver { fun test(scalar: CustomScalar) = scalar.value.length @@ -122,30 +115,29 @@ class MethodFieldResolverTest { val gql = GraphQL.newGraphQL(schema).build() - val result = gql - .execute(ExecutionInput.newExecutionInput() - .query(""" - query Test(${"$"}input: CustomScalar) { - test(input: ${"$"}input) - } - """.trimIndent()) - .variables(mapOf("input" to "FooBar")) - .context(Object()) - .root(Object())) + val result = gql.execute(ExecutionInput.newExecutionInput().query( + """ + query Test(${"$"}input: CustomScalar) { + test(input: ${"$"}input) + } + """) + .variables(mapOf("input" to "FooBar")) + .context(Object()) + .root(Object())) - Assert.assertEquals(6, result.getData>()["test"]) + assertEquals(result.getData(), mapOf("test" to 6)) } @Test fun `should handle lists of scalar types`() { val schema = SchemaParser.newParser() - .schemaString(""" - scalar CustomScalar - type Query { - test(input: [CustomScalar]): Int - } - """.trimIndent() - ) + .schemaString( + """ + scalar CustomScalar + type Query { + test(input: [CustomScalar]): Int + } + """) .scalars(customScalarType) .resolvers(object : GraphQLQueryResolver { fun test(scalars: List) = scalars.map { it.value.length }.sum() @@ -155,18 +147,17 @@ class MethodFieldResolverTest { val gql = GraphQL.newGraphQL(schema).build() - val result = gql - .execute(ExecutionInput.newExecutionInput() - .query(""" - query Test(${"$"}input: [CustomScalar]) { - test(input: ${"$"}input) - } - """.trimIndent()) - .variables(mapOf("input" to listOf("Foo", "Bar"))) - .context(Object()) - .root(Object())) + val result = gql.execute(ExecutionInput.newExecutionInput().query( + """ + query Test(${"$"}input: [CustomScalar]) { + test(input: ${"$"}input) + } + """) + .variables(mapOf("input" to listOf("Foo", "Bar"))) + .context(Object()) + .root(Object())) - Assert.assertEquals(6, result.getData>()["test"]) + assertEquals(result.getData(), mapOf("test" to 6)) } @Test @@ -190,13 +181,13 @@ class MethodFieldResolverTest { ) as GraphQLQueryResolver val schema = SchemaParser.newParser() - .schemaString(""" - scalar CustomScalar - type Query { - test(input: [CustomScalar]): Int - } - """.trimIndent() - ) + .schemaString( + """ + scalar CustomScalar + type Query { + test(input: [CustomScalar]): Int + } + """) .scalars(customScalarType) .resolvers(resolver) .build() @@ -204,18 +195,17 @@ class MethodFieldResolverTest { val gql = GraphQL.newGraphQL(schema).build() - val result = gql - .execute(ExecutionInput.newExecutionInput() - .query(""" - query Test(${"$"}input: [CustomScalar]) { - test(input: ${"$"}input) - } - """.trimIndent()) - .variables(mapOf("input" to listOf("Foo", "Bar"))) - .context(Object()) - .root(Object())) + val result = gql.execute(ExecutionInput.newExecutionInput().query( + """ + query Test(${"$"}input: [CustomScalar]) { + test(input: ${"$"}input) + } + """) + .variables(mapOf("input" to listOf("Foo", "Bar"))) + .context(Object()) + .root(Object())) - Assert.assertEquals(6, result.getData>()["test"]) + assertEquals(result.getData(), mapOf("test" to 6)) } /** diff --git a/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt new file mode 100644 index 00000000..dcb6f8c1 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt @@ -0,0 +1,67 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import graphql.schema.GraphQLSchema +import org.junit.Test + +class MultiResolverTest { + + private val schema: GraphQLSchema = SchemaParser.newParser() + .schemaString( + """ + type Query { + person: Person + } + + type Person { + name: String! + friends(friendName: String!): [Friend!]! + } + + type Friend { + name: String! + } + """) + .resolvers(QueryWithPersonResolver(), PersonFriendResolver(), PersonNameResolver()) + .build() + .makeExecutableSchema() + private val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + @Test + fun `multiple resolvers for one data class should resolve methods with arguments`() { + val data = assertNoGraphQlErrors(gql, mapOf("friendName" to "name")) { + """ + query friendOfPerson(${'$'}friendName: String!) { + person { + friends(friendName: ${'$'}friendName) { + name + } + } + } + """ + } + + assertNotNull(data["person"]) + } + + class QueryWithPersonResolver : GraphQLQueryResolver { + fun getPerson(): Person = Person() + } + + class Person + + class Friend { + var name: String? = null + } + + class PersonFriendResolver : GraphQLResolver { + fun friends(person: Person, friendName: String): List = listOf() + } + + class PersonNameResolver : GraphQLResolver { + fun name(person: Person): String = "name" + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt new file mode 100644 index 00000000..2079cb91 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt @@ -0,0 +1,128 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import org.junit.Test + +class NestedInputTypesTest { + + @Test + fun `nested input types are parsed`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + materials(filter: MaterialFilter): [Material!]! + } + + input MaterialFilter { + title: String + requestFilter: RequestFilter + } + + input RequestFilter { + and: [RequestFilter!] + or: [RequestFilter!] + discountTypeFilter: DiscountTypeFilter + } + + input DiscountTypeFilter { + name: String + } + + type Material { + id: ID! + } + """) + .resolvers(QueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + val data = assertNoGraphQlErrors(gql, mapOf("filter" to mapOf("title" to "title", "requestFilter" to mapOf("discountTypeFilter" to mapOf("name" to "discount"))))) { + """ + query materials(${'$'}filter: MaterialFilter!) { + materials(filter: ${'$'}filter) { + id + } + } + """ + } + + assertEquals((data["materials"]), emptyList()) + } + + @Test + fun `nested input in extensions are parsed`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + materials(filter: MaterialFilter): [Material!]! + } + + input MaterialFilter { + title: String + } + + extend input MaterialFilter { + requestFilter: RequestFilter + } + + input RequestFilter { + and: [RequestFilter!] + or: [RequestFilter!] + discountTypeFilter: DiscountTypeFilter + } + + input DiscountTypeFilter { + name: String + } + + type Material { + id: ID! + } + """) + .resolvers(QueryResolver()) + .build() + .makeExecutableSchema() + val gql = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + val data = assertNoGraphQlErrors(gql, mapOf("filter" to mapOf("title" to "title", "requestFilter" to mapOf("discountTypeFilter" to mapOf("name" to "discount"))))) { + """ + query materials(${'$'}filter: MaterialFilter!) { + materials(filter: ${'$'}filter) { + id + } + } + """ + } + + assertEquals((data["materials"]), emptyList()) + } + + class QueryResolver : GraphQLQueryResolver { + fun materials(filter: MaterialFilter): List = listOf() + } + + class Material { + var id: Long? = null + } + + class MaterialFilter { + var title: String? = null + var requestFilter: RequestFilter? = null + } + + class RequestFilter { + var and: List? = null + var or: List? = null + var discountTypeFilter: DiscountTypeFilter? = null + } + + class DiscountTypeFilter { + var name: String? = null + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt b/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt new file mode 100644 index 00000000..2f6cc754 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt @@ -0,0 +1,65 @@ +package graphql.kickstart.tools + +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import graphql.schema.GraphQLSchema +import org.junit.Test + +class ParameterizedGetterTest { + + private val schema: GraphQLSchema = SchemaParser.newParser() + .schemaString( + """ + type Query { + human: Human + } + + type Human { + bestFriends: [Character!]! + allFriends(limit: Int!): [Character!]! + } + + type Character { + name: String! + } + """) + .resolvers(QueryResolver(), HumanResolver()) + .build() + .makeExecutableSchema() + private val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + @Test + fun `parameterized query is resolved on data type instead of on its resolver`() { + val data = assertNoGraphQlErrors(gql, mapOf("limit" to 10)) { + """ + query allFriends(${'$'}limit: Int!) { + human { + allFriends(limit: ${'$'}limit) { + name + } + } + } + """ + } + + assertNotNull(data["human"]) + } + + class QueryResolver : GraphQLQueryResolver { + fun human(): Human = Human() + } + + class Human { + fun allFriends(limit: Int): List = listOf() + } + + class HumanResolver : GraphQLResolver { + fun bestFriends(human: Human): List = listOf() + } + + class Character { + val name: String? = null + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt index f99fde40..40c08d3f 100644 --- a/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt @@ -4,7 +4,6 @@ import graphql.GraphQL import graphql.execution.AsyncExecutionStrategy import graphql.kickstart.tools.SchemaParser.Companion.newParser import graphql.kickstart.tools.SchemaParserOptions.Companion.newOptions -import groovy.lang.Closure import org.junit.Test import java.util.* import java.util.concurrent.CompletableFuture @@ -33,11 +32,9 @@ class ReactiveTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() - Utils.assertNoGraphQlErrors(gql, HashMap(), Any(), object : Closure(null) { - override fun call(): String { - return "query { organization(organizationId: 1) { user { id } } }" - } - }) + assertNoGraphQlErrors(gql) { + "query { organization(organizationId: 1) { user { id } } }" + } } private class Query : GraphQLQueryResolver { diff --git a/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt b/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt index 19a93b19..0906a90e 100644 --- a/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt @@ -5,50 +5,50 @@ import graphql.execution.AsyncExecutionStrategy import graphql.relay.Connection import graphql.relay.SimpleListConnection import graphql.schema.DataFetchingEnvironment -import groovy.lang.Closure -import org.junit.Assert import org.junit.Test class RelayConnectionTest { @Test fun `should compile relay schema when not using @connection directive`() { - val schema = SchemaParser.newParser().schemaString(""" - type Query { - users(first: Int, after: String): UserConnection - otherTypes: AnotherTypeConnection - } - - type UserConnection { - edges: [UserEdge!]! - pageInfo: PageInfo! - } - - type UserEdge { - node: User! - } - - type User { - id: ID! - name: String - } - - type PageInfo { - hasNextPage: Boolean - } - - type AnotherTypeConnection { - edges: [AnotherTypeEdge!]! - } - - type AnotherTypeEdge { - node: AnotherType! - } - - type AnotherType { - echo: String - } - """) + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + users(first: Int, after: String): UserConnection + otherTypes: AnotherTypeConnection + } + + type UserConnection { + edges: [UserEdge!]! + pageInfo: PageInfo! + } + + type UserEdge { + node: User! + } + + type User { + id: ID! + name: String + } + + type PageInfo { + hasNextPage: Boolean + } + + type AnotherTypeConnection { + edges: [AnotherTypeEdge!]! + } + + type AnotherTypeEdge { + node: AnotherType! + } + + type AnotherType { + echo: String + } + """) .resolvers(QueryResolver()) .build() .makeExecutableSchema() @@ -57,25 +57,26 @@ class RelayConnectionTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() - val result = gql.execute(""" - query { - users { - edges { - node { - id - name + val result = gql.execute( + """ + query { + users { + edges { + node { + id + name + } + } } - } - } - otherTypes { - edges { - node { - echo + otherTypes { + edges { + node { + echo + } + } } - } } - } - """) + """) val expected = mapOf( "users" to mapOf( @@ -94,7 +95,7 @@ class RelayConnectionTest { ) ) - Assert.assertEquals(expected, result.getData>>()) + assertEquals(result.getData(), expected) } @Test @@ -110,29 +111,27 @@ class RelayConnectionTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() - Utils.assertNoGraphQlErrors(gql, emptyMap(), object : Closure(null) { - override fun call(): String { - return """ - query { - users { - edges { + assertNoGraphQlErrors(gql) { + """ + query { + users { + edges { cursor node { - id - name + id + name } - } - pageInfo { + } + pageInfo { hasPreviousPage hasNextPage startCursor endCursor - } } - } - """ + } } - }) + """ + } } private class QueryResolver : GraphQLQueryResolver { diff --git a/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java b/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java index dc735681..8cb3c712 100644 --- a/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java +++ b/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java @@ -12,7 +12,8 @@ import static org.junit.Assert.assertTrue; public class ResolverMethodsTest { - // Note: don't convert this code to Kotlin or Groovy, since it's quite important that the + + // Note: don't convert this code to Kotlin, since it's quite important that the // resolver method is defined with an argument of primitive type, like 'boolean', not 'Boolean': // String testOmittedBoolean(boolean value1, Boolean value2) @Test @@ -20,30 +21,25 @@ public void testOmittedBooleanArgument() { // In this schema, the 'value1' argument is optional, but the Java resolver defines it as 'boolean' // Instead of failing with an error, we expect the argument to be set to the Java default (i.e. false for booleans) GraphQLSchema schema = SchemaParser.newParser() - .schemaString("" + - "type Query {" + - " testOmittedBoolean(value1: Boolean, value2: Boolean): String" + - "}") - .resolvers(new Resolver()) - .build() - .makeExecutableSchema(); + .schemaString("type Query { testOmittedBoolean(value1: Boolean, value2: Boolean): String }") + .resolvers(new Resolver()) + .build() + .makeExecutableSchema(); GraphQL gql = GraphQL.newGraphQL(schema).build(); ExecutionResult result = gql - .execute(ExecutionInput.newExecutionInput() - .query("" + - "query { " + - " testOmittedBoolean" + - "}") - .context(new Object()) - .root(new Object())); + .execute(ExecutionInput.newExecutionInput() + .query("query { testOmittedBoolean }") + .context(new Object()) + .root(new Object())); assertTrue(result.getErrors().isEmpty()); assertEquals("false,null", ((Map) result.getData()).get("testOmittedBoolean")); } static class Resolver implements GraphQLQueryResolver { + @SuppressWarnings("unused") public String testOmittedBoolean(boolean value1, Boolean value2) { return value1 + "," + value2; diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt new file mode 100644 index 00000000..16e533ac --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt @@ -0,0 +1,527 @@ +package graphql.kickstart.tools + +import graphql.schema.* +import org.junit.Test +import java.util.concurrent.CompletableFuture + +class SchemaClassScannerTest { + + @Test + fun `scanner handles futures and immediate return types`() { + SchemaParser.newParser() + .resolvers(FutureImmediateQuery()) + .schemaString( + """ + type Query { + future: Int! + immediate: Int! + } + """) + .build() + } + + private class FutureImmediateQuery : GraphQLQueryResolver { + fun future(): CompletableFuture = + CompletableFuture.completedFuture(1) + + fun immediate(): Int = 1 + } + + @Test + fun `scanner handles primitive and boxed return types`() { + SchemaParser.newParser() + .resolvers(PrimitiveBoxedQuery()) + .schemaString( + """ + type Query { + primitive: Int! + boxed: Int! + } + """) + .build() + } + + private class PrimitiveBoxedQuery : GraphQLQueryResolver { + fun primitive(): Int = 1 + + fun boxed(): Int? = null + } + + @Test + fun `scanner handles different scalars with same java class`() { + SchemaParser.newParser() + .resolvers(ScalarDuplicateQuery()) + .schemaString( + """ + type Query { + string: String! + id: ID! + } + """) + .build() + } + + private class ScalarDuplicateQuery : GraphQLQueryResolver { + fun string(): String = "" + fun id(): String = "" + } + + @Test + fun `scanner handles interfaces referenced by objects that aren't explicitly used`() { + val schema = SchemaParser.newParser() + .resolvers(InterfaceMissingQuery()) + .schemaString( + """ + interface Interface { + id: ID! + } + + type Query implements Interface { + id: ID! + } + """) + .build() + .makeExecutableSchema() + + val interfaceType = schema.additionalTypes.find { it is GraphQLInterfaceType } + assertNotNull(interfaceType) + } + + private class InterfaceMissingQuery : GraphQLQueryResolver { + fun id(): String = "" + } + + @Test + fun `scanner handles input types that reference other input types`() { + val schema = SchemaParser.newParser() + .resolvers(MultipleInputTypeQuery()) + .schemaString( + """ + input FirstInput { + id: String! + second: SecondInput! + third: ThirdInput! + } + input SecondInput { + id: String! + } + input ThirdInput { + id: String! + } + + type Query { + test(input: FirstInput): String! + } + """) + .build() + .makeExecutableSchema() + + val inputTypeCount = schema.additionalTypes.count { it is GraphQLInputType } + assertEquals(inputTypeCount, 3) + } + + private class MultipleInputTypeQuery : GraphQLQueryResolver { + + fun test(input: FirstInput): String = "" + + class FirstInput { + var id: String? = null + + fun second(): SecondInput = SecondInput() + var third: ThirdInput? = null + } + + class SecondInput { + var id: String? = null + } + + class ThirdInput { + var id: String? = null + } + } + + @Test + fun `scanner handles input types extensions`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { test: Boolean } + + type Mutation { + save(input: UserInput!): Boolean + } + + input UserInput { + name: String + } + + extend input UserInput { + password: String + } + """) + .resolvers( + object : GraphQLMutationResolver { + fun save(map: Map<*, *>): Boolean = true + }, + object : GraphQLQueryResolver { + fun test(): Boolean = true + } + ) + .build() + .makeExecutableSchema() + + val inputTypeExtensionCount = schema.additionalTypes + .filterIsInstance() + .flatMap { it.extensionDefinitions } + .count() + assertEquals(inputTypeExtensionCount, 1) + } + + @Test + fun `scanner allows multiple return types for custom scalars`() { + val schema = SchemaParser.newParser() + .resolvers(ScalarsWithMultipleTypes()) + .scalars(GraphQLScalarType.newScalar() + .name("UUID") + .description("Test scalars with duplicate types") + .coercing(object : Coercing { + override fun serialize(dataFetcherResult: Any?): Any? = null + override fun parseValue(input: Any?): Any? = null + override fun parseLiteral(input: Any?): Any? = null + }).build()) + .schemaString( + """ + scalar UUID + + type Query { + first: UUID + second: UUID + } + """) + .build() + .makeExecutableSchema() + + assert(schema.typeMap.containsKey("UUID")) + } + + class ScalarsWithMultipleTypes : GraphQLQueryResolver { + fun first(): Int? = null + fun second(): String? = null + } + + @Test + fun `scanner handles multiple interfaces that are not used as field types`() { + val schema = SchemaParser.newParser() + .resolvers(MultipleInterfaces()) + .schemaString( + """ + type Query { + query1: NamedResourceImpl + query2: VersionedResourceImpl + } + + interface NamedResource { + name: String! + } + + interface VersionedResource { + version: Int! + } + + type NamedResourceImpl implements NamedResource { + name: String! + } + + type VersionedResourceImpl implements VersionedResource { + version: Int! + } + """) + .build() + .makeExecutableSchema() + + val interfaceTypeCount = schema.additionalTypes.count { it is GraphQLInterfaceType } + assertEquals(interfaceTypeCount, 2) + } + + class MultipleInterfaces : GraphQLQueryResolver { + fun query1(): NamedResourceImpl? = null + fun query2(): VersionedResourceImpl? = null + + class NamedResourceImpl : NamedResource { + override fun name(): String? = null + } + + class VersionedResourceImpl : VersionedResource { + override fun version(): Int? = null + } + } + + interface NamedResource { + fun name(): String? + } + + interface VersionedResource { + fun version(): Int? + } + + @Test + fun `scanner handles interface implementation that is not used as field type`() { + val schema = SchemaParser.newParser() + // uncommenting the line below makes the test succeed + .dictionary(InterfaceImplementation.NamedResourceImpl::class) + .resolvers(InterfaceImplementation()) + .schemaString( + """ + type Query { + query1: NamedResource + } + + interface NamedResource { + name: String! + } + + type NamedResourceImpl implements NamedResource { + name: String! + } + """) + .build() + .makeExecutableSchema() + + val interfaceTypeCount = schema.additionalTypes.count { it is GraphQLInterfaceType } + assertEquals(interfaceTypeCount, 1) + } + + class InterfaceImplementation : GraphQLQueryResolver { + fun query1(): NamedResource? = null + + fun query2(): NamedResourceImpl? = null + + class NamedResourceImpl : NamedResource { + override fun name(): String? = null + } + } + + @Test + fun `scanner handles custom scalars when matching input types`() { + val customMap = GraphQLScalarType.newScalar() + .name("customMap") + .coercing(object : Coercing, Map> { + override fun serialize(dataFetcherResult: Any?): Map = mapOf() + override fun parseValue(input: Any?): Map = mapOf() + override fun parseLiteral(input: Any?): Map = mapOf() + }).build() + + val schema = SchemaParser.newParser() + .resolvers(object : GraphQLQueryResolver { + fun hasRawScalar(rawScalar: Map): Boolean = true + fun hasMapField(mapField: HasMapField): Boolean = true + }) + .scalars(customMap) + .schemaString( + """ + type Query { + hasRawScalar(customMap: customMap): Boolean + hasMapField(mapField: HasMapField): Boolean + } + + input HasMapField { + map: customMap + } + + scalar customMap + """) + .build() + .makeExecutableSchema() + + assert(schema.typeMap.containsKey("customMap")) + } + + class HasMapField { + var map: Map? = null + } + + @Test + fun `scanner allows class to be used for object type and input object type`() { + val schema = SchemaParser.newParser() + .resolvers(object : GraphQLQueryResolver { + fun test(pojo: Pojo): Pojo = pojo + }) + .schemaString( + """ + type Query { + test(inPojo: InPojo): OutPojo + } + + input InPojo { + name: String + } + + type OutPojo { + name: String + } + """) + .build() + .makeExecutableSchema() + + val typeCount = schema.additionalTypes.count() + assertEquals(typeCount, 2) + } + + class Pojo { + var name: String? = null + } + + @Test + fun `scanner should handle nested types in input types`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + schema { + query: Query + } + + type Query { + animal: Animal + } + + interface Animal { + type: ComplexType + } + + type Dog implements Animal { + type: ComplexType + } + + type ComplexType { + id: String + } + """) + .resolvers(NestedInterfaceTypeQuery()) + .dictionary(NestedInterfaceTypeQuery.Dog::class) + .build() + .makeExecutableSchema() + + val typeCount = schema.additionalTypes.count() + assertEquals(typeCount, 3) + } + + class NestedInterfaceTypeQuery : GraphQLQueryResolver { + fun animal(): Animal? = null + + class Dog : Animal { + override fun type(): ComplexType? = null + } + + class ComplexType { + var id: String? = null + } + } + + @Test + fun `scanner should handle unused types when option is true`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + # Let's say this is the Products service from Apollo Federation Introduction + + type Query { + allProducts: [Product] + } + + type Product { + name: String + } + + # these directives are defined in the Apollo Federation Specification: + # https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ + type User @key(fields: "id") @extends { + id: ID! @external + recentPurchasedProducts: [Product] + address: Address + } + + type Address { + street: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun allProducts(): List? = null + }) + .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) + .dictionary(User::class) + .build() + .makeExecutableSchema() + + val objectTypes = schema.additionalTypes.filterIsInstance() + assert(objectTypes.any { it.name == "User" }) + assert(objectTypes.any { it.name == "Address" }) + } + + class Product { + var name: String? = null + } + + class User { + var id: String? = null + var recentPurchasedProducts: List? = null + var address: Address? = null + } + + class Address { + var street: String? = null + } + + @Test + fun `scanner should handle unused types with interfaces when option is true`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + whatever: Whatever + } + + type Whatever { + value: String + } + + type Unused { + someInterface: SomeInterface + } + + interface SomeInterface { + value: String + } + + type Implementation implements SomeInterface { + value: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun whatever(): Whatever? = null + }) + .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) + .dictionary(Unused::class, Implementation::class) + .build() + .makeExecutableSchema() + + val objectTypes = schema.additionalTypes.filterIsInstance() + val interfaceTypes = schema.additionalTypes.filterIsInstance() + assert(objectTypes.any { it.name == "Unused" }) + assert(objectTypes.any { it.name == "Implementation" }) + assert(interfaceTypes.any { it.name == "SomeInterface" }) + } + + class Whatever { + var value: String? = null + } + + class Unused { + var someInterface: SomeInterface? = null + } + + class Implementation : SomeInterface { + override fun getValue(): String? { + return null + } + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserBuilderTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserBuilderTest.kt new file mode 100644 index 00000000..d9032e11 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserBuilderTest.kt @@ -0,0 +1,33 @@ +package graphql.kickstart.tools + +import graphql.kickstart.tools.SchemaParser.Companion.newParser +import graphql.parser.InvalidSyntaxException +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class SchemaParserBuilderTest(private val schema: String, private val error: String) { + + @Test + fun `parser errors should be returned in full`() { + try { + newParser() + .schemaString(schema) + .build() + } catch (e: InvalidSyntaxException) { + assert(e.toString().contains(error)) + } + } + + companion object { + @Parameterized.Parameters + @JvmStatic + fun data(): Collection> { + return listOf( + arrayOf("invalid", "offending token 'invalid' at line 1 column 1"), + arrayOf("type Query {\ninvalid!\n}", "offending token '!' at line 2 column 8") + ) + } + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt new file mode 100644 index 00000000..06854b6d --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt @@ -0,0 +1,440 @@ +package graphql.kickstart.tools + +import graphql.kickstart.tools.resolver.FieldResolverError +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.springframework.aop.framework.ProxyFactory +import java.io.FileNotFoundException +import java.util.concurrent.Future + +class SchemaParserTest { + private lateinit var builder: SchemaParserBuilder + + @Rule + @JvmField + var expectedEx: ExpectedException = ExpectedException.none() + + @Before + fun setup() { + builder = SchemaParser.newParser() + .schemaString( + """ + type Query { + get(int: Int!): Int! + } + """) + } + + @Test(expected = FileNotFoundException::class) + fun `builder throws FileNotFound exception when file is missing`() { + builder.file("/404").build() + } + + @Test + fun `builder doesn't throw FileNotFound exception when file is present`() { + SchemaParser.newParser().file("Test.graphqls") + .resolvers(object : GraphQLQueryResolver { + fun getId(): String = "1" + }) + .build() + } + + @Test(expected = SchemaClassScannerError::class) + fun `parser throws SchemaError when Query resolver is missing`() { + builder.build().makeExecutableSchema() + } + + @Test(expected = FieldResolverError::class) + fun `parser throws ResolverError when Query resolver is given without correct method`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + get(int: Int!): Int! + } + """) + .resolvers(object : GraphQLQueryResolver {}) + .build() + .makeExecutableSchema() + } + + @Test + fun `parser should parse correctly when Query resolver is given`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + get(int: Int!): Int! + } + """) + .resolvers(object : GraphQLQueryResolver { + fun get(i: Int): Int = i + }) + .build() + .makeExecutableSchema() + } + + @Test + fun `parser should parse correctly when multiple query resolvers are given`() { + SchemaParser.newParser() + .schemaString( + """ + type Obj { + name: String + } + + type AnotherObj { + key: String + } + + type Query { + obj: Obj + anotherObj: AnotherObj + } + """) + .resolvers(object : GraphQLQueryResolver { + fun getObj(): Obj = Obj() + }, object : GraphQLQueryResolver { + fun getAnotherObj(): AnotherObj = AnotherObj() + }) + .build() + .makeExecutableSchema() + } + + @Test + fun `parser should parse correctly when multiple resolvers for the same data type are given`() { + SchemaParser.newParser() + .schemaString( + """ + type RootObj { + obj: Obj + anotherObj: AnotherObj + } + + type Obj { + name: String + } + + type AnotherObj { + key: String + } + + type Query { + rootObj: RootObj + } + """) + .resolvers(object : GraphQLQueryResolver { + fun getRootObj(): RootObj { + return RootObj() + } + }, object : GraphQLResolver { + fun getObj(rootObj: RootObj): Obj { + return Obj() + } + }, object : GraphQLResolver { + fun getAnotherObj(rootObj: RootObj): AnotherObj { + return AnotherObj() + } + }) + .build() + .makeExecutableSchema() + } + + @Test + fun `parser should allow setting custom generic wrappers`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + one: Object! + two: Object! + } + + type Object { + name: String! + } + """) + .resolvers(object : GraphQLQueryResolver { + fun one(): CustomGenericWrapper? = null + fun two(): Obj? = null + }) + .options(SchemaParserOptions.newOptions().genericWrappers(SchemaParserOptions.GenericWrapper(CustomGenericWrapper::class, 1)).build()) + .build() + .makeExecutableSchema() + } + + @Test(expected = SchemaClassScannerError::class) + fun `parser should allow turning off default generic wrappers`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + one: Object! + two: Object! + } + + type Object { + toString: String! + } + """) + .resolvers(object : GraphQLQueryResolver { + fun one(): Future? = null + fun two(): Obj? = null + }) + .options(SchemaParserOptions.newOptions().useDefaultGenericWrappers(false).build()) + .build() + .makeExecutableSchema() + } + + @Test + fun `parser should throw descriptive exception when object is used as input type incorrectly`() { + expectedEx.expect(SchemaError::class.java) + expectedEx.expectMessage("Was a type only permitted for object types incorrectly used as an input type, or vice-versa") + + SchemaParser.newParser() + .schemaString( + """ + type Query { + name(filter: Filter): [String] + } + + type Filter { + filter: String + } + """) + .resolvers(object : GraphQLQueryResolver { + fun name(filter: Filter): List? = null + }) + .build() + .makeExecutableSchema() + + throw AssertionError("should not be called") + } + + @Test + fun `parser handles spring AOP proxied resolvers by default`() { + val resolver = ProxyFactory(ProxiedResolver()).proxy as GraphQLQueryResolver + + SchemaParser.newParser() + .schemaString( + """ + type Query { + test: [String] + } + """) + .resolvers(resolver) + .build() + } + + @Test + fun `parser handles enums with overridden toString method`() { + SchemaParser.newParser() + .schemaString( + """ + enum CustomEnum { + FOO + } + + type Query { + customEnum: CustomEnum + } + """) + .resolvers(object : GraphQLQueryResolver { + fun customEnum(): CustomEnum? = null + }) + .build() + .makeExecutableSchema() + } + + @Test + fun `parser should include source location for field definition`() { + val schema = SchemaParser.newParser() + .schemaString( + """ + |type Query { + | id: ID! + |} + """.trimMargin()) + .resolvers(QueryWithIdResolver()) + .build() + .makeExecutableSchema() + + val sourceLocation = schema.getObjectType("Query") + .getFieldDefinition("id") + .definition.sourceLocation + assertNotNull(sourceLocation) + assertEquals(sourceLocation.line, 2) + assertEquals(sourceLocation.column, 5) + assertNull(sourceLocation.sourceName) + } + + @Test + fun `parser should include source location for field definition when loaded from single classpath file`() { + val schema = SchemaParser.newParser() + .file("Test.graphqls") + .resolvers(QueryWithIdResolver()) + .build() + .makeExecutableSchema() + + val sourceLocation = schema.getObjectType("Query") + .getFieldDefinition("id") + .definition.sourceLocation + assertNotNull(sourceLocation) + assertEquals(sourceLocation.line, 2) + assertEquals(sourceLocation.column, 3) + assertEquals(sourceLocation.sourceName, "Test.graphqls") + } + + @Test + fun `support enum types if only used as input type`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { test: Boolean } + + type Mutation { + save(input: SaveInput!): Boolean + } + + input SaveInput { + type: EnumType! + } + + enum EnumType { + TEST + } + """) + .resolvers(object : GraphQLMutationResolver { + fun save(input: SaveInput): Boolean = false + inner class SaveInput { + var type: EnumType? = null + } + }, object : GraphQLQueryResolver { + fun test(): Boolean = false + }) + .dictionary(EnumType::class) + .build() + .makeExecutableSchema() + } + + @Test + fun `support enum types if only used in input Map`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { test: Boolean } + + type Mutation { + save(input: SaveInput!): Boolean + } + + input SaveInput { + age: Int + type: EnumType! + } + + enum EnumType { + TEST + } + """) + .resolvers(object : GraphQLMutationResolver { + fun save(input: Map<*, *>): Boolean = false + }, object : GraphQLQueryResolver { + fun test(): Boolean = false + }) + .dictionary(EnumType::class) + .build() + .makeExecutableSchema() + } + + @Test + fun `allow circular relations in input objects`() { + SchemaParser.newParser() + .schemaString( + """ + input A { + id: ID! + b: B + } + input B { + id: ID! + a: A + } + input C { + id: ID! + c: C + } + type Query { test: Boolean } + type Mutation { + test(input: A!): Boolean + testC(input: C!): Boolean + } + """) + .resolvers(object : GraphQLMutationResolver { + inner class A { + var id: String? = null + var b: B? = null + } + + inner class B { + var id: String? = null + var a: A? = null + } + + inner class C { + var id: String? = null + var c: C? = null + } + + fun test(a: A): Boolean { + return true + } + + fun testC(c: C): Boolean { + return true + } + }, object : GraphQLQueryResolver { + fun test(): Boolean = false + }) + .build() + .makeExecutableSchema() + } + + enum class EnumType { + TEST + } + + class QueryWithIdResolver : GraphQLQueryResolver { + fun getId(): String? = null + } + + class Filter { + fun filter(): String? = null + } + + class CustomGenericWrapper + + class Obj { + fun name() = null + } + + class AnotherObj { + fun key() = null + } + + class RootObj + + class ProxiedResolver : GraphQLQueryResolver { + fun test(): List = listOf() + } + + enum class CustomEnum { + FOO { + override fun toString(): String { + return "Bar" + } + } + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt new file mode 100644 index 00000000..c1f24ae9 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt @@ -0,0 +1,49 @@ +package graphql.kickstart.tools + +import org.junit.Test + +class SuperclassResolverTest { + + @Test + fun `methods from generic resolvers are resolved`() { + SchemaParser.newParser() + .schemaString( + """ + type Query { + bar: Bar! + } + + type Bar implements Foo{ + value: String + getValueWithSeveralParameters(arg1: Boolean!, arg2: String): String! + } + + interface Foo { + getValueWithSeveralParameters(arg1: Boolean!, arg2: String): String! + } + """) + .resolvers(QueryResolver(), BarResolver()) + .build() + .makeExecutableSchema() + } + + class QueryResolver : GraphQLQueryResolver { + fun getBar(): Bar = Bar() + } + + class Bar + + abstract class FooResolver : GraphQLResolver { + fun getValue(foo: T): String = "value" + + fun getValueWithSeveralParameters(foo: T, arg1: Boolean, arg2: String): String { + return if (arg1) { + "value" + } else { + arg2 + } + } + } + + class BarResolver : FooResolver() +} diff --git a/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt b/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt new file mode 100644 index 00000000..17a22a91 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/TestInterfaces.kt @@ -0,0 +1,16 @@ +package graphql.kickstart.tools + +interface Animal { + fun type(): SchemaClassScannerTest.NestedInterfaceTypeQuery.ComplexType? +} + +interface Vehicle { + fun getInformation(): VehicleInformation +} + +interface VehicleInformation + +interface SomeInterface { + fun getValue(): String? +} + diff --git a/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt b/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt new file mode 100644 index 00000000..955f7da5 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/TestUtils.kt @@ -0,0 +1,37 @@ +package graphql.kickstart.tools + +import com.fasterxml.jackson.databind.ObjectMapper +import graphql.ExecutionInput +import graphql.GraphQL + +private val mapper = ObjectMapper() + +fun assertNoGraphQlErrors(gql: GraphQL, args: Map = mapOf(), context: Any = Object(), closure: () -> String): Map { + val result = gql.execute(ExecutionInput.newExecutionInput() + .query(closure.invoke()) + .context(context) + .root(context) + .variables(args)) + + if (result.errors.isNotEmpty()) { + throw AssertionError("GraphQL result contained errors!\n${result.errors.map { mapper.writeValueAsString(it) }.joinToString { "\n" }}") + } + + return result.getData() as Map +} + +fun assertEquals(actual: T, expected: T) { + assert(actual == expected) { "expected:<$expected> but was:<$actual>" } +} + +fun assertNotEquals(actual: T, unexpected: T) { + assert(actual != unexpected) { "Values should be different. Actual: $actual" } +} + +fun assertNull(actual: T) { + assertEquals(actual, null) +} + +fun assertNotNull(actual: T) { + assert(actual != null) +} diff --git a/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt b/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt new file mode 100644 index 00000000..4fcb4b34 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt @@ -0,0 +1,181 @@ +package graphql.kickstart.tools + +import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper +import graphql.kickstart.tools.SchemaParserOptions.GenericWrapper.Companion.listCollectionWithTransformer +import graphql.kickstart.tools.resolver.FieldResolverScanner +import graphql.kickstart.tools.util.ParameterizedTypeImpl +import graphql.language.* +import graphql.language.Type +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Suite +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletableFuture.completedFuture +import java.util.concurrent.Future + +@RunWith(Suite::class) +@Suite.SuiteClasses( + TypeClassMatcherTest.Suit1::class, + TypeClassMatcherTest.Suit2::class, + TypeClassMatcherTest.Suit3::class, + TypeClassMatcherTest.Suit4::class +) +class TypeClassMatcherTest { + + companion object { + private val customType: Type<*> = TypeName("CustomType") + private val unwrappedCustomType: Type<*> = TypeName("UnwrappedGenericCustomType") + + private val customDefinition: TypeDefinition<*> = ObjectTypeDefinition("CustomType") + private val unwrappedCustomDefinition: TypeDefinition<*> = ObjectTypeDefinition("UnwrappedGenericCustomType") + + private val matcher: TypeClassMatcher = TypeClassMatcher(mapOf( + "CustomType" to customDefinition, + "UnwrappedGenericCustomType" to unwrappedCustomDefinition + )) + + private val options: SchemaParserOptions = SchemaParserOptions.newOptions().genericWrappers( + GenericWrapper( + GenericCustomType::class.java, + 0 + ), + listCollectionWithTransformer( + GenericCustomListType::class.java, + 0 + ) { x -> x } + ).build() + + private val scanner: FieldResolverScanner = FieldResolverScanner(options) + private val resolver = RootResolverInfo(listOf(QueryMethods()), options) + + private fun createPotentialMatch(methodName: String, graphQLType: Type<*>): TypeClassMatcher.PotentialMatch { + return scanner.findFieldResolver(FieldDefinition(methodName, graphQLType), resolver) + .scanForMatches() + .find { it.location == TypeClassMatcher.Location.RETURN_TYPE }!! + } + + private fun list(other: Type<*> = customType): Type<*> = ListType(other) + private fun nonNull(other: Type<*> = customType): Type<*> = NonNullType(other) + } + + @RunWith(Parameterized::class) + class Suit1(private val methodName: String, private val type: Type<*>) { + + @Test + fun `matcher verifies that nested return type matches graphql definition for method`() { + val match = matcher.match(createPotentialMatch(methodName, type)) + match as TypeClassMatcher.ValidMatch + assertEquals(match.type, customDefinition) + assertEquals(match.javaType, CustomType::class.java) + } + + companion object { + @Parameterized.Parameters + @JvmStatic + fun data(): Collection> { + return listOf( + arrayOf("type", customType), + arrayOf("futureType", customType), + arrayOf("listType", list()), + arrayOf("listListType", list(list())), + arrayOf("futureListType", list()), + arrayOf("listFutureType", list()), + arrayOf("listListFutureType", list(list())), + arrayOf("futureListListType", list(list())), + arrayOf("superType", customType), + arrayOf("superListFutureType", list(nonNull())), + arrayOf("nullableType", customType), + arrayOf("nullableListType", list(nonNull(customType))), + arrayOf("genericCustomType", customType), + arrayOf("genericListType", list()) + ) + } + } + } + + @RunWith(Parameterized::class) + class Suit2(private val methodName: String, private val type: Type<*>) { + + @Test(expected = SchemaClassScannerError::class) + fun `matcher verifies that nested return type doesn't match graphql definition for method`() { + matcher.match(createPotentialMatch(methodName, type)) + } + + companion object { + @Parameterized.Parameters + @JvmStatic + fun data(): Collection> { + return listOf( + arrayOf("type", list()), + arrayOf("futureType", list()) + ) + } + } + } + + @RunWith(Parameterized::class) + class Suit3(private val methodName: String, private val type: Type<*>) { + + @Test(expected = SchemaClassScannerError::class) + fun `matcher verifies return value optionals are used incorrectly for method`() { + matcher.match(createPotentialMatch(methodName, type)) + } + + companion object { + @Parameterized.Parameters + @JvmStatic + fun data(): Collection> { + return listOf( + arrayOf("nullableType", nonNull(customType)), + arrayOf("nullableNullableType", customType), + arrayOf("listNullableType", list(customType)) + ) + } + } + } + + class Suit4 { + @Test + fun `matcher allows unwrapped parameterized types as root types`() { + val match = matcher.match(createPotentialMatch("genericCustomUnwrappedType", unwrappedCustomType)) + match as TypeClassMatcher.ValidMatch + assertEquals(match.type, unwrappedCustomDefinition) + val javatype = match.javaType as ParameterizedTypeImpl + assertEquals(javatype.rawType, UnwrappedGenericCustomType::class.java) + assertEquals(javatype.actualTypeArguments.first(), CustomType::class.java) + } + } + + private abstract class Super : GraphQLQueryResolver { + fun superType(): Type = superType() + fun superListFutureType(): ListFutureType = superListFutureType() + } + + private class QueryMethods : Super>>() { + fun type(): CustomType = CustomType() + fun futureType(): Future = completedFuture(CustomType()) + fun listType(): List = listOf(CustomType()) + fun listListType(): List> = listOf(listOf(CustomType())) + fun futureListType(): CompletableFuture> = completedFuture(listOf(CustomType())) + fun listFutureType(): List> = listOf(completedFuture(CustomType())) + fun listListFutureType(): List>> = listOf(listOf(completedFuture(CustomType()))) + fun futureListListType(): CompletableFuture>> = completedFuture(listOf(listOf(CustomType()))) + fun nullableType(): Optional? = null + fun nullableListType(): Optional?>? = null + fun nullableNullableType(): Optional?>? = null + fun listNullableType(): List?> = listOf(null) + fun genericCustomType(): GenericCustomType = GenericCustomType() + fun genericListType(): GenericCustomListType = GenericCustomListType() + fun genericCustomUnwrappedType(): UnwrappedGenericCustomType = UnwrappedGenericCustomType() + } + + private class CustomType + + private class GenericCustomType + + private class GenericCustomListType + + private class UnwrappedGenericCustomType +}