diff --git a/pom.xml b/pom.xml index 92d32921..653ae2a2 100644 --- a/pom.xml +++ b/pom.xml @@ -276,8 +276,9 @@ + test-compile - test-compile + process-test-sources test-compile diff --git a/src/test/java17/graphql/kickstart/tools/SchemaClassScannerSealedJavaTypesTest.java b/src/test/java17/graphql/kickstart/tools/SchemaClassScannerSealedJavaTypesTest.java index 8880db03..de5022b0 100644 --- a/src/test/java17/graphql/kickstart/tools/SchemaClassScannerSealedJavaTypesTest.java +++ b/src/test/java17/graphql/kickstart/tools/SchemaClassScannerSealedJavaTypesTest.java @@ -50,6 +50,7 @@ interface Animal { .resolvers(new PetQuery(), new AnimalQuery()) .build() .makeExecutableSchema(); + EveryFieldResolvesKt.assertEveryFieldResolves(schema); ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(ExecutionInput.newExecutionInput().query(""" query { diff --git a/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt b/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt index 38f1d150..9a094a71 100644 --- a/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/BuiltInIdTest.kt @@ -33,6 +33,12 @@ class BuiltInIdTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() + @Test + fun `every field resolves`() { + // the made-up ID "1" isn't a UUID + schema.assertEveryFieldResolves(except = setOf("Query.itemByUuidId(id:)", "Query.itemsByUuidIds(ids:)")) + } + @Test fun `supports Long as ID as input`() { val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/CustomScalarInputTest.kt b/src/test/kotlin/graphql/kickstart/tools/CustomScalarInputTest.kt index 04b20e0e..f4311f80 100644 --- a/src/test/kotlin/graphql/kickstart/tools/CustomScalarInputTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/CustomScalarInputTest.kt @@ -191,6 +191,7 @@ class CustomScalarInputTest { .options(options) .build() .makeExecutableSchema() + .assertEveryFieldResolves() return GraphQL.newGraphQL(graphQLSchema).build() } @@ -265,7 +266,7 @@ class CustomScalarInputTest { fun optionalEvent(input: Optional): String = input.map { event(it) }.orElse("empty") fun message(input: MessageInput): String = "${input.name}: ${(listOfNotNull(input.file) + input.files).joinToString(" ") { it.content() }}" fun label(input: LabelInput): String = "${input.date} ${input.opaque}" - fun dateNode(input: ObjectNode): String = (input["date"] as POJONode).pojo.let { "${it.javaClass.simpleName} $it" } + fun dateNode(input: ObjectNode): String = (input["date"] as POJONode?)?.pojo?.let { "${it.javaClass.simpleName} $it" }.orEmpty() fun priceNode(input: ObjectNode): String = input.toString() fun payment(input: PaymentInput): String = listOfNotNull(input.price, input.reference, input.date, input.box).joinToString(" ") fun price(price: MoneyDto): String = price.toString() diff --git a/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt b/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt index 7707f2bc..22effb4d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DeepGenericsHierarchyTest.kt @@ -12,6 +12,7 @@ class PlaceTest { .file("Place.graphqls") .resolvers(PlaceQuery()) .build().makeExecutableSchema() + .assertEveryFieldResolves() val graphql = GraphQL.newGraphQL(schema).build() val query = "query { places1 { id } places2 { id } }" diff --git a/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt index 0acec3bb..acb96b54 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DeprecatedDirectiveTest.kt @@ -35,6 +35,7 @@ class DeprecatedDirectiveTest { .resolvers(UsersQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val userType = schema.getType("User") as GraphQLObjectType val nameDefinition = userType.getField("name") @@ -68,6 +69,7 @@ class DeprecatedDirectiveTest { .resolvers(UsersQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val userType = schema.getType("User") as GraphQLObjectType val nameDefinition = userType.getField("name") @@ -108,6 +110,7 @@ class DeprecatedDirectiveTest { .resolvers(UsersQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val userTypeEnum = schema.getType("UserType") as GraphQLEnumType val droidValue = userTypeEnum.getValue("DROID") @@ -148,6 +151,7 @@ class DeprecatedDirectiveTest { .resolvers(UsersQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val userTypeEnum = schema.getType("UserType") as GraphQLEnumType val droidValue = userTypeEnum.getValue("DROID") @@ -178,9 +182,10 @@ class DeprecatedDirectiveTest { name: String } """) - .resolvers(UsersQueryResolver()) + .resolvers(UsersWithFirstQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val usersQuery = schema.queryType.getField("users") val firstArgument = usersQuery.getArgument("first") @@ -211,9 +216,10 @@ class DeprecatedDirectiveTest { name: String } """) - .resolvers(UsersQueryResolver()) + .resolvers(UsersWithFirstQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val usersQuery = schema.queryType.getField("users") val firstArgument = usersQuery.getArgument("first") @@ -249,6 +255,7 @@ class DeprecatedDirectiveTest { .resolvers(UsersQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val directive = schema.getDirective("uppercase") val argument = directive!!.getArgument("firstCharacterOnly") @@ -284,6 +291,7 @@ class DeprecatedDirectiveTest { .resolvers(UsersQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val directive = schema.getDirective("uppercase") val argument = directive!!.getArgument("firstCharacterOnly") @@ -318,9 +326,10 @@ class DeprecatedDirectiveTest { name: String } """) - .resolvers(UsersQueryResolver()) + .resolvers(UsersWithConnectionInputQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val connectionInputType = schema.getType("ConnectionInput") as GraphQLInputObjectType val firstField = connectionInputType.getField("first") @@ -355,9 +364,10 @@ class DeprecatedDirectiveTest { name: String } """) - .resolvers(UsersQueryResolver()) + .resolvers(UsersWithConnectionInputQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val connectionInputType = schema.getType("ConnectionInput") as GraphQLInputObjectType val firstField = connectionInputType.getField("first") @@ -395,9 +405,10 @@ class DeprecatedDirectiveTest { type: UserType @deprecated(reason: "custom") } """) - .resolvers(UsersQueryResolver()) + .resolvers(UsersWithFirstQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val name = (schema.getType("User") as GraphQLObjectType).getField("name") assertEquals("No longer supported", name.getAppliedDirective("deprecated").getArgument("reason")!!.getValue()) @@ -415,16 +426,30 @@ class DeprecatedDirectiveTest { } private class UsersQueryResolver : GraphQLQueryResolver { - fun users(env: DataFetchingEnvironment): Connection { - return SimpleListConnection(listOf(User(1L, "Luke Skywalker", "Luke", "Skywalker", UserType.JEDI))).get(env) - } - - private data class User( - val id: Long, - val name: String, - val firstName: String, - val lastName: String, - val type: UserType - ) + fun users(env: DataFetchingEnvironment): Connection = SimpleListConnection(users).get(env) + } + + private class UsersWithFirstQueryResolver : GraphQLQueryResolver { + fun users(first: Int?, env: DataFetchingEnvironment): Connection = SimpleListConnection(users).get(env) + } + + private class UsersWithConnectionInputQueryResolver : GraphQLQueryResolver { + fun users(connectionInput: ConnectionInput?, env: DataFetchingEnvironment): Connection = SimpleListConnection(users).get(env) + } + + data class ConnectionInput( + val first: Int? + ) + + private data class User( + val id: Long, + val name: String, + val firstName: String, + val lastName: String, + val type: UserType + ) + + companion object { + private val users = listOf(User(1L, "Luke Skywalker", "Luke", "Skywalker", UserType.JEDI)) } } diff --git a/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt index b6503e34..39f4fe4b 100644 --- a/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt @@ -41,6 +41,7 @@ class DirectiveTest { .directive("uppercase", UppercaseDirective()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -93,6 +94,7 @@ class DirectiveTest { .directive("uppercase", UppercaseDirective()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -137,6 +139,7 @@ class DirectiveTest { .directive("uppercase", UppercaseDirective()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -179,6 +182,7 @@ class DirectiveTest { .directive("double", DoubleDirective()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -215,11 +219,14 @@ class DirectiveTest { name: String @auth @log } """) - .resolvers(NameResolver()) + .resolvers(object : GraphQLQueryResolver { + fun name(): String? = null + }) .directive("auth", DescriptionDirective("auth")) .directive("log", DescriptionDirective("log")) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.queryType.getField("name").description, "Name +auth +log") } @@ -236,11 +243,14 @@ class DirectiveTest { name: String @auth } """) - .resolvers(NameResolver()) + .resolvers(object : GraphQLQueryResolver { + fun name(): String? = null + }) .directive("auth", DescriptionDirective("auth")) .directiveWiring(DescriptionDirective("static")) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.queryType.getField("name").description, "Name +auth +static") } @@ -285,6 +295,7 @@ class DirectiveTest { .directive("uppercase", UppercaseDirective()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -337,6 +348,7 @@ class DirectiveTest { .dictionary(AllowedState::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -365,6 +377,7 @@ class DirectiveTest { .resolvers(BookQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val filter = schema.getType("BookFilter") as GraphQLInputObjectType assert(filter.isOneOf) @@ -404,6 +417,7 @@ class DirectiveTest { .directive("email", emailDirective) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val expectedMessages = mapOf( "contactEmail" to "{path} must be a valid email", @@ -452,6 +466,7 @@ class DirectiveTest { .directive("option", recorder) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val inputObject = listOf("GraphQLInputObjectType") val expectedTypes = mapOf("name" to inputObject, "ConfigInput" to inputObject, "level" to inputObject) @@ -490,6 +505,7 @@ class DirectiveTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assert(wiredFields == listOf("max")) { "$wiredFields with directives declared as $directives" } val limit = (schema.getType("BoundInput") as GraphQLInputObjectType).getField("max").getAppliedDirective("limit") @@ -531,6 +547,7 @@ class DirectiveTest { .dictionary(LinkPurpose::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.schemaAppliedDirectives.map { it.name }, listOf("contact", "link", "link")) assertEquals( @@ -594,6 +611,7 @@ class DirectiveTest { .options(SchemaParserOptions.newOptions().allowUndeclaredDirectives(true).build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val link = schema.getSchemaAppliedDirective("link") assertEquals(link.getArgument("import")?.getValue>(), listOf("@key", "@shareable")) @@ -644,7 +662,7 @@ class DirectiveTest { fun book(filter: BookFilter): Book? = null } - private data class BookFilter( + data class BookFilter( val id: Int?, val name: String? ) @@ -655,7 +673,7 @@ class DirectiveTest { fun updatePerson(person: PersonInput?, state: AllowedState?): String? = null } - private data class PersonInput( + data class PersonInput( val email: String? ) diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt index 305ef636..0d4a05a6 100644 --- a/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt +++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndSpecHelper.kt @@ -292,7 +292,7 @@ class Query : GraphQLQueryResolver, ListListResolver() { fun itemsWithOptionalInput(input: ItemSearchInput?) = if (input == null) items else items(input) fun itemsWithOptionalInputExplicit(input: Optional?) = if (input?.isPresent == true) items(input.get()) else items fun enumInputType(type: Type) = type - fun customScalarMapInputType(customScalarMap: Map) = customScalarMap + fun customScalarMapInputType(customScalarMap: Map?) = customScalarMap fun itemWithGenericProperties() = ItemWithGenericProperties(listOf("A", "B")) fun defaultArgument(arg: Boolean) = arg diff --git a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt index 85197516..e6f37c3a 100644 --- a/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/EndToEndTest.kt @@ -21,6 +21,15 @@ class EndToEndTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() + @Test + fun `every field resolves`() { + val newItem = Item(1, "item", Type.TYPE_1, UUID.randomUUID(), listOf()) + // throws on purpose to test that the original exception is passed on + schema.assertEveryFieldResolves(except = setOf("Query.throwsIllegalArgumentException")) { + graphQLContext(mapOf("newItem" to newItem)) + } + } + @Test fun `schema comments are used as descriptions`() { val type = schema.allTypesAsList.find { it.name == "Type" } as GraphQLEnumType diff --git a/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt b/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt index 20f72c57..69fc6b83 100644 --- a/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/EnumDefaultValueTest.kt @@ -29,6 +29,7 @@ class EnumDefaultValueTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val ggl = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -64,6 +65,7 @@ class EnumDefaultValueTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val ggl = GraphQL.newGraphQL(schema).build() diff --git a/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt b/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt index d0b0c5cd..89809617 100644 --- a/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/EnumListParameterTest.kt @@ -48,6 +48,11 @@ class EnumListParameterTest { assertEquals((data["countries"]), emptyList()) } + @Test + fun `every field resolves`() { + schema.assertEveryFieldResolves() + } + class QueryResolver : GraphQLQueryResolver { fun getCountries(regions: Set): Set { return setOf() diff --git a/src/test/kotlin/graphql/kickstart/tools/EveryFieldResolves.kt b/src/test/kotlin/graphql/kickstart/tools/EveryFieldResolves.kt new file mode 100644 index 00000000..c8661be6 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/EveryFieldResolves.kt @@ -0,0 +1,158 @@ +package graphql.kickstart.tools + +import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.GraphQL +import graphql.Scalars +import graphql.schema.* +import graphql.schema.GraphQLTypeUtil.isNonNull +import graphql.schema.GraphQLTypeUtil.unwrapAll +import org.reactivestreams.Publisher +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * Runs one operation per root field that selects every field reachable from it, and fails on any error. A + * subscription has to send an event, and that event's errors count too. + * + * Arguments without a default get a made-up value. Fields ("Type.field") and arguments ("Type.field(arg:)") named in + * [except] are left out. So is a field with a required argument that's left out or is a custom scalar, which has no + * value to make up. [input] adds to every operation, e.g. a context or a data loader registry. + */ +@JvmOverloads +fun GraphQLSchema.assertEveryFieldResolves(except: Set = setOf(), input: ExecutionInput.Builder.() -> Unit = {}): GraphQLSchema { + val gql = GraphQL.newGraphQL(this).build() + val query = EveryFieldQuery(this, except) + val roots = mapOf("query" to queryType, "mutation" to mutationType, "subscription" to subscriptionType) + val errors = roots.flatMap { (operation, root) -> + if (root == null) return@flatMap listOf() + query.fields(root).flatMap { field -> + val document = "$operation { $field }" + execute(gql, ExecutionInput.newExecutionInput(document).apply(input).build()).map { "$document\n $it" } + } + } + if (errors.isNotEmpty()) { + throw AssertionError("Querying every field returned errors:\n${errors.joinToString("\n")}") + } + return this +} + +private const val MAX_DEPTH = 6 + +private class EveryFieldQuery(private val schema: GraphQLSchema, private val except: Set) { + private val visibility = schema.codeRegistry.fieldVisibility + + fun fields(type: GraphQLFieldsContainer, path: List = listOf()): List = + visibility.getFieldDefinitions(type) + .filter { "${type.name}.${it.name}" !in except } + .mapNotNull { field(type, it, path + type.name) } + + private fun field(parent: GraphQLFieldsContainer, field: GraphQLFieldDefinition, path: List): String? { + val type = unwrapAll(field.type) + // a type can repeat once on a path, so a field that refers back to its own type still runs + if (type is GraphQLCompositeType && (path.size >= MAX_DEPTH || repeated(type, path))) { + return null + } + val arguments = field.arguments.filter { !it.hasSetDefaultValue() }.mapNotNull { argument -> + val value = if ("${parent.name}.${field.name}(${argument.name}:)" in except) null else value(argument.type, setOf()) + when { + value != null -> "${argument.name}: $value" + isNonNull(argument.type) -> return null + else -> null + } + } + val argumentList = if (arguments.isEmpty()) "" else arguments.joinToString(", ", "(", ")") + // aliased so fields with the same name but different types can sit side by side in fragments + return "${parent.name}_${field.name}: ${field.name}$argumentList ${selection(type, path)}" + } + + private fun selection(type: GraphQLNamedType, path: List): String = when (type) { + is GraphQLObjectType -> "{ __typename ${fields(type, path).joinToString(" ")} }" + is GraphQLInterfaceType -> fragments(schema.getImplementations(type) ?: listOf(), path) + is GraphQLUnionType -> fragments(type.types, path) + else -> "" + } + + private fun fragments(types: List, path: List) = + "{ __typename ${types.filter { !repeated(it, path) }.joinToString(" ") { "... on ${it.name} ${selection(it, path)}" }} }" + + private fun repeated(type: GraphQLNamedType, path: List) = path.count { it == type.name } >= 2 + + private fun value(type: GraphQLInputType, path: Set): String? = when (type) { + is GraphQLNonNull -> value(type.wrappedType as GraphQLInputType, path) + is GraphQLList -> value(type.wrappedType as GraphQLInputType, path)?.let { "[$it]" } + is GraphQLEnumType -> type.values.first().name + is GraphQLInputObjectType -> inputObject(type, path) + Scalars.GraphQLString -> "\"a\"" + Scalars.GraphQLID -> "\"1\"" + Scalars.GraphQLInt -> "1" + Scalars.GraphQLFloat -> "1.5" + Scalars.GraphQLBoolean -> "true" + else -> null + } + + private fun inputObject(type: GraphQLInputObjectType, path: Set): String? { + if (type.name in path) return null + val fields = type.fieldDefinitions.filter { !it.hasSetDefaultValue() }.mapNotNull { field -> + val value = value(field.type, path + type.name) + when { + value != null -> "${field.name}: $value" + isNonNull(field.type) -> return null + else -> null + } + } + // a @oneOf input takes exactly one field + if (type.isOneOf) return fields.firstOrNull()?.let { "{ $it }" } + return fields.joinToString(", ", "{ ", " }") + } +} + +private fun execute(gql: GraphQL, input: ExecutionInput): List { + // a resolver that never completes fails here instead of hanging the build + val result = try { + gql.executeAsync(input).get(5, TimeUnit.SECONDS) + } catch (e: TimeoutException) { + return listOf("Timed out") + } catch (e: ExecutionException) { + // graphql-java turns exceptions into GraphQL errors, but not an Error such as a failed assert + return listOf("Failed: ${e.cause}") + } + val events = result.getData() as? Publisher<*> ?: return result.errors.map { it.message } + return result.errors.map { it.message } + firstEventErrors(events) +} + +private fun firstEventErrors(events: Publisher<*>): List { + val first = CompletableFuture() + var subscription: Subscription? = null + events.subscribe(object : Subscriber { + override fun onSubscribe(s: Subscription) { + subscription = s + s.request(1) + } + + override fun onNext(event: Any?) { + first.complete(event as ExecutionResult) + } + + override fun onError(t: Throwable) { + first.completeExceptionally(t) + } + + override fun onComplete() { + first.completeExceptionally(IllegalStateException("completed without an event")) + } + }) + return try { + first.get(5, TimeUnit.SECONDS).errors.map { it.message } + } catch (e: TimeoutException) { + listOf("Subscription sent no event") + } catch (e: ExecutionException) { + listOf("Subscription failed: ${e.cause}") + } finally { + subscription?.cancel() + } +} diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt index 6856a07f..8d812b71 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericInputTypesTest.kt @@ -32,6 +32,7 @@ class GenericInputTypesTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -68,6 +69,7 @@ class GenericInputTypesTest { .resolvers(InheritedQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -112,6 +114,7 @@ class GenericInputTypesTest { .resolvers(MultipleParameterizationsQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -153,6 +156,7 @@ class GenericInputTypesTest { .resolvers(NestedQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -186,6 +190,7 @@ class GenericInputTypesTest { .resolvers(InheritedListQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt index c59e07a0..27c23466 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericObjectTypesTest.kt @@ -32,6 +32,7 @@ class GenericObjectTypesTest { .resolvers(EntityMatchQueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -89,6 +90,7 @@ class GenericObjectTypesTest { .dictionary(Human::class, Droid::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt index 0fc34a45..faf96990 100644 --- a/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/GenericResolverTest.kt @@ -23,6 +23,7 @@ class GenericResolverTest { .resolvers(QueryResolver1(), BarResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } class QueryResolver1 : GraphQLQueryResolver { @@ -54,6 +55,7 @@ class GenericResolverTest { .resolvers(QueryResolver2(), CarResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } class QueryResolver2 : GraphQLQueryResolver { @@ -132,6 +134,7 @@ class GenericResolverTest { .resolvers(QueryResolver3(), resolver) .build() .makeExecutableSchema() + .assertEveryFieldResolves() class QueryResolver3 : GraphQLQueryResolver { fun getPage(): Page = Page(listOf(Item("item"))) diff --git a/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt index dc0d1814..3edbf524 100644 --- a/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt @@ -31,6 +31,8 @@ class InaccessibleFieldResolverTest { .resolvers(Query()) .build() .makeExecutableSchema() + // without LocaleResolver, languageTag reads the private field of Locale, which the module system blocks + .assertEveryFieldResolves(except = setOf("Locale.languageTag")) val gql: GraphQL = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) .build() @@ -68,6 +70,7 @@ class InaccessibleFieldResolverTest { .resolvers(Query(), LocaleResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql: GraphQL = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) .build() diff --git a/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt b/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt index e2452304..606a5733 100644 --- a/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/InheritedTypeVariablesTest.kt @@ -33,6 +33,7 @@ class InheritedTypeVariablesTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -72,6 +73,7 @@ class InheritedTypeVariablesTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -119,6 +121,7 @@ class InheritedTypeVariablesTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -171,6 +174,7 @@ class InheritedTypeVariablesTest { .resolvers(RawGenericFixtures.QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/InputDefaultValueTest.kt b/src/test/kotlin/graphql/kickstart/tools/InputDefaultValueTest.kt index 74a02406..80c0f86d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/InputDefaultValueTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/InputDefaultValueTest.kt @@ -49,6 +49,11 @@ class InputDefaultValueTest { assertEquals(data["listWithEmptyDefault"], "size: 10, after: null") } + @Test + fun `every field resolves`() { + schema.assertEveryFieldResolves() + } + class QueryResolver : GraphQLQueryResolver { fun list(listing: Listing): String = describe(listing) fun listWithEmptyDefault(listing: Listing): String = describe(listing) diff --git a/src/test/kotlin/graphql/kickstart/tools/IntrospectionTest.kt b/src/test/kotlin/graphql/kickstart/tools/IntrospectionTest.kt index fa18e90d..44b97f95 100644 --- a/src/test/kotlin/graphql/kickstart/tools/IntrospectionTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/IntrospectionTest.kt @@ -23,6 +23,7 @@ class IntrospectionTest { .options(SchemaParserOptions.newOptions().introspectionEnabled(introspectionEnabled).build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() return GraphQL.newGraphQL(schema).build() } diff --git a/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt index 70200a8e..b630d0da 100644 --- a/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/MethodFieldResolverTest.kt @@ -38,6 +38,7 @@ class MethodFieldResolverTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() @@ -80,6 +81,7 @@ class MethodFieldResolverTest { .build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() @@ -112,10 +114,11 @@ class MethodFieldResolverTest { """) .scalars(customScalarType) .resolvers(object : GraphQLQueryResolver { - fun test(scalar: CustomScalar) = scalar.value.length + fun test(scalar: CustomScalar?) = scalar?.value?.length }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() @@ -143,10 +146,11 @@ class MethodFieldResolverTest { """) .scalars(customScalarType) .resolvers(object : GraphQLQueryResolver { - fun test(scalars: List) = scalars.map { it.value.length }.sum() + fun test(scalars: List?) = scalars?.map { it.value.length }?.sum() }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() @@ -165,12 +169,12 @@ class MethodFieldResolverTest { @Test fun `should handle proxies`() { val invocationHandler = object : InvocationHandler { - override fun invoke(proxy: Any, method: Method, args: Array): Any { + override fun invoke(proxy: Any, method: Method, args: Array): Any? { return when (method.name) { "toString" -> "Proxy$" + System.identityHashCode(this) "hashCode" -> System.identityHashCode(this) "equals" -> Proxy.isProxyClass(args[0].javaClass) - "test" -> (args[0] as List<*>).map { (it as CustomScalar).value.length }.sum() + "test" -> (args[0] as List<*>?)?.map { (it as CustomScalar).value.length }?.sum() else -> UnsupportedOperationException() } } @@ -194,6 +198,7 @@ class MethodFieldResolverTest { .resolvers(resolver) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() @@ -258,6 +263,7 @@ class MethodFieldResolverTest { .build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val result = gql.execute(ExecutionInput.newExecutionInput().query( @@ -297,7 +303,7 @@ class MethodFieldResolverTest { } interface Resolver { - fun test(scalars: List): Int + fun test(scalars: List?): Int? } private val customScalarType: GraphQLScalarType = GraphQLScalarType.newScalar() diff --git a/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt index a4962f67..3959216c 100644 --- a/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt @@ -48,6 +48,8 @@ class MissingFieldResolverTest { .build() .makeExecutableSchema() } + // missingField has no resolver, so querying it throws + schema.assertEveryFieldResolves(except = setOf("Query.missingField")) assertEquals(warnings, listOf( "Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved " + @@ -76,6 +78,7 @@ class MissingFieldResolverTest { .build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() @@ -116,6 +119,7 @@ class MissingFieldResolverTest { .options(options) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() diff --git a/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt index dcb6f8c1..0a91997a 100644 --- a/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/MultiResolverTest.kt @@ -30,6 +30,11 @@ class MultiResolverTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() + @Test + fun `every field resolves`() { + schema.assertEveryFieldResolves() + } + @Test fun `multiple resolvers for one data class should resolve methods with arguments`() { val data = assertNoGraphQlErrors(gql, mapOf("friendName" to "name")) { diff --git a/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt index 4c20ae06..4c1ea779 100644 --- a/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/NestedInputTypesTest.kt @@ -39,6 +39,7 @@ class NestedInputTypesTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) .build() @@ -89,6 +90,7 @@ class NestedInputTypesTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) .build() @@ -132,12 +134,13 @@ class NestedInputTypesTest { .resolvers(object : GraphQLQueryResolver { fun discountName(filter: Map): String? { val requestFilter = filter["requestFilter"] as Map<*, *> - val discountTypeFilter = (requestFilter["and"] as List<*>).single() as Map<*, *> - return (discountTypeFilter["discountTypeFilter"] as Map<*, *>)["name"] as String? + val discountTypeFilter = (requestFilter["and"] as List<*>?)?.single() as Map<*, *>? + return (discountTypeFilter?.get("discountTypeFilter") as Map<*, *>?)?.get("name") as String? } }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -190,6 +193,7 @@ class NestedInputTypesTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -239,6 +243,7 @@ class NestedInputTypesTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -281,6 +286,7 @@ class NestedInputTypesTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -325,6 +331,7 @@ class NestedInputTypesTest { .dictionary("ShipmentLineInput", ShipmentLineInput::class.java) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -373,6 +380,7 @@ class NestedInputTypesTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt b/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt index 2f6cc754..075895b0 100644 --- a/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/ParameterizedGetterTest.kt @@ -30,6 +30,11 @@ class ParameterizedGetterTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() + @Test + fun `every field resolves`() { + schema.assertEveryFieldResolves() + } + @Test fun `parameterized query is resolved on data type instead of on its resolver`() { val data = assertNoGraphQlErrors(gql, mapOf("limit" to 10)) { diff --git a/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt b/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt index 936c7c82..ed1ad899 100644 --- a/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/PropertyMapListTest.kt @@ -40,6 +40,7 @@ class PropertyMapListTest { .dictionary("Key", HashMap::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -83,6 +84,7 @@ class PropertyMapListTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt index 9531260c..6ef9f762 100644 --- a/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/ReactiveTest.kt @@ -31,6 +31,7 @@ class ReactiveTest { .options(options) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -91,7 +92,10 @@ class ReactiveTest { val options = newOptions() .genericWrappers(GenericWrapper.withTransformer(FutureTask::class, 0, { task -> task.get() })) .build() - val gql = GraphQL.newGraphQL(itemSchema(PlainFutureQuery(), options)).build() + val schema = itemSchema(PlainFutureQuery(), options) + // suspendCompletableItem is a suspend function that returns a future, which is rejected + .assertEveryFieldResolves(except = setOf("Query.suspendCompletableItem")) + val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { """ @@ -179,6 +183,7 @@ class ReactiveTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() diff --git a/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt b/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt index 0906a90e..97598d7d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/RelayConnectionTest.kt @@ -52,6 +52,8 @@ class RelayConnectionTest { .resolvers(QueryResolver()) .build() .makeExecutableSchema() + // the made-up cursor "a" isn't a valid cursor + .assertEveryFieldResolves(except = setOf("Query.users(after:)")) val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) @@ -106,6 +108,8 @@ class RelayConnectionTest { .dictionary(User::class.java) .build() .makeExecutableSchema() + // the made-up cursor "a" isn't a valid cursor + .assertEveryFieldResolves(except = setOf("Query.users(after:)")) val gql = GraphQL.newGraphQL(schema) .queryExecutionStrategy(AsyncExecutionStrategy()) diff --git a/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java b/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java index 864e8c69..17416eca 100644 --- a/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java +++ b/src/test/kotlin/graphql/kickstart/tools/ResolverMethodsTest.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; +import static graphql.kickstart.tools.EveryFieldResolvesKt.assertEveryFieldResolves; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -26,6 +27,7 @@ public void testOmittedBooleanArgument() { .resolvers(new Resolver()) .build() .makeExecutableSchema(); + assertEveryFieldResolves(schema); GraphQL gql = GraphQL.newGraphQL(schema).build(); @@ -63,6 +65,7 @@ public String name(Product product) { }) .build() .makeExecutableSchema(); + assertEveryFieldResolves(schema); GraphQL gql = GraphQL.newGraphQL(schema).build(); @@ -85,6 +88,7 @@ public void testRawResolverForParameterizedDataClass() { .resolvers(new PageQueryResolver(), new RawPageResolver()) .build() .makeExecutableSchema(); + assertEveryFieldResolves(schema); GraphQL gql = GraphQL.newGraphQL(schema).build(); diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt index 58bbdb8b..d4be4579 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerDirectiveTest.kt @@ -33,6 +33,7 @@ class SchemaClassScannerDirectiveTest { .scalars(customValueScalar) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val value = schema.queryType.getFieldDefinition("string") .getAppliedDirective("doSomething") @@ -74,6 +75,7 @@ class SchemaClassScannerDirectiveTest { .scalars(customValueScalar) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val value = schema.queryType.getFieldDefinition("string") .getAppliedDirective("doSomething") @@ -105,6 +107,7 @@ class SchemaClassScannerDirectiveTest { .scalars(customValueScalar) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val value = schema.queryType.getFieldDefinition("string") .getAppliedDirective("doSomething") @@ -134,6 +137,7 @@ class SchemaClassScannerDirectiveTest { .scalars(customValueScalar) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertNotNull(schema.directivesByName["doSomething"]?.getArgument("one")?.directivesByName?.get("somethingElse")) assertNotNull(schema.directivesByName["somethingElse"]?.getArgument("two")?.directivesByName?.get("doSomething")) @@ -147,6 +151,7 @@ class SchemaClassScannerDirectiveTest { .resolvers(helloResolver) .build() val schema = parser.makeExecutableSchema() + .assertEveryFieldResolves() val purpose = schema.getType("Purpose") as GraphQLEnumType assertEquals(purpose.values.map { it.value }, listOf("SECURITY", "EXECUTION")) @@ -172,6 +177,7 @@ class SchemaClassScannerDirectiveTest { .resolvers(helloResolver) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.getSchemaAppliedDirectives("link").map { it.getArgument("for")?.getValue() }, listOf("EXECUTION", "SECURITY")) assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { IntrospectionQuery.INTROSPECTION_QUERY } @@ -190,6 +196,7 @@ class SchemaClassScannerDirectiveTest { .dictionary(Purpose::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Purpose") as GraphQLEnumType).values.map { it.value }, Purpose.entries) } @@ -209,6 +216,7 @@ class SchemaClassScannerDirectiveTest { .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Purpose") as GraphQLEnumType).values.map { it.value }, Purpose.entries) } diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerInterfaceFieldTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerInterfaceFieldTest.kt index e3b8d5d7..de4c06b0 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerInterfaceFieldTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerInterfaceFieldTest.kt @@ -41,6 +41,7 @@ class SchemaClassScannerInterfaceFieldTest { .dictionary(InterfaceOnlyUnion.TestInterfaceImpl::class, InterfaceOnlyUnion.TestUnion2::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val testUnion = schema.getType("TestUnion") as GraphQLUnionType assertEquals(testUnion.types.map { it.name }.toSet(), setOf("TestUnion1", "TestUnion2")) @@ -124,6 +125,7 @@ class SchemaClassScannerInterfaceFieldTest { .resolvers(SingleMemberUnions()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Result") as GraphQLUnionType).types.map { it.name }, listOf("Node")) assertEquals((schema.getType("SortResponse") as GraphQLUnionType).types.map { it.name }, listOf("NodeSortResponse")) @@ -182,6 +184,7 @@ class SchemaClassScannerInterfaceFieldTest { .dictionary(ChainedInterfaceUnion.Box::class, ChainedInterfaceUnion.Image::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Content") as GraphQLUnionType).types.map { it.name }.toSet(), setOf("Text", "Image")) } @@ -246,6 +249,7 @@ class SchemaClassScannerInterfaceFieldTest { .dictionary(NestedInterfaceOnlyUnion.Book::class, NestedInterfaceOnlyUnion.Image::class, NestedInterfaceOnlyUnion.RichCaption::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Caption") as GraphQLUnionType).types.map { it.name }.toSet(), setOf("PlainCaption", "RichCaption")) } @@ -295,6 +299,7 @@ class SchemaClassScannerInterfaceFieldTest { .dictionary(InterfaceOnlyInterface.Box::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assert(schema.getType("Entry") is GraphQLInterfaceType) } @@ -348,6 +353,7 @@ class SchemaClassScannerInterfaceFieldTest { .dictionary(DictionaryReachableUnion.Book::class, DictionaryReachableUnion.Holder::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Content") as GraphQLUnionType).types.map { it.name }.toSet(), setOf("Text", "Image")) } @@ -409,6 +415,7 @@ class SchemaClassScannerInterfaceFieldTest { .dictionary(DictionaryReachableInterface.Shelf::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.getImplementations(schema.getType("Item") as GraphQLInterfaceType)!!.map { it.name }.toSet(), setOf("Book", "Magazine")) } @@ -467,6 +474,7 @@ class SchemaClassScannerInterfaceFieldTest { .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Content") as GraphQLUnionType).types.map { it.name }.toSet(), setOf("Text", "Image", "Caption")) } diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerSealedTypesTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerSealedTypesTest.kt index 9fa50c27..222c3af1 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerSealedTypesTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerSealedTypesTest.kt @@ -47,6 +47,7 @@ class SchemaClassScannerSealedTypesTest { .resolvers(SealedClassUnion()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { petsQuery } @@ -93,6 +94,7 @@ class SchemaClassScannerSealedTypesTest { .resolvers(SealedInterfaceImplementors()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { """ @@ -153,6 +155,7 @@ class SchemaClassScannerSealedTypesTest { .resolvers(GenericSealedClass()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { """ @@ -208,6 +211,7 @@ class SchemaClassScannerSealedTypesTest { .resolvers(MemberReachableThroughSealedSubclass()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("SearchResult") as GraphQLUnionType).types.map { it.name }.toSet(), setOf("Book", "Author")) @@ -254,6 +258,7 @@ class SchemaClassScannerSealedTypesTest { .dictionary("WeirdPet", DictionaryOverridesSealedSubclass.ClassifiedWeirdPet::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { petsQuery } diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt index ac998242..563a1556 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaClassScannerTest.kt @@ -1,5 +1,6 @@ package graphql.kickstart.tools +import com.fasterxml.jackson.annotation.JsonProperty import graphql.GraphQL import graphql.GraphQLContext import graphql.Scalars @@ -24,6 +25,8 @@ class SchemaClassScannerTest { } """) .build() + .makeExecutableSchema() + .assertEveryFieldResolves() } private class FutureImmediateQuery : GraphQLQueryResolver { @@ -45,12 +48,14 @@ class SchemaClassScannerTest { } """) .build() + .makeExecutableSchema() + .assertEveryFieldResolves() } private class PrimitiveBoxedQuery : GraphQLQueryResolver { fun primitive(): Int = 1 - fun boxed(): Int? = null + fun boxed(): Int? = 1 } @Test @@ -65,6 +70,8 @@ class SchemaClassScannerTest { } """) .build() + .makeExecutableSchema() + .assertEveryFieldResolves() } private class ScalarDuplicateQuery : GraphQLQueryResolver { @@ -88,6 +95,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val interfaceType = schema.additionalTypes.find { it is GraphQLInterfaceType } assertNotNull(interfaceType) @@ -121,6 +129,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val inputTypeCount = schema.additionalTypes.count { it is GraphQLInputType } assertEquals(inputTypeCount, 3) @@ -133,7 +142,9 @@ class SchemaClassScannerTest { class FirstInput { var id: String? = null - fun second(): SecondInput = SecondInput() + @field:JsonProperty + private var second = SecondInput() + fun second(): SecondInput = second var third: ThirdInput? = null } @@ -177,6 +188,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } private class FluentSetterMutation : GraphQLMutationResolver { @@ -186,6 +198,7 @@ class SchemaClassScannerTest { class RepairApply { var id: String? = null var repairMan: RepairMan? = null + @field:JsonProperty private var reviewer: RepairMan? = null @JvmField var approver: RepairMan? = null @@ -242,6 +255,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } private class GetterWithArgumentsQuery : GraphQLQueryResolver { @@ -291,6 +305,7 @@ class SchemaClassScannerTest { ) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val inputTypeExtensionCount = schema.additionalTypes .filterIsInstance() @@ -322,6 +337,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assert(schema.typeMap.containsKey("UUID")) } @@ -360,6 +376,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val interfaceTypeCount = schema.additionalTypes.count { it is GraphQLInterfaceType } assertEquals(interfaceTypeCount, 2) @@ -408,6 +425,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val interfaceTypeCount = schema.additionalTypes.count { it is GraphQLInterfaceType } assertEquals(interfaceTypeCount, 1) @@ -435,7 +453,7 @@ class SchemaClassScannerTest { val schema = SchemaParser.newParser() .resolvers(object : GraphQLQueryResolver { - fun hasRawScalar(rawScalar: Map): Boolean = true + fun hasRawScalar(rawScalar: Map?): Boolean = true fun hasMapField(mapField: HasMapField): Boolean = true }) .scalars(customMap) @@ -454,6 +472,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assert(schema.typeMap.containsKey("customMap")) } @@ -496,6 +515,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -531,6 +551,7 @@ class SchemaClassScannerTest { """) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val typeCount = schema.additionalTypes.count() assertEquals(typeCount, 2) @@ -569,6 +590,7 @@ class SchemaClassScannerTest { .dictionary(NestedInterfaceTypeQuery.Dog::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val typeCount = schema.additionalTypes.count() assertEquals(typeCount, 3) @@ -631,6 +653,7 @@ class SchemaClassScannerTest { .scalars(fieldSetScalar, linkImportScalar) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val objectTypes = schema.additionalTypes.filterIsInstance() assert(objectTypes.any { it.name == "User" }) @@ -723,6 +746,7 @@ class SchemaClassScannerTest { .dictionary(Unused::class, Implementation::class, SomeEnum::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val objectTypes = schema.additionalTypes.filterIsInstance() val interfaceTypes = schema.additionalTypes.filterIsInstance() @@ -764,6 +788,7 @@ class SchemaClassScannerTest { .dictionary(SomeEnum::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assert(schema.queryType.getFieldDefinition("preview").getArgument("value").type is GraphQLEnumType) } diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt index b5938d96..9a5f70c1 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserDictionaryTest.kt @@ -45,6 +45,7 @@ class SchemaParserDictionaryTest { .dictionary("NestedB", ObjectNode::class.java) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { @@ -92,6 +93,7 @@ class SchemaParserDictionaryTest { .dictionary(dictionary.getDictionary()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val field = schema.queryType.getFieldDefinition("test") assertEquals(field.getAppliedDirective("a").getArgument("arg")?.getValue>(), mapOf("x" to 1)) @@ -128,6 +130,7 @@ class SchemaParserDictionaryTest { .dictionary("DogInput", LabradorRetriever::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val field = schema.queryType.getFieldDefinition("dog") assertEquals(field.getAppliedDirective("d").getArgument("arg")?.getValue>(), mapOf("name" to "z")) diff --git a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt index 0eaa8aef..e70da4bf 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt @@ -48,6 +48,8 @@ class SchemaParserTest { fun getId(): String = "1" }) .build() + .makeExecutableSchema() + .assertEveryFieldResolves() } @Test(expected = SchemaClassScannerError::class) @@ -83,6 +85,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -110,6 +113,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -149,6 +153,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -160,18 +165,19 @@ class SchemaParserTest { one: Object! two: Object! } - + type Object { name: String! } """) .resolvers(object : GraphQLQueryResolver { - fun one(): CustomGenericWrapper? = null - fun two(): Obj? = null + fun one(): CustomGenericWrapper = CustomGenericWrapper(Obj()) + fun two(): Obj = Obj() }) - .options(SchemaParserOptions.newOptions().genericWrappers(SchemaParserOptions.GenericWrapper(CustomGenericWrapper::class, 1)).build()) + .options(SchemaParserOptions.newOptions().genericWrappers(SchemaParserOptions.GenericWrapper.withTransformer(CustomGenericWrapper::class, 1, { wrapper: CustomGenericWrapper<*, *> -> wrapper.value })).build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test(expected = SchemaClassScannerError::class) @@ -299,6 +305,7 @@ class SchemaParserTest { .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -364,7 +371,7 @@ class SchemaParserTest { enum CustomEnum { FOO } - + type Query { customEnum: CustomEnum } @@ -374,6 +381,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -388,6 +396,7 @@ class SchemaParserTest { .resolvers(QueryWithIdResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val sourceLocation = schema.getObjectType("Query")!! .getFieldDefinition("id") @@ -405,6 +414,7 @@ class SchemaParserTest { .resolvers(QueryWithIdResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val sourceLocation = schema.getObjectType("Query")!! .getFieldDefinition("id") @@ -472,6 +482,7 @@ class SchemaParserTest { .resolvers(QueryWithIdResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val sourceLocation = schema.getObjectType("Query")!! .getFieldDefinition("id") @@ -503,15 +514,13 @@ class SchemaParserTest { """) .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() + .assertEveryFieldResolves() } @Test @@ -542,6 +551,7 @@ class SchemaParserTest { .dictionary(EnumType::class) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -568,6 +578,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } @Test @@ -667,21 +678,6 @@ class SchemaParserTest { } """) .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 } @@ -694,6 +690,22 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() + } + + class A { + var id: String? = null + var b: B? = null + } + + class B { + var id: String? = null + var a: A? = null + } + + class C { + var id: String? = null + var c: C? = null } @Test @@ -729,6 +741,7 @@ class SchemaParserTest { .resolvers(MultiLevelInterfaceResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val traitInterface = schema.getType("Trait") as GraphQLInterfaceType val animalInterface = schema.getType("Animal") as GraphQLInterfaceType val mammalTraitInterface = schema.getType("MammalTrait") as GraphQLInterfaceType @@ -800,6 +813,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val testNonNullableArgument = schema.getObjectType("Query")!! .getFieldDefinition("testNonNullable") @@ -815,7 +829,7 @@ class SchemaParserTest { @Test fun `input objects behind list and non-null input fields should resolve to GraphQLInputObjectType during directive wiring`() { val wiredFieldTypes = mutableMapOf() - SchemaParser.newParser() + val schema = SchemaParser.newParser() .schemaString( """ type Query { @@ -849,6 +863,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() // parent points back at NestedFilter while it's still being built, so it stays a type reference until the schema is built assertEquals(wiredFieldTypes, mapOf( @@ -1005,6 +1020,7 @@ class SchemaParserTest { }) .build() .makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.queryType.name, "SubstituteQuery") assertEquals(schema.mutationType?.name, "SubstituteMutation") @@ -1015,14 +1031,18 @@ class SchemaParserTest { TEST } + class SaveInput { + var type: EnumType? = null + } + class SaveInputMap : HashMap() class QueryWithIdResolver : GraphQLQueryResolver { - fun getId(): String? = null + fun getId(): String = "1" } class Filter { - fun filter(): String? = null + var filter: String? = null } data class NestedFilter(val subs: List?, val other: OtherFilter) @@ -1031,10 +1051,10 @@ class SchemaParserTest { data class OtherFilter(val name: String?) - class CustomGenericWrapper + class CustomGenericWrapper(val value: V) class Obj { - fun name() = null + fun name() = "name" } class AnotherObj { @@ -1142,30 +1162,30 @@ class SchemaParserTest { fun onItemAdded(): Publisher = StringPublisher("added") } - val gql = GraphQL.newGraphQL( - SchemaParser.newParser() - .schemaString( - """ - type Query { - test: String - } + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + test: String + } - type Mutation { - addItem(name: String!): String! - } + type Mutation { + addItem(name: String!): String! + } - type Subscription { - onItemAdded: String! - } - """ - ) - .resolvers( - MutationAndSubscription(), - object : GraphQLQueryResolver { fun test() = "test" } - ) - .build() - .makeExecutableSchema() - ).build() + type Subscription { + onItemAdded: String! + } + """ + ) + .resolvers( + MutationAndSubscription(), + object : GraphQLQueryResolver { fun test() = "test" } + ) + .build() + .makeExecutableSchema() + .assertEveryFieldResolves() + val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { """mutation { addItem(name: "item") }""" } @@ -1180,23 +1200,23 @@ class SchemaParserTest { fun onItemAdded(): Publisher = StringPublisher("added") } - val gql = GraphQL.newGraphQL( - SchemaParser.newParser() - .schemaString( - """ - type Query { - item: String! - } + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + item: String! + } - type Subscription { - onItemAdded: String! - } - """ - ) - .resolvers(QueryAndSubscription()) - .build() - .makeExecutableSchema() - ).build() + type Subscription { + onItemAdded: String! + } + """ + ) + .resolvers(QueryAndSubscription()) + .build() + .makeExecutableSchema() + .assertEveryFieldResolves() + val gql = GraphQL.newGraphQL(schema).build() val data = assertNoGraphQlErrors(gql) { "{ item }" } @@ -1283,32 +1303,32 @@ class SchemaParserTest { override fun onOverriddenPublisher() = SingleEventPublisher("onOverriddenPublisher") } - val gql = GraphQL.newGraphQL( - SchemaParser.newParser() - .schemaString( - """ - type Query { - test: String - } + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + test: String + } - type Subscription { - onCustomPublisher: Event! - onConcretePublisher: Event! - onOverriddenPublisher: Event! - } + type Subscription { + onCustomPublisher: Event! + onConcretePublisher: Event! + onOverriddenPublisher: Event! + } - type Event { - name: String! - } - """ - ) - .resolvers( - Subscription(), - object : GraphQLQueryResolver { fun test() = "test" } - ) - .build() - .makeExecutableSchema() - ).build() + type Event { + name: String! + } + """ + ) + .resolvers( + Subscription(), + object : GraphQLQueryResolver { fun test() = "test" } + ) + .build() + .makeExecutableSchema() + .assertEveryFieldResolves() + val gql = GraphQL.newGraphQL(schema).build() listOf("onCustomPublisher", "onConcretePublisher", "onOverriddenPublisher").forEach { field -> assertEquals(gql.firstSubscriptionEvent("subscription { $field { name } }"), mapOf(field to mapOf("name" to field))) @@ -1325,34 +1345,34 @@ class SchemaParserTest { fun onWildcardPublisherFuture(): CompletionStage> = completedStage(SingleEventPublisher("onWildcardPublisherFuture")) } - val gql = GraphQL.newGraphQL( - SchemaParser.newParser() - .schemaString( - """ - type Query { - test: String - } + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + test: String + } - type Subscription { - onCompletableFuture: Event! - onCompletionStage: Event! - onFuture: Event! - onConcretePublisherFuture: Event! - onWildcardPublisherFuture: Event! - } + type Subscription { + onCompletableFuture: Event! + onCompletionStage: Event! + onFuture: Event! + onConcretePublisherFuture: Event! + onWildcardPublisherFuture: Event! + } - type Event { - name: String! - } - """ - ) - .resolvers( - Subscription(), - object : GraphQLQueryResolver { fun test() = "test" } - ) - .build() - .makeExecutableSchema() - ).build() + type Event { + name: String! + } + """ + ) + .resolvers( + Subscription(), + object : GraphQLQueryResolver { fun test() = "test" } + ) + .build() + .makeExecutableSchema() + .assertEveryFieldResolves() + val gql = GraphQL.newGraphQL(schema).build() listOf("onCompletableFuture", "onCompletionStage", "onFuture", "onConcretePublisherFuture", "onWildcardPublisherFuture").forEach { field -> assertEquals(gql.firstSubscriptionEvent("subscription { $field { name } }"), mapOf(field to mapOf("name" to field))) @@ -1365,33 +1385,33 @@ class SchemaParserTest { fun onChannel(): Channel = Channel(1).also { it.trySend("onChannel") } } - val gql = GraphQL.newGraphQL( - SchemaParser.newParser() - .schemaString( - """ - type Query { - test: String - } + val schema = SchemaParser.newParser() + .schemaString( + """ + type Query { + test: String + } - type Subscription { - onChannel: String! - } - """ - ) - .resolvers( - Subscription(), - object : GraphQLQueryResolver { fun test() = "test" } - ) - .options( - SchemaParserOptions.newOptions() - .genericWrappers(SchemaParserOptions.GenericWrapper.withTransformer(Channel::class, 0, { channel: Channel<*> -> - publish { for (item in channel) send(item) } - })) - .build() - ) - .build() - .makeExecutableSchema() - ).build() + type Subscription { + onChannel: String! + } + """ + ) + .resolvers( + Subscription(), + object : GraphQLQueryResolver { fun test() = "test" } + ) + .options( + SchemaParserOptions.newOptions() + .genericWrappers(SchemaParserOptions.GenericWrapper.withTransformer(Channel::class, 0, { channel: Channel<*> -> + publish { for (item in channel) send(item) } + })) + .build() + ) + .build() + .makeExecutableSchema() + .assertEveryFieldResolves() + val gql = GraphQL.newGraphQL(schema).build() assertEquals(gql.firstSubscriptionEvent("subscription { onChannel }"), mapOf("onChannel" to "onChannel")) } diff --git a/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt index c1f24ae9..41eaa0a5 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SuperclassResolverTest.kt @@ -25,6 +25,7 @@ class SuperclassResolverTest { .resolvers(QueryResolver(), BarResolver()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() } class QueryResolver : GraphQLQueryResolver { diff --git a/src/test/kotlin/graphql/kickstart/tools/SupertypeResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/SupertypeResolverTest.kt index f3d1ef1d..efab89c8 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SupertypeResolverTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SupertypeResolverTest.kt @@ -41,6 +41,11 @@ class SupertypeResolverTest { .queryExecutionStrategy(AsyncExecutionStrategy()) .build() + @Test + fun `every field resolves`() { + schema.assertEveryFieldResolves() + } + @Test fun `resolvers for a superclass or interface should resolve fields of subclasses`() { val data = assertNoGraphQlErrors(gql) { diff --git a/src/test/kotlin/graphql/kickstart/tools/SuspendFunctionDataLoaderTest.kt b/src/test/kotlin/graphql/kickstart/tools/SuspendFunctionDataLoaderTest.kt index 20b8a4e7..a3d95e8b 100644 --- a/src/test/kotlin/graphql/kickstart/tools/SuspendFunctionDataLoaderTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/SuspendFunctionDataLoaderTest.kt @@ -30,6 +30,11 @@ class SuspendFunctionDataLoaderTest { .makeExecutableSchema() private val gql = GraphQL.newGraphQL(schema).build() + @Test + fun `every field resolves`() { + schema.assertEveryFieldResolves() { dataLoaderRegistry(registry()) } + } + @Test fun `root suspend function can await a data loader`() { repeat(20) { @@ -52,19 +57,21 @@ class SuspendFunctionDataLoaderTest { } private fun execute(query: String): Any? { - val userLoader = DataLoaderFactory.newDataLoader { ids -> - CompletableFuture.supplyAsync { ids.map { User(it) } } - } - val registry = DataLoaderRegistry.newRegistry().register("user", userLoader).build() - // a resolver that awaits a load before it is dispatched hangs, so don't wait forever - val result = gql.executeAsync(ExecutionInput.newExecutionInput(query).dataLoaderRegistry(registry)) + val result = gql.executeAsync(ExecutionInput.newExecutionInput(query).dataLoaderRegistry(registry())) .get(5, TimeUnit.SECONDS) assert(result.errors.isEmpty()) { result.errors.joinToString { it.message } } return result.getData() } + private fun registry(): DataLoaderRegistry { + val userLoader = DataLoaderFactory.newDataLoader { ids -> + CompletableFuture.supplyAsync { ids.map { User(it) } } + } + return DataLoaderRegistry.newRegistry().register("user", userLoader).build() + } + class Query : GraphQLQueryResolver { fun user(id: Int): User = User(id) diff --git a/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt b/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt index d89194f6..0e86778d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt @@ -52,6 +52,7 @@ class TypeExtensionTest { dictionary(Foo::class, Bar::class, Baz::class, Item::class) scalars(GraphQLScalarType.newScalar().name("Date").coercing(Scalars.GraphQLString.coercing).build()) }.makeExecutableSchema() + .assertEveryFieldResolves() val fooBar = schema.getType("FooBar") as GraphQLUnionType val color = schema.getType("Color") as GraphQLEnumType @@ -107,6 +108,7 @@ class TypeExtensionTest { """) { dictionary(Foo::class, Bar::class, Baz::class) }.makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("FooBar") as GraphQLUnionType).types.map { it.name }, listOf("Foo", "Bar", "Baz")) } @@ -130,6 +132,7 @@ class TypeExtensionTest { """) { dictionary(Item::class, Bar::class) }.makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Node") as GraphQLInterfaceType).fieldDefinitions.map { it.name }, listOf("id", "fooBar")) assertEquals((schema.getType("FooBar") as GraphQLUnionType).types.map { it.name }.toSet(), setOf("Foo", "Bar")) @@ -231,7 +234,7 @@ class TypeExtensionTest { type Item implements Node @tag(name: "Item") { id: ID! } extend type Item @tag(name: "Item extension") { name: String } - union FooBar @tag(name: "FooBar") = Foo + union FooBar @tag(name: "FooBar") = Baz extend union FooBar @tag(name: "FooBar extension") enum Color @tag(name: "Color") { RED GREEN } @@ -240,13 +243,14 @@ class TypeExtensionTest { input Filter @tag(name: "Filter") { id: ID } extend input Filter @tag(name: "Filter extension") @once(name: "Filter extension") - type Foo { foo: String } + type Baz { baz: String } """) { - dictionary(Item::class, Foo::class) + dictionary(Item::class, Baz::class) directive("tag", wiring) directive("once", wiring) }.makeExecutableSchema() } + schema.assertEveryFieldResolves() assertEquals(warnings, listOf("Directive @once isn't repeatable but an extension of type 'Query' applies it again, only the first one is kept.")) @@ -295,6 +299,7 @@ class TypeExtensionTest { """) { options(SchemaParserOptions.newOptions().allowUndeclaredDirectives(true).build()) }.makeExecutableSchema() + .assertEveryFieldResolves() assertEquals(schema.queryType.appliedDirectives.map { it.getArgument("fields")?.getValue() }, listOf("upc", "name")) } @@ -322,6 +327,7 @@ class TypeExtensionTest { dictionary(Foo::class, Bar::class, Baz::class) options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) }.makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("FooBar") as GraphQLUnionType).types.map { it.name }, listOf("Foo", "Bar", "Baz")) } @@ -359,6 +365,7 @@ class TypeExtensionTest { dictionary(Item::class, Foo::class, Bar::class, Baz::class) }.makeExecutableSchema() } + schema.assertEveryFieldResolves() assertEquals(warnings, listOf( "Type 'Node' declares field 'id' more than once, only the first one is kept.", @@ -395,6 +402,7 @@ class TypeExtensionTest { dictionary(Item::class) }.makeExecutableSchema() } + schema.assertEveryFieldResolves() assertEquals(warnings, listOf( "Type 'Node' is defined as an interface, but extended as an object type. The extension is ignored.", @@ -450,6 +458,7 @@ class TypeExtensionTest { enum Purpose { SECURITY } extend enum Purpose { EXECUTION } """).makeExecutableSchema() + .assertEveryFieldResolves() assertEquals((schema.getType("Purpose") as GraphQLEnumType).values.map { it.value }, listOf("SECURITY", "EXECUTION")) assertEquals(schema.getSchemaAppliedDirectives("link").map { it.getArgument("for")?.getValue() }, listOf("EXECUTION")) diff --git a/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt b/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt index da8f5acd..949e252d 100644 --- a/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt +++ b/src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt @@ -201,6 +201,7 @@ class UnmappedInputFieldsTest { .build() .makeExecutableSchema() } + schema.assertEveryFieldResolves() assertEquals(warnings, listOf(unmapped("GreetingInput.name", GreetingInput::class.java, "argument 'input' of field 'Query.greet'", "nmae", "drops the field's value, since the ObjectMapper ignores unknown properties"))) @@ -249,7 +250,10 @@ class UnmappedInputFieldsTest { } assertEquals(error.message, unmapped("NameInput.firstName", NameInput::class.java, "argument 'input' of field 'Query.snake'", "first_name")) - val gql = GraphQL.newGraphQL(parser(SchemaParserOptions.newOptions()).makeExecutableSchema()).build() + val schema = parser(SchemaParserOptions.newOptions()).makeExecutableSchema() + // the snake case ObjectMapper doesn't know firstName, so snake fails when queried + .assertEveryFieldResolves(except = setOf("Query.snake")) + val gql = GraphQL.newGraphQL(schema).build() val result = gql.execute("query { snake(input: { firstName: \"Ada\" }) camel(input: { firstName: \"Ada\" }) }") assertContains(result.errors.single().message, "Unrecognized field \"firstName\"") assertEquals(result.getData>()!!["camel"], "Ada") @@ -459,6 +463,7 @@ class UnmappedInputFieldsTest { .build() .makeExecutableSchema() } + schema.assertEveryFieldResolves() assertEquals(warnings, emptyList()) @@ -550,6 +555,7 @@ class UnmappedInputFieldsTest { .options(failOptions().build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves() val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { """ @@ -643,6 +649,8 @@ class UnmappedInputFieldsTest { .build()) .build() .makeExecutableSchema() + // the made-up kind isn't one of the type ids Jackson knows + .assertEveryFieldResolves(except = setOf("Query.shape(input:)", "Query.animals(input:)", "Query.animal(input:)", "Query.zoo(input:)")) val data = assertNoGraphQlErrors(GraphQL.newGraphQL(schema).build()) { """ @@ -712,6 +720,12 @@ class UnmappedInputFieldsTest { .build()) .build() .makeExecutableSchema() + .assertEveryFieldResolves(except = setOf( + // the input classes misspell name on purpose + "Query.injected", "Query.builder", + // TODO query generic once the type variable of its inherited parameter is resolved (#880) + "Query.generic" + )) val result = GraphQL.newGraphQL(schema).build().execute( """