Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,54 @@ class SchemaClassScannerInterfaceFieldTest {
assert(schema.getType("Entry") is GraphQLInterfaceType)
}

@Test
fun `scanner handles interfaces used only by fields of interface extensions`() {
val schema = SchemaParser.newParser()
.schemaString(
"""
type Query {
container: Container
}

interface Container {
id: ID
}

extend interface Container {
entry: Entry
}

interface Entry {
id: ID!
}

interface Draft implements Entry {
id: ID!
}

type Box implements Container {
id: ID
entry: Draft
}
""")
.resolvers(InterfaceOnlyInterface())
.dictionary(InterfaceOnlyInterface.Box::class)
.build()
.makeExecutableSchema()
.assertEveryFieldResolves()

assertEquals((schema.getType("Container") as GraphQLInterfaceType).fieldDefinitions.map { it.name }, listOf("id", "entry"))
assert(schema.getType("Entry") is GraphQLInterfaceType)
}

class InterfaceOnlyInterface : GraphQLQueryResolver {
fun container(): Container? = null

interface Container

interface Draft

class Box(val entry: Draft?) : Container
class Box(val entry: Draft?, val id: String? = null) : Container
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,28 @@ class SchemaParserDictionaryTest {
assertEquals(error.message, "Class in supplied dictionary '${ThirdItem::class.java.name}' specified type names 'A', 'B', " +
"but only input object types can share a class!")
}

@Test
fun `dictionary names need a type definition, which an object or input extension alone isn't`() {
fun parser(extension: String) = SchemaParser.newParser()
.schemaString("type Query { id: ID }\n$extension")
.resolvers(object : GraphQLQueryResolver {
fun id(): String = "1"
})
.dictionary("Item", ThirdItem::class)
.build()

listOf("extend type Item { id: Int }", "extend input Item { id: Int }").forEach { extension ->
val error = assertThrows(SchemaClassScannerError::class.java) { parser(extension) }

assertEquals(error.message, "Class in supplied dictionary '${ThirdItem::class.java.name}' specified type name 'Item', " +
"but a type definition with that name was not found!")
}

val (_, warnings) = withWarnings { parser("extend union Item = Query") }
assertEquals(warnings, listOf(
"Dictionary mapping was provided but never used, and can be safely deleted: \"Item\" -> ${ThirdItem::class.java.name}",
"Schema type was defined but can never be accessed, and can be safely deleted: Item"
))
}
}
79 changes: 79 additions & 0 deletions src/test/kotlin/graphql/kickstart/tools/SchemaParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package graphql.kickstart.tools
import graphql.ExecutionResult
import graphql.GraphQL
import graphql.kickstart.tools.resolver.FieldResolverError
import graphql.language.ObjectTypeDefinition
import graphql.parser.InvalidSyntaxException
import graphql.schema.*
import graphql.schema.idl.SchemaDirectiveWiring
Expand Down Expand Up @@ -316,6 +317,84 @@ class SchemaParserTest {
assertEquals(error.message, "Union 'Result' contains itself through Result -> Result at line 5, column 23.")
}

@Test
fun `parser should throw descriptive exception when a union extension lists a union that contains it`() {
val error = assertThrows(SchemaError::class.java) {
unionParser(
"""
|union Result = Book | Media
|union Media = Movie
|extend union Media = Result
""".trimMargin())
}

assertEquals(error.message, "Union 'Result' contains itself through Result -> Media -> Result at line 7, column 22.")
}

@Test
fun `parser lists a member once when nested unions reach it more than once`() {
val schema = unionParser(
"""
|union Result = Left | Right
|union Left = Book
|extend union Left = Shared
|union Right = Shared
|union Shared = Movie
""".trimMargin())

assertEquals(listOf("Result", "Left", "Right").associateWith { name -> (schema.getType(name) as GraphQLUnionType).types.map { it.name } }, mapOf(
"Result" to listOf("Book", "Movie"),
"Left" to listOf("Book", "Movie"),
"Right" to listOf("Movie")
))
}

@Test
fun `parser builds a type defined twice from its last definition, or from its first when only its interface reaches it`() {
val (throughField, fieldWarnings) = withWarnings {
SchemaParser.newParser()
.schemaString(
"""
type Query { item: Item }
type Item { id: ID }
type Item { id: ID name: String }
""")
.resolvers(TwiceDefinedQuery())
.build()
}

assertEquals(fieldWarnings, listOf())
assertEquals(throughField.getUnusedDefinitions(), setOf())
assertEquals((throughField.makeExecutableSchema().getType("Item") as GraphQLObjectType).fieldDefinitions.map { it.name }, listOf("id", "name"))

val (throughInterface, interfaceWarnings) = withWarnings {
SchemaParser.newParser()
.schemaString(
"""
type Query { node: Node }
interface Node { id: ID }
type Item implements Node { id: ID }
type Item implements Node { id: ID name: String }
""")
.resolvers(TwiceDefinedQuery())
.dictionary("Item", TwiceDefined::class)
.build()
}

assertEquals(interfaceWarnings, listOf("Schema type was defined but can never be accessed, and can be safely deleted: Item"))
assertEquals(throughInterface.getUnusedDefinitions().map { it.name to (it as ObjectTypeDefinition).fieldDefinitions.map { field -> field.name } }, listOf("Item" to listOf("id", "name")))
assertEquals((throughInterface.makeExecutableSchema().getType("Item") as GraphQLObjectType).fieldDefinitions.map { it.name }, listOf("id"))
}

class TwiceDefinedQuery : GraphQLQueryResolver {
fun item(): TwiceDefined = TwiceDefined("1", "one")
fun node(): TwiceDefinedNode = TwiceDefined("1", "one")
}

interface TwiceDefinedNode

class TwiceDefined(val id: String, val name: String?) : TwiceDefinedNode

private fun unionParser(unions: String) = SchemaParser.newParser()
.schemaString(
"""
Expand Down
Loading
Loading