github.com/99designs/gqlgen@v0.17.45/plugin/federation/federation_entityresolver_test.go (about) 1 //go:generate go run ../../testdata/gqlgen.go -config testdata/entityresolver/gqlgen.yml 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 // Entity resolution can handle heterogenenous representations. Meaning, 191 // the representations for resolving entities can be of different 192 // __typename. So the tests here will interleve two different entity 193 // types so that we can test support for resolving different types and 194 // correctly handle ordering. 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 // Let's interleve the representations to test ordering of the 401 // responses from the entity query 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 // What we want! 592 // query($representations:[_Any!]!){_entities(representations:$representations){ ...on Hello{secondary} }} 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 }