github.com/dgraph-io/dgraph@v1.2.8/graphql/resolve/resolver_error_test.go (about)

     1  /*
     2   * Copyright 2019 Dgraph Labs, Inc. and Contributors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package resolve
    18  
    19  import (
    20  	"context"
    21  	"io/ioutil"
    22  	"testing"
    23  
    24  	dgoapi "github.com/dgraph-io/dgo/v2/protos/api"
    25  	"github.com/dgraph-io/dgraph/gql"
    26  	"github.com/dgraph-io/dgraph/graphql/schema"
    27  	"github.com/dgraph-io/dgraph/graphql/test"
    28  	"github.com/dgraph-io/dgraph/x"
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/pkg/errors"
    31  	"github.com/stretchr/testify/require"
    32  	"gopkg.in/yaml.v2"
    33  )
    34  
    35  // Tests that result completion and GraphQL error propagation are working properly.
    36  
    37  // All the tests work on a mocked json response, rather than a running Dgraph.
    38  // It's better to mock the Dgraph client interface in these tests and have cases
    39  // where one can directly see the json response and how it gets modified, than
    40  // to try and orchestrate conditions for all these complicated tests in a live
    41  // Dgraph instance.  Done on a real Dgraph, you also can't see the responses
    42  // to see what the test is actually doing.
    43  
    44  type executor struct {
    45  	resp     string
    46  	assigned map[string]string
    47  	result   map[string]interface{}
    48  
    49  	// start reporting Dgraph fails at this point (0 = never fail, 1 = fail on
    50  	// first request, 2 = succeed once and then fail on 2nd request, etc.)
    51  	failQuery    int
    52  	failMutation int
    53  }
    54  
    55  type QueryCase struct {
    56  	Name        string
    57  	GQLQuery    string
    58  	Explanation string
    59  	Response    string // Dgraph json response
    60  	Expected    string // Expected data from Resolve()
    61  	Errors      x.GqlErrorList
    62  }
    63  
    64  var testGQLSchema = `
    65  type Author {
    66  	id: ID!
    67  	name: String!
    68  	dob: DateTime
    69  	postsRequired: [Post!]!
    70  	postsElmntRequired: [Post!]
    71  	postsNullable: [Post]
    72  	postsNullableListRequired: [Post]!
    73  }
    74  
    75  type Post {
    76  	id: ID!
    77  	title: String!
    78  	text: String
    79  	author: Author!
    80  }`
    81  
    82  func (ex *executor) Query(ctx context.Context, query *gql.GraphQuery) ([]byte, error) {
    83  	ex.failQuery--
    84  	if ex.failQuery == 0 {
    85  		return nil, schema.GQLWrapf(errors.New("_bad stuff happend_"), "Dgraph query failed")
    86  	}
    87  	return []byte(ex.resp), nil
    88  }
    89  
    90  func (ex *executor) Mutate(ctx context.Context,
    91  	query *gql.GraphQuery,
    92  	mutations []*dgoapi.Mutation) (map[string]string, map[string]interface{}, error) {
    93  	ex.failMutation--
    94  	if ex.failMutation == 0 {
    95  		return nil, nil, schema.GQLWrapf(errors.New("_bad stuff happend_"), "Dgraph mutation failed")
    96  	}
    97  	return ex.assigned, ex.result, nil
    98  }
    99  
   100  // Tests in resolver_test.yaml are about what gets into a completed result (addition
   101  // of "null", errors and error propagation).  Exact JSON result (e.g. order) doesn't
   102  // matter here - that makes for easier to format and read tests for these many cases.
   103  //
   104  // The []bytes built by Resolve() have some other properties, such as ordering of
   105  // fields, which are tested by TestResponseOrder().
   106  func TestResolver(t *testing.T) {
   107  	b, err := ioutil.ReadFile("resolver_error_test.yaml")
   108  	require.NoError(t, err, "Unable to read test file")
   109  
   110  	var tests []QueryCase
   111  	err = yaml.Unmarshal(b, &tests)
   112  	require.NoError(t, err, "Unable to unmarshal tests to yaml.")
   113  
   114  	gqlSchema := test.LoadSchemaFromString(t, testGQLSchema)
   115  
   116  	for _, tcase := range tests {
   117  		t.Run(tcase.Name, func(t *testing.T) {
   118  			resp := resolve(gqlSchema, tcase.GQLQuery, tcase.Response)
   119  
   120  			if diff := cmp.Diff(tcase.Errors, resp.Errors); diff != "" {
   121  				t.Errorf("errors mismatch (-want +got):\n%s", diff)
   122  			}
   123  
   124  			require.JSONEq(t, tcase.Expected, resp.Data.String(), tcase.Explanation)
   125  		})
   126  	}
   127  }
   128  
   129  // Ordering of results and inserted null values matters in GraphQL:
   130  // https://graphql.github.io/graphql-spec/June2018/#sec-Serialized-Map-Ordering
   131  func TestResponseOrder(t *testing.T) {
   132  	query := `query {
   133  		 getAuthor(id: "0x1") {
   134  			 name
   135  			 dob
   136  			 postsNullable {
   137  				 title
   138  				 text
   139  			 }
   140  		 }
   141  	 }`
   142  
   143  	tests := []QueryCase{
   144  		{Name: "Response is in same order as GQL query",
   145  			GQLQuery: query,
   146  			Response: `{ "getAuthor": [ { "name": "A.N. Author", "dob": "2000-01-01", ` +
   147  				`"postsNullable": [ ` +
   148  				`{ "title": "A Title", "text": "Some Text" }, ` +
   149  				`{ "title": "Another Title", "text": "More Text" } ] } ] }`,
   150  			Expected: `{"getAuthor": {"name": "A.N. Author", "dob": "2000-01-01", ` +
   151  				`"postsNullable": [` +
   152  				`{"title": "A Title", "text": "Some Text"}, ` +
   153  				`{"title": "Another Title", "text": "More Text"}]}}`},
   154  		{Name: "Response is in same order as GQL query no matter Dgraph order",
   155  			GQLQuery: query,
   156  			Response: `{ "getAuthor": [ { "dob": "2000-01-01", "name": "A.N. Author", ` +
   157  				`"postsNullable": [ ` +
   158  				`{ "text": "Some Text", "title": "A Title" }, ` +
   159  				`{ "title": "Another Title", "text": "More Text" } ] } ] }`,
   160  			Expected: `{"getAuthor": {"name": "A.N. Author", "dob": "2000-01-01", ` +
   161  				`"postsNullable": [` +
   162  				`{"title": "A Title", "text": "Some Text"}, ` +
   163  				`{"title": "Another Title", "text": "More Text"}]}}`},
   164  		{Name: "Inserted null is in GQL query order",
   165  			GQLQuery: query,
   166  			Response: `{ "getAuthor": [ { "name": "A.N. Author", ` +
   167  				`"postsNullable": [ ` +
   168  				`{ "title": "A Title" }, ` +
   169  				`{ "title": "Another Title", "text": "More Text" } ] } ] }`,
   170  			Expected: `{"getAuthor": {"name": "A.N. Author", "dob": null, ` +
   171  				`"postsNullable": [` +
   172  				`{"title": "A Title", "text": null}, ` +
   173  				`{"title": "Another Title", "text": "More Text"}]}}`},
   174  	}
   175  
   176  	gqlSchema := test.LoadSchemaFromString(t, testGQLSchema)
   177  
   178  	for _, test := range tests {
   179  		t.Run(test.Name, func(t *testing.T) {
   180  			resp := resolve(gqlSchema, test.GQLQuery, test.Response)
   181  
   182  			require.Nil(t, resp.Errors)
   183  			require.Equal(t, test.Expected, resp.Data.String())
   184  		})
   185  	}
   186  }
   187  
   188  // For add and update mutations, we don't need to re-test all the cases from the
   189  // query tests.  So just test enough to demonstrate that we'll catch it if we were
   190  // to delete the call to completeDgraphResult before adding to the response.
   191  func TestAddMutationUsesErrorPropagation(t *testing.T) {
   192  	mutation := `mutation {
   193  		addPost(input: [{title: "A Post", text: "Some text", author: {id: "0x1"}}]) {
   194  			post {
   195  				title
   196  				text
   197  				author {
   198  					name
   199  					dob
   200  				}
   201  			}
   202  		}
   203  	}`
   204  
   205  	tests := map[string]struct {
   206  		explanation   string
   207  		mutResponse   map[string]string
   208  		mutQryResp    map[string]interface{}
   209  		queryResponse string
   210  		expected      string
   211  		errors        x.GqlErrorList
   212  	}{
   213  		"Add mutation adds missing nullable fields": {
   214  			explanation: "Field 'dob' is nullable, so null should be inserted " +
   215  				"if the mutation's query doesn't return a value.",
   216  			mutResponse: map[string]string{"Post1": "0x2"},
   217  			mutQryResp: map[string]interface{}{
   218  				"Author2": []interface{}{map[string]string{"uid": "0x1"}}},
   219  			queryResponse: `{ "post" : [
   220  				{ "title": "A Post",
   221  				"text": "Some text",
   222  				"author": { "name": "A.N. Author" } } ] }`,
   223  			expected: `{ "addPost": { "post" :
   224  				[{ "title": "A Post",
   225  				"text": "Some text",
   226  				"author": { "name": "A.N. Author", "dob": null } }] } }`,
   227  		},
   228  		"Add mutation triggers GraphQL error propagation": {
   229  			explanation: "An Author's name is non-nullable, so if that's missing, " +
   230  				"the author is squashed to null, but that's also non-nullable, so the " +
   231  				"propagates to the query root.",
   232  			mutResponse: map[string]string{"Post1": "0x2"},
   233  			mutQryResp: map[string]interface{}{
   234  				"Author2": []interface{}{map[string]string{"uid": "0x1"}}},
   235  			queryResponse: `{ "post" : [
   236  				{ "title": "A Post",
   237  				"text": "Some text",
   238  				"author": { "dob": "2000-01-01" } } ] }`,
   239  			expected: `{ "addPost": { "post" : [null] } }`,
   240  			errors: x.GqlErrorList{&x.GqlError{
   241  				Message: `Non-nullable field 'name' (type String!) ` +
   242  					`was not present in result from Dgraph.  GraphQL error propagation triggered.`,
   243  				Locations: []x.Location{{Column: 6, Line: 7}},
   244  				Path:      []interface{}{"addPost", "post", 0, "author", "name"}}},
   245  		},
   246  	}
   247  
   248  	gqlSchema := test.LoadSchemaFromString(t, testGQLSchema)
   249  
   250  	for name, tcase := range tests {
   251  		t.Run(name, func(t *testing.T) {
   252  			resp := resolveWithClient(gqlSchema, mutation, nil,
   253  				&executor{
   254  					resp:     tcase.queryResponse,
   255  					assigned: tcase.mutResponse,
   256  					result:   tcase.mutQryResp,
   257  				})
   258  
   259  			test.RequireJSONEq(t, tcase.errors, resp.Errors)
   260  			require.JSONEq(t, tcase.expected, resp.Data.String(), tcase.explanation)
   261  		})
   262  	}
   263  }
   264  
   265  func TestUpdateMutationUsesErrorPropagation(t *testing.T) {
   266  	mutation := `mutation {
   267  		updatePost(input: { filter: { id: ["0x1"] }, set: { text: "Some more text" } }) {
   268  			post {
   269  				title
   270  				text
   271  				author {
   272  					name
   273  					dob
   274  				}
   275  			}
   276  		}
   277  	}`
   278  
   279  	// There's no need to have mocks for the mutation part here because with nil results all the
   280  	// rewriting and rewriting from results will silently succeed.  All we care about the is the
   281  	// result from the query that follows the mutation.  In that add case we have to satisfy
   282  	// the type checking, but that's not required here.
   283  
   284  	tests := map[string]struct {
   285  		explanation   string
   286  		mutResponse   map[string]string
   287  		queryResponse string
   288  		expected      string
   289  		errors        x.GqlErrorList
   290  	}{
   291  		"Update Mutation adds missing nullable fields": {
   292  			explanation: "Field 'dob' is nullable, so null should be inserted " +
   293  				"if the mutation's query doesn't return a value.",
   294  			queryResponse: `{ "post" : [
   295  				{ "title": "A Post",
   296  				"text": "Some text",
   297  				"author": { "name": "A.N. Author" } } ] }`,
   298  			expected: `{ "updatePost": { "post" :
   299  				[{ "title": "A Post",
   300  				"text": "Some text",
   301  				"author": { "name": "A.N. Author", "dob": null } }] } }`,
   302  		},
   303  		"Update Mutation triggers GraphQL error propagation": {
   304  			explanation: "An Author's name is non-nullable, so if that's missing, " +
   305  				"the author is squashed to null, but that's also non-nullable, so the error " +
   306  				"propagates to the query root.",
   307  			queryResponse: `{ "post" : [ {
   308  				"title": "A Post",
   309  				"text": "Some text",
   310  				"author": { "dob": "2000-01-01" } } ] }`,
   311  			expected: `{ "updatePost": { "post" : [null] } }`,
   312  			errors: x.GqlErrorList{&x.GqlError{
   313  				Message: `Non-nullable field 'name' (type String!) ` +
   314  					`was not present in result from Dgraph.  GraphQL error propagation triggered.`,
   315  				Locations: []x.Location{{Column: 6, Line: 7}},
   316  				Path:      []interface{}{"updatePost", "post", 0, "author", "name"}}},
   317  		},
   318  	}
   319  
   320  	gqlSchema := test.LoadSchemaFromString(t, testGQLSchema)
   321  
   322  	for name, tcase := range tests {
   323  		t.Run(name, func(t *testing.T) {
   324  			resp := resolveWithClient(gqlSchema, mutation, nil,
   325  				&executor{resp: tcase.queryResponse, assigned: tcase.mutResponse})
   326  
   327  			test.RequireJSONEq(t, tcase.errors, resp.Errors)
   328  			require.JSONEq(t, tcase.expected, resp.Data.String(), tcase.explanation)
   329  		})
   330  	}
   331  }
   332  
   333  // TestManyMutationsWithError : Multiple mutations run serially (queries would
   334  // run in parallel) and, in GraphQL, if an error is encountered in a request with
   335  // multiple mutations, the mutations following the error are not run.  The mutations
   336  // that have succeeded are permanent - i.e. not rolled back.
   337  //
   338  // There's no real way to test this E2E against a live instance because the only
   339  // real fails during a mutation are either failure to communicate with Dgraph, or
   340  // a bug that causes a query rewriting that Dgraph rejects.  There are some other
   341  // cases: e.g. a delete that doesn't end up deleting anything (but we interpret
   342  // that as not an error, it just deleted 0 things), and a mutation with some error
   343  // in the input data/query (but that gets caught by validation before any mutations
   344  // are executed).
   345  //
   346  // So this mocks a failing mutation and tests that we behave correctly in the case
   347  // of multiple mutations.
   348  func TestManyMutationsWithError(t *testing.T) {
   349  
   350  	// add1 - should succeed
   351  	// add2 - should fail
   352  	// add3 - is never executed
   353  	multiMutation := `mutation multipleMutations($id: ID!) {
   354  			add1: addPost(input: [{title: "A Post", text: "Some text", author: {id: "0x1"}}]) {
   355  				post { title }
   356  			}
   357  
   358  			add2: addPost(input: [{title: "A Post", text: "Some text", author: {id: $id}}]) {
   359  				post { title }
   360  			}
   361  
   362  			add3: addPost(input: [{title: "A Post", text: "Some text", author: {id: "0x1"}}]) {
   363  				post { title }
   364  			}
   365  		}`
   366  
   367  	tests := map[string]struct {
   368  		explanation   string
   369  		idValue       string
   370  		mutResponse   map[string]string
   371  		mutQryResp    map[string]interface{}
   372  		queryResponse string
   373  		expected      string
   374  		errors        x.GqlErrorList
   375  	}{
   376  		"Dgraph fail": {
   377  			explanation: "a Dgraph, network or error in rewritten query failed the mutation",
   378  			idValue:     "0x1",
   379  			mutResponse: map[string]string{"Post1": "0x2"},
   380  			mutQryResp: map[string]interface{}{
   381  				"Author2": []interface{}{map[string]string{"uid": "0x1"}}},
   382  			queryResponse: `{ "post" : [{ "title": "A Post" } ] }`,
   383  			expected: `{
   384  				"add1": { "post": [{ "title": "A Post" }] },
   385  				"add2" : null
   386  			}`,
   387  			errors: x.GqlErrorList{
   388  				&x.GqlError{Message: `mutation addPost failed because ` +
   389  					`Dgraph mutation failed because _bad stuff happend_`,
   390  					Locations: []x.Location{{Line: 6, Column: 4}}},
   391  				&x.GqlError{Message: `Mutation add3 was not executed because of ` +
   392  					`a previous error.`,
   393  					Locations: []x.Location{{Line: 10, Column: 4}}}},
   394  		},
   395  		"Rewriting error": {
   396  			explanation: "The reference ID is not a uint64, so can't be converted to a uid",
   397  			idValue:     "hi",
   398  			mutResponse: map[string]string{"Post1": "0x2"},
   399  			mutQryResp: map[string]interface{}{
   400  				"Author2": []interface{}{map[string]string{"uid": "0x1"}}},
   401  			queryResponse: `{ "post" : [{ "title": "A Post" } ] }`,
   402  			expected: `{
   403  				"add1": { "post": [{ "title": "A Post" }] },
   404  				"add2" : null
   405  			}`,
   406  			errors: x.GqlErrorList{
   407  				&x.GqlError{Message: `couldn't rewrite mutation addPost because ` +
   408  					`failed to rewrite mutation payload because ` +
   409  					`ID argument (hi) was not able to be parsed`},
   410  				&x.GqlError{Message: `Mutation add3 was not executed because of ` +
   411  					`a previous error.`,
   412  					Locations: []x.Location{{Line: 10, Column: 4}}}},
   413  		},
   414  	}
   415  
   416  	gqlSchema := test.LoadSchemaFromString(t, testGQLSchema)
   417  
   418  	for name, tcase := range tests {
   419  		t.Run(name, func(t *testing.T) {
   420  
   421  			resp := resolveWithClient(
   422  				gqlSchema,
   423  				multiMutation,
   424  				map[string]interface{}{"id": tcase.idValue},
   425  				&executor{
   426  					resp:         tcase.queryResponse,
   427  					assigned:     tcase.mutResponse,
   428  					failMutation: 2})
   429  
   430  			if diff := cmp.Diff(tcase.errors, resp.Errors); diff != "" {
   431  				t.Errorf("errors mismatch (-want +got):\n%s", diff)
   432  			}
   433  			require.JSONEq(t, tcase.expected, resp.Data.String())
   434  		})
   435  	}
   436  }
   437  
   438  func resolve(gqlSchema schema.Schema, gqlQuery string, dgResponse string) *schema.Response {
   439  	return resolveWithClient(gqlSchema, gqlQuery, nil, &executor{resp: dgResponse})
   440  }
   441  
   442  func resolveWithClient(
   443  	gqlSchema schema.Schema,
   444  	gqlQuery string,
   445  	vars map[string]interface{},
   446  	ex *executor) *schema.Response {
   447  	resolver := New(
   448  		gqlSchema,
   449  		NewResolverFactory(nil, nil).WithConventionResolvers(gqlSchema, &ResolverFns{
   450  			Qrw: NewQueryRewriter(),
   451  			Arw: NewAddRewriter,
   452  			Urw: NewUpdateRewriter,
   453  			Qe:  ex,
   454  			Me:  ex,
   455  		}))
   456  
   457  	return resolver.Resolve(context.Background(), &schema.Request{Query: gqlQuery, Variables: vars})
   458  }