github.com/niko0xdev/gqlgen@v0.17.55-0.20240120102243-2ecff98c3e37/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/niko0xdev/gqlgen/client"
    13  	"github.com/niko0xdev/gqlgen/graphql/handler"
    14  	"github.com/niko0xdev/gqlgen/plugin/federation/testdata/entityresolver"
    15  	"github.com/niko0xdev/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  }