Skip to content

Commit 3dcbbb7

Browse files
authored
Merge pull request #878 from graphql-java-kickstart/test/867-log-capture
Test the schema warnings through a shared log-capture helper
2 parents 7205867 + 0a4659d commit 3dcbbb7

4 files changed

Lines changed: 134 additions & 84 deletions

File tree

‎src/test/kotlin/graphql/kickstart/tools/MissingFieldResolverTest.kt‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ class MissingFieldResolverTest {
2929
.makeExecutableSchema()
3030
}
3131

32+
@Test
33+
fun `should warn about each missing field when unimplemented resolvers are allowed`() {
34+
val (schema, warnings) = withWarnings {
35+
SchemaParser.newParser()
36+
.schemaString(
37+
"""
38+
type Query {
39+
implementedField(input: String): String
40+
missingField(input: Int): Int
41+
}
42+
"""
43+
)
44+
.resolvers(object : GraphQLQueryResolver {
45+
fun implementedField(input: Optional<String>) = input.toString()
46+
})
47+
.options(SchemaParserOptions.newOptions().allowUnimplementedResolvers(true).build())
48+
.build()
49+
.makeExecutableSchema()
50+
}
51+
52+
assertEquals(warnings, listOf(
53+
"Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved " +
54+
"to query time instead of schema creation time. Make sure this is turned off in production.",
55+
"Missing resolver for field: ${schema.queryType.getFieldDefinition("missingField").definition}"
56+
))
57+
}
58+
3259
@Test
3360
@Suppress("DEPRECATION")
3461
fun `should call missing resolver data fetcher if provided`() {

‎src/test/kotlin/graphql/kickstart/tools/TestUtils.kt‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package graphql.kickstart.tools
22

3+
import ch.qos.logback.classic.Level
4+
import ch.qos.logback.classic.Logger
5+
import ch.qos.logback.classic.spi.ILoggingEvent
6+
import ch.qos.logback.core.read.ListAppender
37
import com.fasterxml.jackson.databind.ObjectMapper
48
import graphql.ExecutionInput
59
import graphql.GraphQL
10+
import org.slf4j.LoggerFactory
611

712
private val mapper = ObjectMapper()
813

@@ -37,3 +42,19 @@ fun <T> assertNull(actual: T) {
3742
fun <T> assertNotNull(actual: T) {
3843
assert(actual != null)
3944
}
45+
46+
fun <T> withWarnings(block: () -> T): Pair<T, List<String>> {
47+
// the tests run in the library's module, which doesn't read logback's
48+
listOf("ch.qos.logback.classic", "ch.qos.logback.core").forEach { name ->
49+
ModuleLayer.boot().findModule(name).ifPresent { SchemaParser::class.java.module.addReads(it) }
50+
}
51+
val logger = LoggerFactory.getLogger("graphql.kickstart.tools") as Logger
52+
val appender = ListAppender<ILoggingEvent>().apply { start() }
53+
logger.addAppender(appender)
54+
try {
55+
val result = block()
56+
return result to appender.list.filter { it.level.isGreaterOrEqual(Level.WARN) }.map { it.formattedMessage }
57+
} finally {
58+
logger.detachAppender(appender)
59+
}
60+
}

‎src/test/kotlin/graphql/kickstart/tools/TypeExtensionTest.kt‎

Lines changed: 86 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -210,41 +210,45 @@ class TypeExtensionTest {
210210
@Test
211211
fun `directives of extensions follow those of the type and are wired`() {
212212
val wiring = RecordingWiring()
213-
val schema = parser(
214-
"""
215-
directive @tag(name: String!) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT
216-
directive @once(name: String!) on OBJECT | INPUT_OBJECT
213+
val (schema, warnings) = withWarnings {
214+
parser(
215+
"""
216+
directive @tag(name: String!) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT
217+
directive @once(name: String!) on OBJECT | INPUT_OBJECT
217218
218-
type Query @once(name: "Query") {
219-
node: Node
220-
fooBar: FooBar
221-
color: Color
222-
search(filter: Filter): String
223-
}
219+
type Query @once(name: "Query") {
220+
node: Node
221+
fooBar: FooBar
222+
color: Color
223+
search(filter: Filter): String
224+
}
224225
225-
extend type Query @once(name: "Query extension")
226+
extend type Query @once(name: "Query extension")
226227
227-
interface Node @tag(name: "Node") { id: ID! }
228-
extend interface Node @tag(name: "Node extension")
228+
interface Node @tag(name: "Node") { id: ID! }
229+
extend interface Node @tag(name: "Node extension")
229230
230-
type Item implements Node @tag(name: "Item") { id: ID! }
231-
extend type Item @tag(name: "Item extension") { name: String }
231+
type Item implements Node @tag(name: "Item") { id: ID! }
232+
extend type Item @tag(name: "Item extension") { name: String }
232233
233-
union FooBar @tag(name: "FooBar") = Foo
234-
extend union FooBar @tag(name: "FooBar extension")
234+
union FooBar @tag(name: "FooBar") = Foo
235+
extend union FooBar @tag(name: "FooBar extension")
235236
236-
enum Color @tag(name: "Color") { RED GREEN }
237-
extend enum Color @tag(name: "Color extension")
237+
enum Color @tag(name: "Color") { RED GREEN }
238+
extend enum Color @tag(name: "Color extension")
238239
239-
input Filter @tag(name: "Filter") { id: ID }
240-
extend input Filter @tag(name: "Filter extension") @once(name: "Filter extension")
240+
input Filter @tag(name: "Filter") { id: ID }
241+
extend input Filter @tag(name: "Filter extension") @once(name: "Filter extension")
241242
242-
type Foo { foo: String }
243-
""") {
244-
dictionary(Item::class, Foo::class)
245-
directive("tag", wiring)
246-
directive("once", wiring)
247-
}.makeExecutableSchema()
243+
type Foo { foo: String }
244+
""") {
245+
dictionary(Item::class, Foo::class)
246+
directive("tag", wiring)
247+
directive("once", wiring)
248+
}.makeExecutableSchema()
249+
}
250+
251+
assertEquals(warnings, listOf("Directive @once isn't repeatable but an extension of type 'Query' applies it again, only the first one is kept."))
248252

249253
val applied = listOf("Query", "Node", "Item", "FooBar", "Color", "Filter").associateWith { name ->
250254
(schema.getType(name) as GraphQLDirectiveContainer).appliedDirectives.map { it.describe() }
@@ -304,12 +308,15 @@ class TypeExtensionTest {
304308
$fooBarBaz
305309
"""
306310

307-
assertEquals(parser(unusedUnion).getUnusedDefinitions().map { it.javaClass.simpleName to it.name }.toSet(), setOf(
311+
val (unused, warnings) = withWarnings { parser(unusedUnion).getUnusedDefinitions() }
312+
313+
assertEquals(unused.map { it.javaClass.simpleName to it.name }.toSet(), setOf(
308314
"UnionTypeDefinition" to "FooBar",
309315
"ObjectTypeDefinition" to "Foo",
310316
"ObjectTypeDefinition" to "Bar",
311317
"ObjectTypeDefinition" to "Baz"
312318
))
319+
assertEquals(warnings, listOf("FooBar", "Foo", "Bar", "Baz").map { "Schema type was defined but can never be accessed, and can be safely deleted: $it" })
313320

314321
val schema = parser(unusedUnion) {
315322
dictionary(Foo::class, Bar::class, Baz::class)
@@ -321,35 +328,43 @@ class TypeExtensionTest {
321328

322329
@Test
323330
fun `repeats and extensions without a type keep building`() {
324-
val schema = parser(
325-
"""
326-
type Query {
327-
color: Color
328-
node: Node
329-
fooBar: FooBar
330-
}
331+
val (schema, warnings) = withWarnings {
332+
parser(
333+
"""
334+
type Query {
335+
color: Color
336+
node: Node
337+
fooBar: FooBar
338+
}
331339
332-
extend enum Color {
333-
"The first red"
334-
RED
335-
}
336-
extend enum Color { RED GREEN }
340+
extend enum Color {
341+
"The first red"
342+
RED
343+
}
344+
extend enum Color { RED GREEN }
337345
338-
interface Node {
339-
"The base id"
340-
id: ID!
341-
}
342-
extend interface Node { id: ID! name: String }
346+
interface Node {
347+
"The base id"
348+
id: ID!
349+
}
350+
extend interface Node { id: ID! name: String }
343351
344-
type Item implements Node { id: ID! name: String }
352+
type Item implements Node { id: ID! name: String }
345353
346-
extend union FooBar = Foo | Bar
347-
extend union FooBar = Bar | Baz
354+
extend union FooBar = Foo | Bar
355+
extend union FooBar = Bar | Baz
348356
349-
$fooBarBaz
350-
""") {
351-
dictionary(Item::class, Foo::class, Bar::class, Baz::class)
352-
}.makeExecutableSchema()
357+
$fooBarBaz
358+
""") {
359+
dictionary(Item::class, Foo::class, Bar::class, Baz::class)
360+
}.makeExecutableSchema()
361+
}
362+
363+
assertEquals(warnings, listOf(
364+
"Type 'Node' declares field 'id' more than once, only the first one is kept.",
365+
"Type 'FooBar' declares member 'Bar' more than once, only the first one is kept.",
366+
"Type 'Color' declares value 'RED' more than once, only the first one is kept."
367+
))
353368

354369
val color = schema.getType("Color") as GraphQLEnumType
355370
assertEquals(color.values.map { it.name }, listOf("RED", "GREEN"))
@@ -364,20 +379,28 @@ class TypeExtensionTest {
364379

365380
@Test
366381
fun `extensions of a different kind than the type are ignored`() {
367-
val schema = parser(
368-
"""
369-
type Query { node: Node }
382+
val (schema, warnings) = withWarnings {
383+
parser(
384+
"""
385+
type Query { node: Node }
370386
371-
interface Node { id: ID! }
372-
extend type Node { name: String }
387+
interface Node { id: ID! }
388+
extend type Node { name: String }
373389
374-
type Item implements Node { id: ID! }
375-
extend union Item = Foo
390+
type Item implements Node { id: ID! }
391+
extend union Item = Foo
376392
377-
type Foo { foo: String }
378-
""") {
379-
dictionary(Item::class)
380-
}.makeExecutableSchema()
393+
type Foo { foo: String }
394+
""") {
395+
dictionary(Item::class)
396+
}.makeExecutableSchema()
397+
}
398+
399+
assertEquals(warnings, listOf(
400+
"Type 'Node' is defined as an interface, but extended as an object type. The extension is ignored.",
401+
"Type 'Item' is defined as an object type, but extended as a union. The extension is ignored.",
402+
"Schema type was defined but can never be accessed, and can be safely deleted: Foo"
403+
))
381404

382405
val node = schema.getType("Node") as GraphQLInterfaceType
383406
val item = schema.getType("Item") as GraphQLObjectType

‎src/test/kotlin/graphql/kickstart/tools/UnmappedInputFieldsTest.kt‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package graphql.kickstart.tools
22

3-
import ch.qos.logback.classic.Level
4-
import ch.qos.logback.classic.Logger
5-
import ch.qos.logback.classic.spi.ILoggingEvent
6-
import ch.qos.logback.core.read.ListAppender
73
import com.fasterxml.jackson.annotation.JacksonInject
84
import com.fasterxml.jackson.annotation.JsonCreator
95
import com.fasterxml.jackson.annotation.JsonFormat
@@ -32,7 +28,6 @@ import graphql.kickstart.tools.UnmappedInputFieldsFixtures.*
3228
import graphql.schema.DataFetchingEnvironment
3329
import org.junit.Assert.assertThrows
3430
import org.junit.Test
35-
import org.slf4j.LoggerFactory
3631
import java.util.*
3732

3833
class UnmappedInputFieldsTest {
@@ -809,22 +804,6 @@ class UnmappedInputFieldsTest {
809804
"Input field '$field' has no matching property in ${clazz.name}. Converting $usage $consequence. Known properties: $knownProperties. " +
810805
"Rename the property or map it with @JsonProperty(\"${field.substringAfter('.')}\"). If it's left unmapped on purpose, list it in @JsonIgnoreProperties on the class."
811806

812-
private fun <T> withWarnings(build: () -> T): Pair<T, List<String>> {
813-
// the tests run in the library's module, which doesn't read logback's
814-
listOf("ch.qos.logback.classic", "ch.qos.logback.core").forEach { name ->
815-
ModuleLayer.boot().findModule(name).ifPresent { javaClass.module.addReads(it) }
816-
}
817-
val logger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger
818-
val appender = ListAppender<ILoggingEvent>().apply { start() }
819-
logger.addAppender(appender)
820-
try {
821-
val result = build()
822-
return result to appender.list.filter { it.level.isGreaterOrEqual(Level.WARN) }.map { it.formattedMessage }
823-
} finally {
824-
logger.detachAppender(appender)
825-
}
826-
}
827-
828807
class GreetingInput {
829808
var nmae: String? = null
830809
}

0 commit comments

Comments
 (0)