github.com/dgraph-io/dgraph@v1.2.8/graphql/e2e/common/error.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 common
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/http/httptest"
    25  	"sort"
    26  	"testing"
    27  
    28  	"github.com/dgraph-io/dgo/v2"
    29  	"github.com/dgraph-io/dgo/v2/protos/api"
    30  	dgoapi "github.com/dgraph-io/dgo/v2/protos/api"
    31  	"github.com/dgraph-io/dgraph/gql"
    32  	"github.com/dgraph-io/dgraph/graphql/resolve"
    33  	"github.com/dgraph-io/dgraph/graphql/test"
    34  	"github.com/dgraph-io/dgraph/graphql/web"
    35  	"github.com/dgraph-io/dgraph/x"
    36  	"github.com/google/go-cmp/cmp"
    37  	"github.com/stretchr/testify/require"
    38  	"google.golang.org/grpc"
    39  	"gopkg.in/yaml.v2"
    40  )
    41  
    42  type ErrorCase struct {
    43  	Name       string
    44  	GQLRequest string
    45  	variables  map[string]interface{}
    46  	Errors     x.GqlErrorList
    47  }
    48  
    49  func graphQLCompletionOn(t *testing.T) {
    50  	newCountry := addCountry(t, postExecutor)
    51  
    52  	// delete the country's name.
    53  	// The schema states type Country `{ ... name: String! ... }`
    54  	// so a query error will be raised if we ask for the country's name in a
    55  	// query.  Don't think a GraphQL update can do this ATM, so do through Dgraph.
    56  	d, err := grpc.Dial(alphagRPC, grpc.WithInsecure())
    57  	require.NoError(t, err)
    58  	client := dgo.NewDgraphClient(api.NewDgraphClient(d))
    59  	mu := &api.Mutation{
    60  		CommitNow: true,
    61  		DelNquads: []byte(fmt.Sprintf("<%s> <Country.name> * .", newCountry.ID)),
    62  	}
    63  	_, err = client.NewTxn().Mutate(context.Background(), mu)
    64  	require.NoError(t, err)
    65  
    66  	tests := [2]string{"name", "id name"}
    67  	for _, test := range tests {
    68  		t.Run(test, func(t *testing.T) {
    69  			queryCountry := &GraphQLParams{
    70  				Query: fmt.Sprintf(`query {queryCountry {%s}}`, test),
    71  			}
    72  
    73  			// Check that the error is valid
    74  			gqlResponse := queryCountry.ExecuteAsPost(t, graphqlURL)
    75  			require.NotNil(t, gqlResponse.Errors)
    76  			require.Equal(t, 1, len(gqlResponse.Errors))
    77  			require.Contains(t, gqlResponse.Errors[0].Error(),
    78  				"Non-nullable field 'name' (type String!) was not present"+
    79  					" in result from Dgraph.")
    80  
    81  			// Check that the result is valid
    82  			var result, expected struct {
    83  				QueryCountry []*country
    84  			}
    85  			err := json.Unmarshal([]byte(gqlResponse.Data), &result)
    86  			require.NoError(t, err)
    87  			require.Equal(t, 4, len(result.QueryCountry))
    88  			expected.QueryCountry = []*country{
    89  				&country{Name: "Angola"},
    90  				&country{Name: "Bangladesh"},
    91  				&country{Name: "Mozambique"},
    92  				nil,
    93  			}
    94  
    95  			sort.Slice(result.QueryCountry, func(i, j int) bool {
    96  				if result.QueryCountry[i] == nil {
    97  					return false
    98  				}
    99  				return result.QueryCountry[i].Name < result.QueryCountry[j].Name
   100  			})
   101  
   102  			for i := 0; i < 3; i++ {
   103  				require.NotNil(t, result.QueryCountry[i])
   104  				require.Equal(t, result.QueryCountry[i].Name, expected.QueryCountry[i].Name)
   105  			}
   106  			require.Nil(t, result.QueryCountry[3])
   107  		})
   108  	}
   109  
   110  	cleanUp(t,
   111  		[]*country{newCountry},
   112  		[]*author{},
   113  		[]*post{},
   114  	)
   115  }
   116  
   117  func deepMutationErrors(t *testing.T) {
   118  	executeRequest := postExecutor
   119  
   120  	newCountry := addCountry(t, postExecutor)
   121  
   122  	tcases := map[string]struct {
   123  		set *country
   124  		exp string
   125  	}{
   126  		"missing ID and XID": {
   127  			set: &country{States: []*state{{Name: "NOT A VALID STATE"}}},
   128  			exp: "couldn't rewrite mutation updateCountry because failed to rewrite mutation " +
   129  				"payload because type State requires a value for field xcode, but no value present",
   130  		},
   131  		"ID not valid": {
   132  			set: &country{States: []*state{{ID: "HI"}}},
   133  			exp: "couldn't rewrite mutation updateCountry because failed to rewrite " +
   134  				"mutation payload because ID argument (HI) was not able to be parsed",
   135  		},
   136  		"ID not found": {
   137  			set: &country{States: []*state{{ID: "0x1"}}},
   138  			exp: "couldn't rewrite query for mutation updateCountry because ID \"0x1\" isn't a State",
   139  		},
   140  		"XID not found": {
   141  			set: &country{States: []*state{{Code: "NOT A VALID CODE"}}},
   142  			exp: "couldn't rewrite query for mutation updateCountry because xid " +
   143  				"\"NOT A VALID CODE\" doesn't exist and input object not well formed because type " +
   144  				"State requires a value for field name, but no value present",
   145  		},
   146  	}
   147  
   148  	for name, tcase := range tcases {
   149  		t.Run(name, func(t *testing.T) {
   150  			updateCountryParams := &GraphQLParams{
   151  				Query: `mutation updateCountry($id: ID!, $set: CountryPatch!) {
   152  					updateCountry(input: {filter: {id: [$id]}, set: $set}) {
   153  						country { id }
   154  					}
   155  				}`,
   156  				Variables: map[string]interface{}{
   157  					"id":  newCountry.ID,
   158  					"set": tcase.set,
   159  				},
   160  			}
   161  
   162  			gqlResponse := executeRequest(t, graphqlURL, updateCountryParams)
   163  			require.NotNil(t, gqlResponse.Errors)
   164  			require.Equal(t, 1, len(gqlResponse.Errors))
   165  			require.EqualError(t, gqlResponse.Errors[0], tcase.exp)
   166  		})
   167  	}
   168  
   169  	cleanUp(t, []*country{newCountry}, []*author{}, []*post{})
   170  }
   171  
   172  // requestValidationErrors just makes sure we are catching validation failures.
   173  // Mostly this is provided by an external lib, so just checking we hit common cases.
   174  func requestValidationErrors(t *testing.T) {
   175  	b, err := ioutil.ReadFile("../common/error_test.yaml")
   176  	require.NoError(t, err, "Unable to read test file")
   177  
   178  	var tests []ErrorCase
   179  	err = yaml.Unmarshal(b, &tests)
   180  	require.NoError(t, err, "Unable to unmarshal test cases from yaml.")
   181  
   182  	for _, tcase := range tests {
   183  		t.Run(tcase.Name, func(t *testing.T) {
   184  			test := &GraphQLParams{
   185  				Query:     tcase.GQLRequest,
   186  				Variables: tcase.variables,
   187  			}
   188  			gqlResponse := test.ExecuteAsPost(t, graphqlURL)
   189  
   190  			require.Nil(t, gqlResponse.Data)
   191  			if diff := cmp.Diff(tcase.Errors, gqlResponse.Errors); diff != "" {
   192  				t.Errorf("errors mismatch (-want +got):\n%s", diff)
   193  			}
   194  		})
   195  	}
   196  }
   197  
   198  // panicCatcher tests that the GraphQL server behaves properly when an internal
   199  // bug triggers a panic.  Here, this is mocked up with httptest and a dgraph package
   200  // that just panics.
   201  //
   202  // Not really an e2e test cause it uses httptest and mocks up a panicing Dgraph, but
   203  // uses all the e2e infrastructure.
   204  func panicCatcher(t *testing.T) {
   205  
   206  	// queries and mutations have different panic paths.
   207  	//
   208  	// Because queries run concurrently in their own goroutine, any panics are
   209  	// caught by a panic handler deferred when starting those goroutines.
   210  	//
   211  	// Mutations run serially in the same goroutine as the original http handler,
   212  	// so a panic here is caught by the panic catching http handler that wraps
   213  	// the http stack.
   214  
   215  	tests := map[string]*GraphQLParams{
   216  		"query": &GraphQLParams{Query: `query { queryCountry { name } }`},
   217  		"mutation": &GraphQLParams{
   218  			Query: `mutation {
   219  						addCountry(input: [{ name: "A Country" }]) { country { id } }
   220  					}`,
   221  		},
   222  	}
   223  
   224  	gqlSchema := test.LoadSchemaFromFile(t, "schema.graphql")
   225  
   226  	fns := &resolve.ResolverFns{
   227  		Qrw: resolve.NewQueryRewriter(),
   228  		Arw: resolve.NewAddRewriter,
   229  		Urw: resolve.NewUpdateRewriter,
   230  		Drw: resolve.NewDeleteRewriter(),
   231  		Qe:  &panicClient{},
   232  		Me:  &panicClient{}}
   233  
   234  	resolverFactory := resolve.NewResolverFactory(nil, nil).
   235  		WithConventionResolvers(gqlSchema, fns)
   236  
   237  	resolvers := resolve.New(gqlSchema, resolverFactory)
   238  	server := web.NewServer(resolvers)
   239  
   240  	ts := httptest.NewServer(server.HTTPHandler())
   241  	defer ts.Close()
   242  
   243  	for name, test := range tests {
   244  		t.Run(name, func(t *testing.T) {
   245  			gqlResponse := test.ExecuteAsPost(t, ts.URL)
   246  
   247  			require.Equal(t, x.GqlErrorList{
   248  				{Message: fmt.Sprintf("[%s] Internal Server Error - a panic was trapped.  "+
   249  					"This indicates a bug in the GraphQL server.  A stack trace was logged.  "+
   250  					"Please let us know : https://github.com/dgraph-io/dgraph/issues.",
   251  					gqlResponse.Extensions["requestID"].(string))}},
   252  				gqlResponse.Errors)
   253  
   254  			require.Nil(t, gqlResponse.Data)
   255  		})
   256  	}
   257  }
   258  
   259  type panicClient struct{}
   260  
   261  func (dg *panicClient) Query(ctx context.Context, query *gql.GraphQuery) ([]byte, error) {
   262  	panic("bugz!!!")
   263  }
   264  
   265  func (dg *panicClient) Mutate(
   266  	ctx context.Context,
   267  	query *gql.GraphQuery,
   268  	mutations []*dgoapi.Mutation) (map[string]string, map[string]interface{}, error) {
   269  	panic("bugz!!!")
   270  }