1 package followschema
2
3 import (
4 "context"
5 "fmt"
6 "reflect"
7 "testing"
8
9 "github.com/stretchr/testify/require"
10
11 "github.com/99designs/gqlgen/client"
12 "github.com/99designs/gqlgen/graphql"
13 "github.com/99designs/gqlgen/graphql/handler"
14 )
15
16 func TestInterfaces(t *testing.T) {
17 t.Run("slices of interfaces are not pointers", func(t *testing.T) {
18 field, ok := reflect.TypeOf((*QueryResolver)(nil)).Elem().MethodByName("Shapes")
19 require.True(t, ok)
20 require.Equal(t, "[]followschema.Shape", field.Type.Out(0).String())
21 })
22
23 t.Run("models returning interfaces", func(t *testing.T) {
24 resolvers := &Stub{}
25 resolvers.QueryResolver.Node = func(ctx context.Context) (node Node, err error) {
26 return &ConcreteNodeA{
27 ID: "1234",
28 Name: "asdf",
29 child: &ConcreteNodeA{
30 ID: "5678",
31 Name: "hjkl",
32 child: nil,
33 },
34 }, nil
35 }
36
37 srv := handler.NewDefaultServer(
38 NewExecutableSchema(Config{
39 Resolvers: resolvers,
40 }),
41 )
42
43 c := client.New(srv)
44
45 var resp struct {
46 Node struct {
47 ID string
48 Child struct {
49 ID string
50 }
51 }
52 }
53 c.MustPost(`{ node { id, child { id } } }`, &resp)
54 require.Equal(t, "1234", resp.Node.ID)
55 require.Equal(t, "5678", resp.Node.Child.ID)
56 })
57
58 t.Run("interfaces can be nil", func(t *testing.T) {
59 resolvers := &Stub{}
60 resolvers.QueryResolver.NoShape = func(ctx context.Context) (shapes Shape, e error) {
61 return nil, nil
62 }
63
64 srv := handler.NewDefaultServer(
65 NewExecutableSchema(Config{
66 Resolvers: resolvers,
67 Directives: DirectiveRoot{
68 MakeNil: func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
69 return nil, nil
70 },
71 },
72 }),
73 )
74
75 c := client.New(srv)
76
77 var resp interface{}
78 c.MustPost(`{ noShape { area } }`, &resp)
79 })
80
81 t.Run("interfaces can be typed nil", func(t *testing.T) {
82 resolvers := &Stub{}
83 resolvers.QueryResolver.NoShapeTypedNil = func(ctx context.Context) (shapes Shape, e error) {
84 panic("should not be called")
85 }
86
87 srv := handler.NewDefaultServer(
88 NewExecutableSchema(Config{
89 Resolvers: resolvers,
90 Directives: DirectiveRoot{
91 MakeTypedNil: func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
92 var circle *Circle
93 return circle, nil
94 },
95 },
96 }),
97 )
98
99 c := client.New(srv)
100
101 var resp interface{}
102 c.MustPost(`{ noShapeTypedNil { area } }`, &resp)
103 })
104
105 t.Run("interfaces can be nil (test with code-generated resolver)", func(t *testing.T) {
106 resolvers := &Stub{}
107 resolvers.QueryResolver.Animal = func(ctx context.Context) (animal Animal, e error) {
108 panic("should not be called")
109 }
110
111 srv := handler.NewDefaultServer(
112 NewExecutableSchema(Config{
113 Resolvers: resolvers,
114 Directives: DirectiveRoot{
115 MakeTypedNil: func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
116 var dog *Dog
117 return dog, nil
118 },
119 },
120 }),
121 )
122
123 c := client.New(srv)
124
125 var resp interface{}
126 c.MustPost(`{ animal { species } }`, &resp)
127 })
128
129 t.Run("can bind to interfaces even when the graphql is not", func(t *testing.T) {
130 resolvers := &Stub{}
131 resolvers.BackedByInterfaceResolver.ID = func(ctx context.Context, obj BackedByInterface) (s string, err error) {
132 return "ID:" + obj.ThisShouldBind(), nil
133 }
134 resolvers.QueryResolver.NotAnInterface = func(ctx context.Context) (byInterface BackedByInterface, err error) {
135 return &BackedByInterfaceImpl{
136 Value: "A",
137 Error: nil,
138 }, nil
139 }
140
141 c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))
142
143 var resp struct {
144 NotAnInterface struct {
145 ID string
146 ThisShouldBind string
147 ThisShouldBindWithError string
148 }
149 }
150 c.MustPost(`{ notAnInterface { id, thisShouldBind, thisShouldBindWithError } }`, &resp)
151 require.Equal(t, "ID:A", resp.NotAnInterface.ID)
152 require.Equal(t, "A", resp.NotAnInterface.ThisShouldBind)
153 require.Equal(t, "A", resp.NotAnInterface.ThisShouldBindWithError)
154 })
155
156 t.Run("can return errors from interface funcs", func(t *testing.T) {
157 resolvers := &Stub{}
158 resolvers.BackedByInterfaceResolver.ID = func(ctx context.Context, obj BackedByInterface) (s string, err error) {
159 return "ID:" + obj.ThisShouldBind(), nil
160 }
161 resolvers.QueryResolver.NotAnInterface = func(ctx context.Context) (byInterface BackedByInterface, err error) {
162 return &BackedByInterfaceImpl{
163 Value: "A",
164 Error: fmt.Errorf("boom"),
165 }, nil
166 }
167
168 c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))
169
170 var resp struct {
171 NotAnInterface struct {
172 ID string
173 ThisShouldBind string
174 ThisShouldBindWithError string
175 }
176 }
177 err := c.Post(`{ notAnInterface { id, thisShouldBind, thisShouldBindWithError } }`, &resp)
178 require.EqualError(t, err, `[{"message":"boom","path":["notAnInterface","thisShouldBindWithError"]}]`)
179 })
180
181 t.Run("interfaces can implement other interfaces", func(t *testing.T) {
182 resolvers := &Stub{}
183 resolvers.QueryResolver.Node = func(ctx context.Context) (node Node, err error) {
184 return ConcreteNodeInterfaceImplementor{}, nil
185 }
186
187 c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))
188
189 var resp struct {
190 Node struct {
191 ID string
192 Child struct {
193 ID string
194 }
195 }
196 }
197 c.MustPost(`{ node { id, child { id } } }`, &resp)
198 require.Equal(t, "CNII", resp.Node.ID)
199 require.Equal(t, "Child", resp.Node.Child.ID)
200 })
201
202 t.Run("interface implementors should return merged base fields", func(t *testing.T) {
203 resolvers := &Stub{}
204 resolvers.QueryResolver.Shapes = func(ctx context.Context) (shapes []Shape, err error) {
205 return []Shape{
206 &Rectangle{
207 Coordinates: Coordinates{
208 X: -1,
209 Y: -1,
210 },
211 },
212 &Circle{
213 Coordinates: Coordinates{
214 X: 1,
215 Y: 1,
216 },
217 },
218 }, nil
219 }
220
221 c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))
222 var resp struct {
223 Shapes []struct {
224 Coordinates struct {
225 X float64
226 Y float64
227 }
228 }
229 }
230
231 c.MustPost(`
232 {
233 shapes {
234 coordinates {
235 x
236 }
237 ... on Rectangle {
238 coordinates {
239 x
240 }
241 }
242 ... on Circle {
243 coordinates {
244 y
245 }
246 }
247 }
248 }
249 `, &resp)
250
251 require.Equal(t, 2, len(resp.Shapes))
252 require.Equal(t, float64(-1), resp.Shapes[0].Coordinates.X)
253 require.Equal(t, float64(0), resp.Shapes[0].Coordinates.Y)
254 require.Equal(t, float64(1), resp.Shapes[1].Coordinates.X)
255 require.Equal(t, float64(1), resp.Shapes[1].Coordinates.Y)
256 })
257
258 t.Run("fragment on interface must return merged fields", func(t *testing.T) {
259 resolvers := &Stub{}
260 resolvers.QueryResolver.Dog = func(ctx context.Context) (dog *Dog, err error) {
261 return &Dog{
262 Size: &Size{
263 Height: 100,
264 Weight: 35,
265 },
266 }, nil
267 }
268
269 c := client.New(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolvers})))
270 var resp struct {
271 Dog struct {
272 Size struct {
273 Height int
274 Weight int
275 }
276 }
277 }
278
279 c.MustPost(`
280 {
281 dog {
282 size {
283 height
284 }
285 ...AnimalWeight
286 }
287 }
288 fragment AnimalWeight on Animal {
289 size {
290 weight
291 }
292 }
293 `, &resp)
294
295 require.Equal(t, 100, resp.Dog.Size.Height)
296 require.Equal(t, 35, resp.Dog.Size.Weight)
297 })
298 }
299
View as plain text