Repository: graph-gophers/graphql-go (go)

Issue:
Interface inline fragment on union types results in panic.
## Error
Given:
```
interface Node {
  id: ID!
}

type A implements Node {
  id: ID!
  a: String!
}

type B implements Node {
  id: ID!
  b: String!
}

type C implements Node {
  id: ID!
  c: String!
}

union ABC = A | B | C

query {
  search(input: InputType!): ABC
}
```
If I make the following query to this mock api:
```
{
  search(input: { ... }) {
    ... on A {
      a
    }
    ... on B {
      b
    }
    ... on C {
      c
    }
    ... on Node {
      id
    }
  }
}
```
I get a panic error that Node does not implement ABC, originating from the following line of code: https://github.com/graph-gophers/graphql-go/blob/bb9738501bd42a6536227b96068349b814379d6e/internal/exec/selected/selected.go#L170

## Expected
I expect the Node inline fragment to resolve just fine, because A) relay makes queries of this nature via its api, and B) each type in the union implements the Node interface.

## Probable source of the error
Looking through the code I can see - correct me if I'm wrong - that the type assertion is only checking for possible types for the union. It is not checking if A) the inline fragment is an interface type, or if B) the union objects implement said interface.

### Update
I've decided to just make all my union types into interface types for the time being.

Pull request description:
fix #241
Similar to https://github.com/graph-gophers/graphql-go/pull/407, but adds test cases.

fix #241

Patch:
diff --git a/example/social/introspect.json b/example/social/introspect.json
index 88c4c00..330564e 100644
--- a/example/social/introspect.json
+++ b/example/social/introspect.json
@@ -214,6 +214,39 @@
         "name": "Pagination",
         "possibleTypes": null
       },
+      {
+        "description": null,
+        "enumValues": null,
+        "fields": [
+          {
+            "args": [],
+            "deprecationReason": null,
+            "description": null,
+            "isDeprecated": false,
+            "name": "name",
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "String",
+                "ofType": null
+              }
+            }
+          }
+        ],
+        "inputFields": null,
+        "interfaces": null,
+        "kind": "INTERFACE",
+        "name": "Person",
+        "possibleTypes": [
+          {
+            "kind": "OBJECT",
+            "name": "User",
+            "ofType": null
+          }
+        ]
+      },
       {
         "description": null,
         "enumValues": null,
@@ -545,6 +578,11 @@
             "kind": "INTERFACE",
             "name": "Admin",
             "ofType": null
+          },
+          {
+            "kind": "INTERFACE",
+            "name": "Person",
+            "ofType": null
           }
         ],
         "kind": "OBJECT",
diff --git a/example/social/social.go b/example/social/social.go
index 781b234..3de8efc 100644
--- a/example/social/social.go
+++ b/example/social/social.go
@@ -27,9 +27,13 @@ const Schema = `
 		role: Role!
 	}
 
+	interface Person {
+		name: String!
+	}
+
 	scalar Time	
 
-	type User implements Admin {
+	type User implements Admin & Person {
 		id: ID!
 		name: String!
 		email: String!
@@ -64,6 +68,15 @@ type admin interface {
 	Role() string
 }
 
+type adminResolver struct {
+	admin
+}
+
+func (r *adminResolver) ToUser() (*user, bool) {
+	n, ok := r.admin.(user)
+	return &n, ok
+}
+
 type searchResult struct {
 	result interface{}
 }
@@ -189,14 +202,14 @@ type Resolver struct{}
 func (r *Resolver) Admin(ctx context.Context, args struct {
 	ID   string
 	Role string
-}) (admin, error) {
+}) (*adminResolver, error) {
 	if usr, ok := usersMap[args.ID]; ok {
 		if usr.RoleField == args.Role {
-			return *usr, nil
+			return &adminResolver{*usr}, nil
 		}
 	}
 	err := fmt.Errorf("user with id=%s and role=%s does not exist", args.ID, args.Role)
-	return user{}, err
+	return nil, err
 }
 
 func (r *Resolver) User(ctx context.Context, args struct{ Id string }) (user, error) {
diff --git a/internal/exec/selected/selected.go b/internal/exec/selected/selected.go
index 3075521..1c60125 100644
--- a/internal/exec/selected/selected.go
+++ b/internal/exec/selected/selected.go
@@ -173,16 +173,39 @@ func applySelectionSet(r *Request, s *resolvable.Schema, e *resolvable.Object, s
 }
 
 func applyFragment(r *Request, s *resolvable.Schema, e *resolvable.Object, frag *query.Fragment) []Selection {
-	if frag.On.Name != "" && frag.On.Name != e.Name {
-		a, ok := e.TypeAssertions[frag.On.Name]
-		if !ok {
-			panic(fmt.Errorf("%q does not implement %q", frag.On, e.Name)) // TODO proper error handling
+	if frag.On.Name != e.Name {
+		t := r.Schema.Resolve(frag.On.Name)
+		face, ok := t.(*schema.Interface)
+		if !ok && frag.On.Name != "" {
+			a, ok := e.TypeAssertions[frag.On.Name]
+			if !ok {
+				panic(fmt.Errorf("%q does not implement %q", frag.On, e.Name)) // TODO proper error handling
+			}
+
+			return []Selection{&TypeAssertion{
+				TypeAssertion: *a,
+				Sels:          applySelectionSet(r, s, a.TypeExec.(*resolvable.Object), frag.Selections),
+			}}
 		}
+		if ok && len(face.PossibleTypes) > 0 {
+			sels := []Selection{}
+			for _, t := range face.PossibleTypes {
+				if t.Name == e.Name {
+					return applySelectionSet(r, s, e, frag.Selections)
+				}
 
-		return []Selection{&TypeAssertion{
-			TypeAssertion: *a,
-			Sels:          applySelectionSet(r, s, a.TypeExec.(*resolvable.Object), frag.Selections),
-		}}
+				if a, ok := e.TypeAssertions[t.Name]; ok {
+					sels = append(sels, &TypeAssertion{
+						TypeAssertion: *a,
+						Sels:          applySelectionSet(r, s, a.TypeExec.(*resolvable.Object), frag.Selections),
+					})
+				}
+			}
+			if len(sels) == 0 {
+				panic(fmt.Errorf("%q does not implement %q", e.Name, frag.On)) // TODO proper error handling
+			}
+			return sels
+		}
 	}
 	return applySelectionSet(r, s, e, frag.Selections)
 }


Test patch:
diff --git a/graphql_test.go b/graphql_test.go
index a6a318d..b0a78f5 100644
--- a/graphql_test.go
+++ b/graphql_test.go
@@ -1703,6 +1703,77 @@ func TestInlineFragments(t *testing.T) {
 				}
 			`,
 		},
+
+		{
+			Schema: starwarsSchema,
+			Query: `
+				query CharacterSearch {
+					search(text: "C-3PO") {
+						... on Character {
+							name
+						}
+					}
+				}
+			`,
+			ExpectedResult: `
+				{
+					"search": [
+						{
+							"name": "C-3PO"
+						}
+					]
+				}
+			`,
+		},
+
+		{
+			Schema: starwarsSchema,
+			Query: `
+				query CharacterSearch {
+					hero {
+						... on Character {
+							... on Human {
+								name
+							}
+							... on Droid {
+								name
+							}
+						}
+					}
+				}
+			`,
+			ExpectedResult: `
+				{
+					"hero": {
+						"name": "R2-D2"
+					}
+				}
+			`,
+		},
+
+		{
+			Schema: socialSchema,
+			Query: `
+				query {
+					admin(id: "0x01") {
+						... on User {
+							email
+						}
+						... on Person {
+							name
+						}
+					}
+				}
+			`,
+			ExpectedResult: `
+				{
+					"admin": {
+						"email": "Albus@hogwarts.com",
+						"name": "Albus Dumbledore"
+					}
+				}
+			`,
+		},
 	})
 }
 
@@ -2946,7 +3017,7 @@ type helloInputMismatch struct {
 	World string
 }
 
-func (r *inputArgumentsHello) Hello(args struct { Input *helloInput }) string {
+func (r *inputArgumentsHello) Hello(args struct{ Input *helloInput }) string {
 	return "Hello " + args.Input.Name + "!"
 }
 
@@ -2954,7 +3025,7 @@ func (r *inputArgumentsScalarMismatch1) Hello(name string) string {
 	return "Hello " + name + "!"
 }
 
-func (r *inputArgumentsScalarMismatch2) Hello(args struct { World string }) string {
+func (r *inputArgumentsScalarMismatch2) Hello(args struct{ World string }) string {
 	return "Hello " + args.World + "!"
 }
 
@@ -2962,11 +3033,11 @@ func (r *inputArgumentsObjectMismatch1) Hello(in helloInput) string {
 	return "Hello " + in.Name + "!"
 }
 
-func (r *inputArgumentsObjectMismatch2) Hello(args struct { Input *helloInputMismatch }) string {
+func (r *inputArgumentsObjectMismatch2) Hello(args struct{ Input *helloInputMismatch }) string {
 	return "Hello " + args.Input.World + "!"
 }
 
-func (r *inputArgumentsObjectMismatch3) Hello(args struct { Input *struct { Thing string } }) string {
+func (r *inputArgumentsObjectMismatch3) Hello(args struct{ Input *struct{ Thing string } }) string {
 	return "Hello " + args.Input.Thing + "!"
 }
 
diff --git a/introspection_test.go b/introspection_test.go
index 63058a4..4b05e5e 100644
--- a/introspection_test.go
+++ b/introspection_test.go
@@ -11,6 +11,8 @@ import (
 	"github.com/graph-gophers/graphql-go/example/starwars"
 )
 
+var socialSchema = graphql.MustParseSchema(social.Schema, &social.Resolver{}, graphql.UseFieldResolvers())
+
 func TestSchema_ToJSON(t *testing.T) {
 	t.Parallel()
 
@@ -27,7 +29,7 @@ func TestSchema_ToJSON(t *testing.T) {
 	}{
 		{
 			Name: "Social Schema",
-			Args: args{Schema: graphql.MustParseSchema(social.Schema, &social.Resolver{}, graphql.UseFieldResolvers())},
+			Args: args{Schema: socialSchema},
 			Want: want{JSON: mustReadFile("example/social/introspect.json")},
 		},
 		{