github.com/dgraph-io/dgraph@v1.2.8/graphql/e2e/common/common.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 "bytes" 21 "compress/gzip" 22 "context" 23 "encoding/json" 24 "fmt" 25 "io/ioutil" 26 "net/http" 27 "strconv" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/dgraph-io/dgo/v2" 33 "github.com/dgraph-io/dgo/v2/protos/api" 34 "github.com/dgraph-io/dgraph/x" 35 "github.com/google/uuid" 36 "github.com/pkg/errors" 37 "github.com/stretchr/testify/require" 38 "google.golang.org/grpc" 39 ) 40 41 const ( 42 graphqlURL = "http://localhost:8180/graphql" 43 graphqlAdminURL = "http://localhost:8180/admin" 44 alphagRPC = "localhost:9180" 45 46 graphqlAdminTestURL = "http://localhost:8280/graphql" 47 graphqlAdminTestAdminURL = "http://localhost:8280/admin" 48 alphaAdminTestgRPC = "localhost:9280" 49 ) 50 51 // GraphQLParams is parameters for the constructing a GraphQL query - that's 52 // http POST with this body, or http GET with this in the query string. 53 // 54 // https://graphql.org/learn/serving-over-http/ says: 55 // 56 // POST 57 // ---- 58 // 'A standard GraphQL POST request should use the application/json content type, 59 // and include a JSON-encoded body of the following form: 60 // { 61 // "query": "...", 62 // "operationName": "...", 63 // "variables": { "myVariable": "someValue", ... } 64 // } 65 // operationName and variables are optional fields. operationName is only 66 // required if multiple operations are present in the query.' 67 // 68 // 69 // GET 70 // --- 71 // 72 // http://myapi/graphql?query={me{name}} 73 // "Query variables can be sent as a JSON-encoded string in an additional query parameter 74 // called variables. If the query contains several named operations, an operationName query 75 // parameter can be used to control which one should be executed." 76 // 77 // acceptGzip sends "Accept-Encoding: gzip" header to the server, which would return the 78 // response after gzip. 79 // gzipEncoding would compress the request to the server and add "Content-Encoding: gzip" 80 // header to the same. 81 82 type GraphQLParams struct { 83 Query string `json:"query"` 84 OperationName string `json:"operationName"` 85 Variables map[string]interface{} `json:"variables"` 86 acceptGzip bool 87 gzipEncoding bool 88 } 89 90 type requestExecutor func(t *testing.T, url string, params *GraphQLParams) *GraphQLResponse 91 92 // GraphQLResponse GraphQL response structure. 93 // see https://graphql.github.io/graphql-spec/June2018/#sec-Response 94 type GraphQLResponse struct { 95 Data json.RawMessage `json:"data,omitempty"` 96 Errors x.GqlErrorList `json:"errors,omitempty"` 97 Extensions map[string]interface{} `json:"extensions,omitempty"` 98 } 99 100 type country struct { 101 ID string `json:"id,omitempty"` 102 Name string `json:"name,omitempty"` 103 States []*state `json:"states,omitempty"` 104 } 105 106 type author struct { 107 ID string `json:"id,omitempty"` 108 Name string `json:"name,omitempty"` 109 Dob *time.Time `json:"dob,omitempty"` 110 Reputation float32 `json:"reputation,omitempty"` 111 Country *country `json:"country,omitempty"` 112 Posts []*post `json:"posts,omitempty"` 113 } 114 115 type post struct { 116 PostID string `json:"postID,omitempty"` 117 Title string `json:"title,omitempty"` 118 Text string `json:"text,omitempty"` 119 Tags []string `json:"tags,omitempty"` 120 NumLikes int `json:"numLikes,omitempty"` 121 IsPublished bool `json:"isPublished,omitempty"` 122 PostType string `json:"postType,omitempty"` 123 Author *author `json:"author,omitempty"` 124 Category *category `json:"category,omitempty"` 125 } 126 127 type category struct { 128 ID string `json:"id,omitempty"` 129 Name string `json:"name,omitempty"` 130 Posts []post `json:"posts,omitempty"` 131 } 132 133 type state struct { 134 ID string `json:"id,omitempty"` 135 Name string `json:"name,omitempty"` 136 Code string `json:"xcode,omitempty"` 137 Country *country `json:"country,omitempty"` 138 } 139 140 func BootstrapServer(schema, data []byte) { 141 err := checkGraphQLLayerStarted(graphqlAdminURL) 142 if err != nil { 143 panic(fmt.Sprintf("Waited for GraphQL test server to become available, but it never did.\n"+ 144 "Got last error %+v", err.Error())) 145 } 146 147 err = checkGraphQLLayerStarted(graphqlAdminTestAdminURL) 148 if err != nil { 149 panic(fmt.Sprintf("Waited for GraphQL AdminTest server to become available, "+ 150 "but it never did.\n Got last error: %+v", err.Error())) 151 } 152 153 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 154 defer cancel() 155 d, err := grpc.DialContext(ctx, alphagRPC, grpc.WithInsecure()) 156 if err != nil { 157 panic(err) 158 } 159 client := dgo.NewDgraphClient(api.NewDgraphClient(d)) 160 161 err = addSchema(graphqlAdminURL, string(schema)) 162 if err != nil { 163 panic(err) 164 } 165 166 err = populateGraphQLData(client, data) 167 if err != nil { 168 panic(err) 169 } 170 171 err = checkGraphQLHealth(graphqlAdminURL, []string{"Healthy"}) 172 if err != nil { 173 panic(err) 174 } 175 176 if err = d.Close(); err != nil { 177 panic(err) 178 } 179 } 180 181 // RunAll runs all the test functions in this package as sub tests. 182 func RunAll(t *testing.T) { 183 // admin tests 184 t.Run("admin", admin) 185 186 // schema tests 187 t.Run("graphql descriptions", graphQLDescriptions) 188 189 // encoding 190 t.Run("gzip compression", gzipCompression) 191 t.Run("gzip compression header", gzipCompressionHeader) 192 t.Run("gzip compression no header", gzipCompressionNoHeader) 193 194 // query tests 195 t.Run("get request", getRequest) 196 t.Run("get query empty variable", getQueryEmptyVariable) 197 t.Run("query by type", queryByType) 198 t.Run("uid alias", uidAlias) 199 t.Run("order at root", orderAtRoot) 200 t.Run("page at root", pageAtRoot) 201 t.Run("regexp", regExp) 202 t.Run("multiple search indexes", multipleSearchIndexes) 203 t.Run("multiple search indexes wrong field", multipleSearchIndexesWrongField) 204 t.Run("hash search", hashSearch) 205 t.Run("deep filter", deepFilter) 206 t.Run("many queries", manyQueries) 207 t.Run("query order at root", queryOrderAtRoot) 208 t.Run("queries with error", queriesWithError) 209 t.Run("date filters", dateFilters) 210 t.Run("float filters", floatFilters) 211 t.Run("int filters", intFilters) 212 t.Run("boolean filters", booleanFilters) 213 t.Run("term filters", termFilters) 214 t.Run("full text filters", fullTextFilters) 215 t.Run("string exact filters", stringExactFilters) 216 t.Run("scalar list filters", scalarListFilters) 217 t.Run("skip directive", skipDirective) 218 t.Run("include directive", includeDirective) 219 t.Run("include and skip directive", includeAndSkipDirective) 220 t.Run("query by mutliple ids", queryByMultipleIds) 221 t.Run("enum filter", enumFilter) 222 t.Run("default enum filter", defaultEnumFilter) 223 t.Run("query by multiple invalid ids", queryByMultipleInvalidIds) 224 t.Run("query typename", queryTypename) 225 t.Run("query nested typename", queryNestedTypename) 226 t.Run("typename for interface", typenameForInterface) 227 228 t.Run("get state by xid", getStateByXid) 229 t.Run("get state without args", getStateWithoutArgs) 230 t.Run("get state by both xid and uid", getStateByBothXidAndUid) 231 t.Run("query state by xid", queryStateByXid) 232 t.Run("query state by xid regex", queryStateByXidRegex) 233 t.Run("multiple operations", multipleOperations) 234 t.Run("query post with author", queryPostWithAuthor) 235 236 // mutation tests 237 t.Run("add mutation", addMutation) 238 t.Run("update mutation by ids", updateMutationByIds) 239 t.Run("update mutation by name", updateMutationByName) 240 t.Run("update mutation by name no match", updateMutationByNameNoMatch) 241 t.Run("update delete", updateDelete) 242 t.Run("filter in update", filterInUpdate) 243 t.Run("selection in add object", testSelectionInAddObject) 244 t.Run("delete mutation with multiple ids", deleteMutationWithMultipleIds) 245 t.Run("delete mutation with single id", deleteMutationWithSingleID) 246 t.Run("delete mutation by name", deleteMutationByName) 247 t.Run("delete wrong id", deleteWrongID) 248 t.Run("many mutations", manyMutations) 249 t.Run("mutations with deep filter", mutationWithDeepFilter) 250 t.Run("many mutations with query error", manyMutationsWithQueryError) 251 t.Run("query interface after add mutation", queryInterfaceAfterAddMutation) 252 t.Run("add mutation with xid", addMutationWithXID) 253 t.Run("deep mutations", deepMutations) 254 t.Run("add multiple mutations", testMultipleMutations) 255 t.Run("deep XID mutations", deepXIDMutations) 256 t.Run("error in multiple mutations", addMultipleMutationWithOneError) 257 258 // error tests 259 t.Run("graphql completion on", graphQLCompletionOn) 260 t.Run("request validation errors", requestValidationErrors) 261 t.Run("panic catcher", panicCatcher) 262 t.Run("deep mutation errors", deepMutationErrors) 263 264 } 265 266 func gunzipData(data []byte) ([]byte, error) { 267 b := bytes.NewBuffer(data) 268 269 r, err := gzip.NewReader(b) 270 if err != nil { 271 return nil, err 272 } 273 274 var resB bytes.Buffer 275 if _, err := resB.ReadFrom(r); err != nil { 276 return nil, err 277 } 278 return resB.Bytes(), nil 279 } 280 281 func gzipData(data []byte) ([]byte, error) { 282 var b bytes.Buffer 283 gz := gzip.NewWriter(&b) 284 285 if _, err := gz.Write(data); err != nil { 286 return nil, err 287 } 288 289 if err := gz.Close(); err != nil { 290 return nil, err 291 } 292 return b.Bytes(), nil 293 } 294 295 // This tests that if a request has gzip header but the body is 296 // not compressed, then it should return an error 297 func gzipCompressionHeader(t *testing.T) { 298 queryCountry := &GraphQLParams{ 299 Query: `query { 300 queryCountry { 301 name 302 } 303 }`, 304 } 305 306 req, err := queryCountry.createGQLPost(graphqlURL) 307 require.NoError(t, err) 308 309 req.Header.Set("Content-Encoding", "gzip") 310 311 resData, err := runGQLRequest(req) 312 require.NoError(t, err) 313 314 var result *GraphQLResponse 315 err = json.Unmarshal(resData, &result) 316 require.NoError(t, err) 317 require.NotNil(t, result.Errors) 318 require.Contains(t, result.Errors[0].Message, "Unable to parse gzip") 319 } 320 321 // This tests that if a req's body is compressed but the 322 // header is not present, then it should return an error 323 func gzipCompressionNoHeader(t *testing.T) { 324 queryCountry := &GraphQLParams{ 325 Query: `query { 326 queryCountry { 327 name 328 } 329 }`, 330 gzipEncoding: true, 331 } 332 333 req, err := queryCountry.createGQLPost(graphqlURL) 334 require.NoError(t, err) 335 336 req.Header.Del("Content-Encoding") 337 resData, err := runGQLRequest(req) 338 require.NoError(t, err) 339 340 var result *GraphQLResponse 341 err = json.Unmarshal(resData, &result) 342 require.NoError(t, err) 343 require.NotNil(t, result.Errors) 344 require.Contains(t, result.Errors[0].Message, "Not a valid GraphQL request body") 345 } 346 347 func getRequest(t *testing.T) { 348 add(t, getExecutor) 349 } 350 351 func getQueryEmptyVariable(t *testing.T) { 352 queryCountry := &GraphQLParams{ 353 Query: `query { 354 queryCountry { 355 name 356 } 357 }`, 358 } 359 req, err := queryCountry.createGQLGet(graphqlURL) 360 require.NoError(t, err) 361 362 q := req.URL.Query() 363 q.Del("variables") 364 req.URL.RawQuery = q.Encode() 365 366 res := queryCountry.Execute(t, req) 367 require.Nil(t, res.Errors) 368 } 369 370 // Execute takes a HTTP request from either ExecuteAsPost or ExecuteAsGet 371 // and executes the request 372 func (params *GraphQLParams) Execute(t *testing.T, req *http.Request) *GraphQLResponse { 373 res, err := runGQLRequest(req) 374 require.NoError(t, err) 375 376 var result *GraphQLResponse 377 if params.acceptGzip { 378 res, err = gunzipData(res) 379 require.NoError(t, err) 380 require.Contains(t, req.Header.Get("Accept-Encoding"), "gzip") 381 } 382 err = json.Unmarshal(res, &result) 383 require.NoError(t, err) 384 385 requireContainsRequestID(t, result) 386 387 return result 388 389 } 390 391 // ExecuteAsPost builds a HTTP POST request from the GraphQL input structure 392 // and executes the request to url. 393 func (params *GraphQLParams) ExecuteAsPost(t *testing.T, url string) *GraphQLResponse { 394 req, err := params.createGQLPost(url) 395 require.NoError(t, err) 396 397 return params.Execute(t, req) 398 } 399 400 // ExecuteAsGet builds a HTTP GET request from the GraphQL input structure 401 // and executes the request to url. 402 func (params *GraphQLParams) ExecuteAsGet(t *testing.T, url string) *GraphQLResponse { 403 req, err := params.createGQLGet(url) 404 require.NoError(t, err) 405 406 return params.Execute(t, req) 407 } 408 409 func getExecutor(t *testing.T, url string, params *GraphQLParams) *GraphQLResponse { 410 return params.ExecuteAsGet(t, url) 411 } 412 413 func postExecutor(t *testing.T, url string, params *GraphQLParams) *GraphQLResponse { 414 return params.ExecuteAsPost(t, url) 415 } 416 417 func (params *GraphQLParams) createGQLGet(url string) (*http.Request, error) { 418 req, err := http.NewRequest("GET", url, nil) 419 if err != nil { 420 return nil, err 421 } 422 423 q := req.URL.Query() 424 q.Add("query", params.Query) 425 q.Add("operationName", params.OperationName) 426 427 variableString, err := json.Marshal(params.Variables) 428 if err != nil { 429 return nil, err 430 } 431 q.Add("variables", string(variableString)) 432 433 req.URL.RawQuery = q.Encode() 434 if params.acceptGzip { 435 req.Header.Set("Accept-Encoding", "gzip") 436 } 437 return req, nil 438 } 439 440 func (params *GraphQLParams) createGQLPost(url string) (*http.Request, error) { 441 body, err := json.Marshal(params) 442 if err != nil { 443 return nil, err 444 } 445 446 if params.gzipEncoding { 447 if body, err = gzipData(body); err != nil { 448 return nil, err 449 } 450 } 451 452 req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) 453 if err != nil { 454 return nil, err 455 } 456 req.Header.Set("Content-Type", "application/json") 457 if params.gzipEncoding { 458 req.Header.Set("Content-Encoding", "gzip") 459 } 460 461 if params.acceptGzip { 462 req.Header.Set("Accept-Encoding", "gzip") 463 } 464 465 return req, nil 466 } 467 468 // runGQLRequest runs a HTTP GraphQL request and returns the data or any errors. 469 func runGQLRequest(req *http.Request) ([]byte, error) { 470 client := &http.Client{Timeout: 5 * time.Second} 471 resp, err := client.Do(req) 472 if err != nil { 473 return nil, err 474 } 475 476 // GraphQL server should always return OK, even when there are errors 477 if status := resp.StatusCode; status != http.StatusOK { 478 return nil, errors.Errorf("unexpected status code: %v", status) 479 } 480 481 if strings.ToLower(resp.Header.Get("Content-Type")) != "application/json" { 482 return nil, errors.Errorf("unexpected content type: %v", resp.Header.Get("Content-Type")) 483 } 484 485 if resp.Header.Get("Access-Control-Allow-Origin") != "*" { 486 return nil, errors.Errorf("cors headers weren't set in response") 487 } 488 489 defer resp.Body.Close() 490 body, err := ioutil.ReadAll(resp.Body) 491 if err != nil { 492 return nil, errors.Errorf("unable to read response body: %v", err) 493 } 494 495 return body, nil 496 } 497 498 func requireContainsRequestID(t *testing.T, resp *GraphQLResponse) { 499 500 v, ok := resp.Extensions["requestID"] 501 require.True(t, ok, 502 "GraphQL response didn't contain a request ID - response was:\n%s", 503 serializeOrError(resp)) 504 505 str, ok := v.(string) 506 require.True(t, ok, "GraphQL requestID is not a string - response was:\n%s", 507 serializeOrError(resp)) 508 509 _, err := uuid.Parse(str) 510 require.NoError(t, err, "GraphQL requestID is not a UUID - response was:\n%s", 511 serializeOrError(resp)) 512 } 513 514 func requireUID(t *testing.T, uid string) { 515 _, err := strconv.ParseUint(uid, 0, 64) 516 require.NoError(t, err) 517 } 518 519 func requireNoGQLErrors(t *testing.T, resp *GraphQLResponse) { 520 require.Nil(t, resp.Errors, 521 "required no GraphQL errors, but received :\n%s", serializeOrError(resp.Errors)) 522 } 523 524 func serializeOrError(toSerialize interface{}) string { 525 byts, err := json.Marshal(toSerialize) 526 if err != nil { 527 return "unable to serialize because " + err.Error() 528 } 529 return string(byts) 530 } 531 532 func populateGraphQLData(client *dgo.Dgraph, data []byte) error { 533 // Helps in local dev to not re-add data multiple times. 534 countries, err := allCountriesAdded() 535 if err != nil { 536 return errors.Wrap(err, "couldn't determine if GraphQL data had already been added") 537 } 538 if len(countries) > 0 { 539 return nil 540 } 541 542 mu := &api.Mutation{ 543 CommitNow: true, 544 SetJson: data, 545 } 546 _, err = client.NewTxn().Mutate(context.Background(), mu) 547 if err != nil { 548 return errors.Wrap(err, "Unable to add GraphQL test data") 549 } 550 551 return nil 552 } 553 554 func allCountriesAdded() ([]*country, error) { 555 body, err := json.Marshal(&GraphQLParams{Query: `query { queryCountry { name } }`}) 556 if err != nil { 557 return nil, errors.Wrap(err, "unable to build GraphQL query") 558 } 559 560 req, err := http.NewRequest("POST", graphqlURL, bytes.NewBuffer(body)) 561 if err != nil { 562 return nil, errors.Wrap(err, "unable to build GraphQL request") 563 } 564 req.Header.Set("Content-Type", "application/json") 565 566 resp, err := runGQLRequest(req) 567 if err != nil { 568 return nil, errors.Wrap(err, "error running GraphQL query") 569 } 570 571 var result struct { 572 Data struct { 573 QueryCountry []*country 574 } 575 } 576 err = json.Unmarshal(resp, &result) 577 if err != nil { 578 return nil, errors.Wrap(err, "error trying to unmarshal GraphQL query result") 579 } 580 581 return result.Data.QueryCountry, nil 582 } 583 584 func checkGraphQLLayerStarted(url string) error { 585 var err error 586 retries := 6 587 sleep := 10 * time.Second 588 589 // Because of how the test containers are brought up, there's no guarantee 590 // that the GraphQL layer is running by now. So we 591 // need to try and connect and potentially retry a few times. 592 for retries > 0 { 593 retries-- 594 595 // In local dev, we might already have an instance Healthy. In CI, 596 // we expect the GraphQL layer to be waiting for a first schema. 597 err = checkGraphQLHealth(url, []string{"NoGraphQLSchema", "Healthy"}) 598 if err == nil { 599 return nil 600 } 601 time.Sleep(sleep) 602 } 603 return err 604 } 605 606 func checkGraphQLHealth(url string, status []string) error { 607 health := &GraphQLParams{ 608 Query: `query { 609 health { 610 message 611 status 612 } 613 }`, 614 } 615 req, err := health.createGQLPost(url) 616 if err != nil { 617 return errors.Wrap(err, "while creating gql post") 618 } 619 620 resp, err := runGQLRequest(req) 621 if err != nil { 622 return errors.Wrap(err, "error running GraphQL query") 623 } 624 625 var healthResult struct { 626 Data struct { 627 Health struct { 628 Message string 629 Status string 630 } 631 } 632 Errors x.GqlErrorList 633 } 634 635 err = json.Unmarshal(resp, &healthResult) 636 if err != nil { 637 return errors.Wrap(err, "error trying to unmarshal GraphQL query result") 638 } 639 640 if len(healthResult.Errors) > 0 { 641 return healthResult.Errors 642 } 643 644 for _, s := range status { 645 if healthResult.Data.Health.Status == s { 646 return nil 647 } 648 } 649 650 return errors.Errorf("GraphQL server was not at right health: found %s", 651 healthResult.Data.Health.Status) 652 } 653 654 func addSchema(url string, schema string) error { 655 add := &GraphQLParams{ 656 Query: `mutation updateGQLSchema($sch: String!) { 657 updateGQLSchema(input: { set: { schema: $sch }}) { 658 gqlSchema { 659 schema 660 } 661 } 662 }`, 663 Variables: map[string]interface{}{"sch": schema}, 664 } 665 req, err := add.createGQLPost(url) 666 if err != nil { 667 return errors.Wrap(err, "error running GraphQL query") 668 } 669 670 resp, err := runGQLRequest(req) 671 if err != nil { 672 return errors.Wrap(err, "error running GraphQL query") 673 } 674 675 var addResult struct { 676 Data struct { 677 UpdateGQLSchema struct { 678 GQLSchema struct { 679 Schema string 680 } 681 } 682 } 683 } 684 685 err = json.Unmarshal(resp, &addResult) 686 if err != nil { 687 return errors.Wrap(err, "error trying to unmarshal GraphQL mutation result") 688 } 689 690 if addResult.Data.UpdateGQLSchema.GQLSchema.Schema == "" { 691 return errors.New("GraphQL schema mutation failed") 692 } 693 694 return nil 695 }