1
2 package federation
3
4 import (
5 "encoding/json"
6 "strconv"
7 "strings"
8 "testing"
9
10 "github.com/stretchr/testify/require"
11
12 "github.com/99designs/gqlgen/client"
13 "github.com/99designs/gqlgen/graphql/handler"
14 "github.com/99designs/gqlgen/plugin/federation/testdata/entityresolver"
15 "github.com/99designs/gqlgen/plugin/federation/testdata/entityresolver/generated"
16 )
17
18 func TestEntityResolver(t *testing.T) {
19 c := client.New(handler.NewDefaultServer(
20 generated.NewExecutableSchema(generated.Config{
21 Resolvers: &entityresolver.Resolver{},
22 }),
23 ))
24
25 t.Run("Hello entities", func(t *testing.T) {
26 representations := []map[string]interface{}{
27 {
28 "__typename": "Hello",
29 "name": "first name - 1",
30 }, {
31 "__typename": "Hello",
32 "name": "first name - 2",
33 },
34 }
35
36 var resp struct {
37 Entities []struct {
38 Name string `json:"name"`
39 } `json:"_entities"`
40 }
41
42 err := c.Post(
43 entityQuery([]string{
44 "Hello {name}",
45 }),
46 &resp,
47 client.Var("representations", representations),
48 )
49
50 require.NoError(t, err)
51 require.Equal(t, resp.Entities[0].Name, "first name - 1")
52 require.Equal(t, resp.Entities[1].Name, "first name - 2")
53 })
54
55 t.Run("HelloWithError entities", func(t *testing.T) {
56 representations := []map[string]interface{}{
57 {
58 "__typename": "HelloWithErrors",
59 "name": "first name - 1",
60 }, {
61 "__typename": "HelloWithErrors",
62 "name": "first name - 2",
63 }, {
64 "__typename": "HelloWithErrors",
65 "name": "inject error",
66 }, {
67 "__typename": "HelloWithErrors",
68 "name": "first name - 3",
69 }, {
70 "__typename": "HelloWithErrors",
71 "name": "",
72 },
73 }
74
75 var resp struct {
76 Entities []struct {
77 Name string `json:"name"`
78 } `json:"_entities"`
79 }
80
81 err := c.Post(
82 entityQuery([]string{
83 "HelloWithErrors {name}",
84 }),
85 &resp,
86 client.Var("representations", representations),
87 )
88
89 require.Error(t, err)
90 entityErrors, err := getEntityErrors(err)
91 require.NoError(t, err)
92 require.Len(t, entityErrors, 2)
93 errMessages := []string{
94 entityErrors[0].Message,
95 entityErrors[1].Message,
96 }
97
98 require.Contains(t, errMessages, "resolving Entity \"HelloWithErrors\": error (empty key) resolving HelloWithErrorsByName")
99 require.Contains(t, errMessages, "resolving Entity \"HelloWithErrors\": error resolving HelloWithErrorsByName")
100
101 require.Len(t, resp.Entities, 5)
102 require.Equal(t, resp.Entities[0].Name, "first name - 1")
103 require.Equal(t, resp.Entities[1].Name, "first name - 2")
104 require.Equal(t, resp.Entities[2].Name, "")
105 require.Equal(t, resp.Entities[3].Name, "first name - 3")
106 require.Equal(t, resp.Entities[4].Name, "")
107 })
108
109 t.Run("World entities with nested key", func(t *testing.T) {
110 representations := []map[string]interface{}{
111 {
112 "__typename": "World",
113 "hello": map[string]interface{}{
114 "name": "world name - 1",
115 },
116 "foo": "foo 1",
117 }, {
118 "__typename": "World",
119 "hello": map[string]interface{}{
120 "name": "world name - 2",
121 },
122 "foo": "foo 2",
123 },
124 }
125
126 var resp struct {
127 Entities []struct {
128 Foo string `json:"foo"`
129 Hello struct {
130 Name string `json:"name"`
131 } `json:"hello"`
132 } `json:"_entities"`
133 }
134
135 err := c.Post(
136 entityQuery([]string{
137 "World {foo hello {name}}",
138 }),
139 &resp,
140 client.Var("representations", representations),
141 )
142
143 require.NoError(t, err)
144 require.Equal(t, resp.Entities[0].Foo, "foo 1")
145 require.Equal(t, resp.Entities[0].Hello.Name, "world name - 1")
146 require.Equal(t, resp.Entities[1].Foo, "foo 2")
147 require.Equal(t, resp.Entities[1].Hello.Name, "world name - 2")
148 })
149
150 t.Run("World entities with multiple keys", func(t *testing.T) {
151 representations := []map[string]interface{}{
152 {
153 "__typename": "WorldWithMultipleKeys",
154 "hello": map[string]interface{}{
155 "name": "world name - 1",
156 },
157 "foo": "foo 1",
158 }, {
159 "__typename": "WorldWithMultipleKeys",
160 "bar": 11,
161 },
162 }
163
164 var resp struct {
165 Entities []struct {
166 Foo string `json:"foo"`
167 Hello struct {
168 Name string `json:"name"`
169 } `json:"hello"`
170 Bar int `json:"bar"`
171 } `json:"_entities"`
172 }
173
174 err := c.Post(
175 entityQuery([]string{
176 "WorldWithMultipleKeys {foo hello {name}}",
177 "WorldWithMultipleKeys {bar}",
178 }),
179 &resp,
180 client.Var("representations", representations),
181 )
182
183 require.NoError(t, err)
184 require.Equal(t, resp.Entities[0].Foo, "foo 1")
185 require.Equal(t, resp.Entities[0].Hello.Name, "world name - 1")
186 require.Equal(t, resp.Entities[1].Bar, 11)
187 })
188
189 t.Run("Hello WorldName entities (heterogeneous)", func(t *testing.T) {
190
191
192
193
194
195 representations := []map[string]interface{}{}
196 count := 10
197
198 for i := 0; i < count; i++ {
199 if i%2 == 0 {
200 representations = append(representations, map[string]interface{}{
201 "__typename": "Hello",
202 "name": "hello - " + strconv.Itoa(i),
203 })
204 } else {
205 representations = append(representations, map[string]interface{}{
206 "__typename": "WorldName",
207 "name": "world name - " + strconv.Itoa(i),
208 })
209 }
210 }
211
212 var resp struct {
213 Entities []struct {
214 Name string `json:"name"`
215 } `json:"_entities"`
216 }
217
218 err := c.Post(
219 entityQuery([]string{
220 "Hello {name}",
221 "WorldName {name}",
222 }),
223 &resp,
224 client.Var("representations", representations),
225 )
226
227 require.NoError(t, err)
228 require.Len(t, resp.Entities, count)
229
230 for i := 0; i < count; i++ {
231 if i%2 == 0 {
232 require.Equal(t, resp.Entities[i].Name, "hello - "+strconv.Itoa(i))
233 } else {
234 require.Equal(t, resp.Entities[i].Name, "world name - "+strconv.Itoa(i))
235 }
236 }
237 })
238
239 t.Run("PlanetRequires entities with requires directive", func(t *testing.T) {
240 representations := []map[string]interface{}{
241 {
242 "__typename": "PlanetRequires",
243 "name": "earth",
244 "diameter": 12,
245 }, {
246 "__typename": "PlanetRequires",
247 "name": "mars",
248 "diameter": 10,
249 },
250 }
251
252 var resp struct {
253 Entities []struct {
254 Name string `json:"name"`
255 Diameter int `json:"diameter"`
256 } `json:"_entities"`
257 }
258
259 err := c.Post(
260 entityQuery([]string{
261 "PlanetRequires {name, diameter}",
262 }),
263 &resp,
264 client.Var("representations", representations),
265 )
266
267 require.NoError(t, err)
268 require.Equal(t, resp.Entities[0].Name, "earth")
269 require.Equal(t, resp.Entities[0].Diameter, 12)
270 require.Equal(t, resp.Entities[1].Name, "mars")
271 require.Equal(t, resp.Entities[1].Diameter, 10)
272 })
273
274 t.Run("PlanetRequires entities with multiple required fields directive", func(t *testing.T) {
275 representations := []map[string]interface{}{
276 {
277 "__typename": "PlanetMultipleRequires",
278 "name": "earth",
279 "density": 800,
280 "diameter": 12,
281 }, {
282 "__typename": "PlanetMultipleRequires",
283 "name": "mars",
284 "density": 850,
285 "diameter": 10,
286 },
287 }
288
289 var resp struct {
290 Entities []struct {
291 Name string `json:"name"`
292 Density int `json:"density"`
293 Diameter int `json:"diameter"`
294 } `json:"_entities"`
295 }
296
297 err := c.Post(
298 entityQuery([]string{
299 "PlanetMultipleRequires {name, diameter, density}",
300 }),
301 &resp,
302 client.Var("representations", representations),
303 )
304
305 require.NoError(t, err)
306 require.Equal(t, resp.Entities[0].Name, "earth")
307 require.Equal(t, resp.Entities[0].Diameter, 12)
308 require.Equal(t, resp.Entities[0].Density, 800)
309 require.Equal(t, resp.Entities[1].Name, "mars")
310 require.Equal(t, resp.Entities[1].Diameter, 10)
311 require.Equal(t, resp.Entities[1].Density, 850)
312 })
313
314 t.Run("PlanetRequiresNested entities with requires directive having nested field", func(t *testing.T) {
315 representations := []map[string]interface{}{
316 {
317 "__typename": "PlanetRequiresNested",
318 "name": "earth",
319 "world": map[string]interface{}{
320 "foo": "A",
321 },
322 }, {
323 "__typename": "PlanetRequiresNested",
324 "name": "mars",
325 "world": map[string]interface{}{
326 "foo": "B",
327 },
328 },
329 }
330
331 var resp struct {
332 Entities []struct {
333 Name string `json:"name"`
334 World struct {
335 Foo string `json:"foo"`
336 } `json:"world"`
337 } `json:"_entities"`
338 }
339
340 err := c.Post(
341 entityQuery([]string{
342 "PlanetRequiresNested {name, world { foo }}",
343 }),
344 &resp,
345 client.Var("representations", representations),
346 )
347
348 require.NoError(t, err)
349 require.Equal(t, resp.Entities[0].Name, "earth")
350 require.Equal(t, resp.Entities[0].World.Foo, "A")
351 require.Equal(t, resp.Entities[1].Name, "mars")
352 require.Equal(t, resp.Entities[1].World.Foo, "B")
353 })
354 }
355
356 func TestMultiEntityResolver(t *testing.T) {
357 c := client.New(handler.NewDefaultServer(
358 generated.NewExecutableSchema(generated.Config{
359 Resolvers: &entityresolver.Resolver{},
360 }),
361 ))
362
363 t.Run("MultiHello entities", func(t *testing.T) {
364 itemCount := 10
365 representations := []map[string]interface{}{}
366
367 for i := 0; i < itemCount; i++ {
368 representations = append(representations, map[string]interface{}{
369 "__typename": "MultiHello",
370 "name": "world name - " + strconv.Itoa(i),
371 })
372 }
373
374 var resp struct {
375 Entities []struct {
376 Name string `json:"name"`
377 } `json:"_entities"`
378 }
379
380 err := c.Post(
381 entityQuery([]string{
382 "MultiHello {name}",
383 }),
384 &resp,
385 client.Var("representations", representations),
386 )
387
388 require.NoError(t, err)
389
390 for i := 0; i < itemCount; i++ {
391 require.Equal(t, resp.Entities[i].Name, "world name - "+strconv.Itoa(i)+" - from multiget")
392 }
393 })
394
395 t.Run("MultiHello and Hello (heterogeneous) entities", func(t *testing.T) {
396 itemCount := 20
397 representations := []map[string]interface{}{}
398
399 for i := 0; i < itemCount; i++ {
400
401
402 if i%2 == 0 {
403 representations = append(representations, map[string]interface{}{
404 "__typename": "MultiHello",
405 "name": "world name - " + strconv.Itoa(i),
406 })
407 } else {
408 representations = append(representations, map[string]interface{}{
409 "__typename": "Hello",
410 "name": "hello - " + strconv.Itoa(i),
411 })
412 }
413 }
414
415 var resp struct {
416 Entities []struct {
417 Name string `json:"name"`
418 } `json:"_entities"`
419 }
420
421 err := c.Post(
422 entityQuery([]string{
423 "MultiHello {name}",
424 "Hello {name}",
425 }),
426 &resp,
427 client.Var("representations", representations),
428 )
429
430 require.NoError(t, err)
431
432 for i := 0; i < itemCount; i++ {
433 if i%2 == 0 {
434 require.Equal(t, resp.Entities[i].Name, "world name - "+strconv.Itoa(i)+" - from multiget")
435 } else {
436 require.Equal(t, resp.Entities[i].Name, "hello - "+strconv.Itoa(i))
437 }
438 }
439 })
440
441 t.Run("MultiHelloWithError entities", func(t *testing.T) {
442 itemCount := 10
443 representations := []map[string]interface{}{}
444
445 for i := 0; i < itemCount; i++ {
446 representations = append(representations, map[string]interface{}{
447 "__typename": "MultiHelloWithError",
448 "name": "world name - " + strconv.Itoa(i),
449 })
450 }
451
452 var resp struct {
453 Entities []struct {
454 Name string `json:"name"`
455 } `json:"_entities"`
456 }
457
458 err := c.Post(
459 entityQuery([]string{
460 "MultiHelloWithError {name}",
461 }),
462 &resp,
463 client.Var("representations", representations),
464 )
465
466 require.Error(t, err)
467 entityErrors, err := getEntityErrors(err)
468 require.NoError(t, err)
469 require.Len(t, entityErrors, 1)
470 require.Contains(t, entityErrors[0].Message, "error resolving MultiHelloWorldWithError")
471 })
472
473 t.Run("MultiHelloRequires entities with requires directive", func(t *testing.T) {
474 representations := []map[string]interface{}{
475 {
476 "__typename": "MultiHelloRequires",
477 "name": "first name - 1",
478 "key1": "key1 - 1",
479 }, {
480 "__typename": "MultiHelloRequires",
481 "name": "first name - 2",
482 "key1": "key1 - 2",
483 },
484 }
485
486 var resp struct {
487 Entities []struct {
488 Name string `json:"name"`
489 Key1 string `json:"key1"`
490 } `json:"_entities"`
491 }
492
493 err := c.Post(
494 entityQuery([]string{
495 "MultiHelloRequires {name, key1}",
496 }),
497 &resp,
498 client.Var("representations", representations),
499 )
500
501 require.NoError(t, err)
502 require.Equal(t, resp.Entities[0].Name, "first name - 1")
503 require.Equal(t, resp.Entities[0].Key1, "key1 - 1")
504 require.Equal(t, resp.Entities[1].Name, "first name - 2")
505 require.Equal(t, resp.Entities[1].Key1, "key1 - 2")
506 })
507
508 t.Run("MultiHelloMultipleRequires entities with multiple required fields", func(t *testing.T) {
509 representations := []map[string]interface{}{
510 {
511 "__typename": "MultiHelloMultipleRequires",
512 "name": "first name - 1",
513 "key1": "key1 - 1",
514 "key2": "key2 - 1",
515 }, {
516 "__typename": "MultiHelloMultipleRequires",
517 "name": "first name - 2",
518 "key1": "key1 - 2",
519 "key2": "key2 - 2",
520 },
521 }
522
523 var resp struct {
524 Entities []struct {
525 Name string `json:"name"`
526 Key1 string `json:"key1"`
527 Key2 string `json:"key2"`
528 } `json:"_entities"`
529 }
530
531 err := c.Post(
532 entityQuery([]string{
533 "MultiHelloMultipleRequires {name, key1, key2}",
534 }),
535 &resp,
536 client.Var("representations", representations),
537 )
538
539 require.NoError(t, err)
540 require.Equal(t, resp.Entities[0].Name, "first name - 1")
541 require.Equal(t, resp.Entities[0].Key1, "key1 - 1")
542 require.Equal(t, resp.Entities[0].Key2, "key2 - 1")
543 require.Equal(t, resp.Entities[1].Name, "first name - 2")
544 require.Equal(t, resp.Entities[1].Key1, "key1 - 2")
545 require.Equal(t, resp.Entities[1].Key2, "key2 - 2")
546 })
547
548 t.Run("MultiPlanetRequiresNested entities with requires directive having nested field", func(t *testing.T) {
549 representations := []map[string]interface{}{
550 {
551 "__typename": "MultiPlanetRequiresNested",
552 "name": "earth",
553 "world": map[string]interface{}{
554 "foo": "A",
555 },
556 }, {
557 "__typename": "MultiPlanetRequiresNested",
558 "name": "mars",
559 "world": map[string]interface{}{
560 "foo": "B",
561 },
562 },
563 }
564
565 var resp struct {
566 Entities []struct {
567 Name string `json:"name"`
568 World struct {
569 Foo string `json:"foo"`
570 } `json:"world"`
571 } `json:"_entities"`
572 }
573
574 err := c.Post(
575 entityQuery([]string{
576 "MultiPlanetRequiresNested {name, world { foo }}",
577 }),
578 &resp,
579 client.Var("representations", representations),
580 )
581
582 require.NoError(t, err)
583 require.Equal(t, resp.Entities[0].Name, "earth")
584 require.Equal(t, resp.Entities[0].World.Foo, "A")
585 require.Equal(t, resp.Entities[1].Name, "mars")
586 require.Equal(t, resp.Entities[1].World.Foo, "B")
587 })
588 }
589
590 func entityQuery(queries []string) string {
591
592
593 entityQueries := make([]string, len(queries))
594 for i, query := range queries {
595 entityQueries[i] = " ... on " + query
596 }
597
598 return "query($representations:[_Any!]!){_entities(representations:$representations){" + strings.Join(entityQueries, "") + "}}"
599 }
600
601 type entityResolverError struct {
602 Message string `json:"message"`
603 Path []string `json:"path"`
604 }
605
606 func getEntityErrors(err error) ([]*entityResolverError, error) {
607 var errors []*entityResolverError
608 err = json.Unmarshal([]byte(err.Error()), &errors)
609 return errors, err
610 }
611
View as plain text