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 }