github.com/dgraph-io/dgraph@v1.2.8/graphql/e2e/common/mutation.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 // Tests that mutate the GraphQL database should return the database state to what it 20 // was at the begining of the test. The GraphQL query tests rely on a fixed input 21 // dataset and mutating and leaving unexpected data will result in flaky tests. 22 23 import ( 24 "context" 25 "encoding/json" 26 "fmt" 27 "sort" 28 "testing" 29 30 "github.com/dgraph-io/dgo/v2" 31 "github.com/dgraph-io/dgo/v2/protos/api" 32 "github.com/dgraph-io/dgraph/testutil" 33 "github.com/dgraph-io/dgraph/x" 34 "github.com/google/go-cmp/cmp" 35 "github.com/google/go-cmp/cmp/cmpopts" 36 "github.com/stretchr/testify/require" 37 "google.golang.org/grpc" 38 ) 39 40 // TestAddMutation tests that add mutations work as expected. There's a few angles 41 // that need testing: 42 // - add single object, 43 // - add object with reference to existing object, and 44 // - add where @hasInverse edges need linking. 45 // 46 // These really need to run as one test because the created uid from the Country 47 // needs to flow to the author, etc. 48 func addMutation(t *testing.T) { 49 add(t, postExecutor) 50 } 51 52 func add(t *testing.T, executeRequest requestExecutor) { 53 var newCountry *country 54 var newAuthor *author 55 var newPost *post 56 57 // Add single object : 58 // Country is a single object not linked to anything else. 59 // So only need to check that it gets added as expected. 60 newCountry = addCountry(t, executeRequest) 61 62 // addCountry() asserts that the mutation response was as expected. 63 // Let's also check that what's in the DB is what we expect. 64 requireCountry(t, newCountry.ID, newCountry, false, executeRequest) 65 66 // Add object with reference to existing object : 67 // An Author links to an existing country. So need to check that the author 68 // was added and that it has the link to the right Country. 69 newAuthor = addAuthor(t, newCountry.ID, executeRequest) 70 requireAuthor(t, newAuthor.ID, newAuthor, executeRequest) 71 72 // Add with @hasInverse : 73 // Posts link to an Author and the Author has a link back to all their Posts. 74 // So need to check that the Post was added to the right Author 75 // AND that the Author's posts now includes the new post. 76 newPost = addPost(t, newAuthor.ID, newCountry.ID, executeRequest) 77 requirePost(t, newPost.PostID, newPost, true, executeRequest) 78 79 cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{newPost}) 80 } 81 82 func addCountry(t *testing.T, executeRequest requestExecutor) *country { 83 addCountryParams := &GraphQLParams{ 84 Query: `mutation addCountry($name: String!) { 85 addCountry(input: [{ name: $name }]) { 86 country { 87 id 88 name 89 } 90 } 91 }`, 92 Variables: map[string]interface{}{"name": "Testland"}, 93 } 94 addCountryExpected := ` 95 { "addCountry": { "country": [{ "id": "_UID_", "name": "Testland" }] } }` 96 97 gqlResponse := executeRequest(t, graphqlURL, addCountryParams) 98 requireNoGQLErrors(t, gqlResponse) 99 100 var expected, result struct { 101 AddCountry struct { 102 Country []*country 103 } 104 } 105 err := json.Unmarshal([]byte(addCountryExpected), &expected) 106 require.NoError(t, err) 107 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 108 require.NoError(t, err) 109 110 require.Equal(t, len(result.AddCountry.Country), 1) 111 requireUID(t, result.AddCountry.Country[0].ID) 112 113 // Always ignore the ID of the object that was just created. That ID is 114 // minted by Dgraph. 115 opt := cmpopts.IgnoreFields(country{}, "ID") 116 if diff := cmp.Diff(expected, result, opt); diff != "" { 117 t.Errorf("result mismatch (-want +got):\n%s", diff) 118 } 119 120 return result.AddCountry.Country[0] 121 } 122 123 // requireCountry enforces that node with ID uid in the GraphQL store is of type 124 // Country and is value expectedCountry. 125 func requireCountry(t *testing.T, uid string, expectedCountry *country, includeStates bool, 126 executeRequest requestExecutor) { 127 128 params := &GraphQLParams{ 129 Query: `query getCountry($id: ID!, $includeStates: Boolean!) { 130 getCountry(id: $id) { 131 id 132 name 133 states(order: { asc: xcode }) @include(if: $includeStates) { 134 id 135 xcode 136 name 137 } 138 } 139 }`, 140 Variables: map[string]interface{}{"id": uid, "includeStates": includeStates}, 141 } 142 gqlResponse := executeRequest(t, graphqlURL, params) 143 requireNoGQLErrors(t, gqlResponse) 144 145 var result struct { 146 GetCountry *country 147 } 148 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 149 require.NoError(t, err) 150 151 if diff := cmp.Diff(expectedCountry, result.GetCountry, ignoreOpts()...); diff != "" { 152 t.Errorf("result mismatch (-want +got):\n%s", diff) 153 } 154 } 155 156 func addAuthor(t *testing.T, countryUID string, 157 executeRequest requestExecutor) *author { 158 159 addAuthorParams := &GraphQLParams{ 160 Query: `mutation addAuthor($author: AddAuthorInput!) { 161 addAuthor(input: [$author]) { 162 author { 163 id 164 name 165 dob 166 reputation 167 country { 168 id 169 name 170 } 171 posts { 172 title 173 text 174 } 175 } 176 } 177 }`, 178 Variables: map[string]interface{}{"author": map[string]interface{}{ 179 "name": "Test Author", 180 "dob": "2010-01-01T05:04:33Z", 181 "reputation": 7.75, 182 "country": map[string]interface{}{"id": countryUID}, 183 }}, 184 } 185 186 addAuthorExpected := fmt.Sprintf(`{ "addAuthor": { 187 "author": [{ 188 "id": "_UID_", 189 "name": "Test Author", 190 "dob": "2010-01-01T05:04:33Z", 191 "reputation": 7.75, 192 "country": { 193 "id": "%s", 194 "name": "Testland" 195 }, 196 "posts": [] 197 }] 198 } }`, countryUID) 199 200 gqlResponse := executeRequest(t, graphqlURL, addAuthorParams) 201 requireNoGQLErrors(t, gqlResponse) 202 203 var expected, result struct { 204 AddAuthor struct { 205 Author []*author 206 } 207 } 208 err := json.Unmarshal([]byte(addAuthorExpected), &expected) 209 require.NoError(t, err) 210 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 211 require.NoError(t, err) 212 213 require.Equal(t, len(result.AddAuthor.Author), 1) 214 requireUID(t, result.AddAuthor.Author[0].ID) 215 216 opt := cmpopts.IgnoreFields(author{}, "ID") 217 if diff := cmp.Diff(expected, result, opt); diff != "" { 218 t.Errorf("result mismatch (-want +got):\n%s", diff) 219 } 220 221 return result.AddAuthor.Author[0] 222 } 223 224 func requireAuthor(t *testing.T, authorID string, expectedAuthor *author, 225 executeRequest requestExecutor) { 226 227 params := &GraphQLParams{ 228 Query: `query getAuthor($id: ID!) { 229 getAuthor(id: $id) { 230 id 231 name 232 dob 233 reputation 234 country { 235 id 236 name 237 } 238 posts(order: { asc: title }) { 239 postID 240 title 241 text 242 tags 243 category { 244 id 245 name 246 } 247 } 248 } 249 }`, 250 Variables: map[string]interface{}{"id": authorID}, 251 } 252 gqlResponse := executeRequest(t, graphqlURL, params) 253 requireNoGQLErrors(t, gqlResponse) 254 255 var result struct { 256 GetAuthor *author 257 } 258 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 259 require.NoError(t, err) 260 261 if diff := cmp.Diff(expectedAuthor, result.GetAuthor, ignoreOpts()...); diff != "" { 262 t.Errorf("result mismatch (-want +got):\n%s", diff) 263 } 264 } 265 266 func ignoreOpts() []cmp.Option { 267 return []cmp.Option{ 268 cmpopts.IgnoreFields(author{}, "ID"), 269 cmpopts.IgnoreFields(country{}, "ID"), 270 cmpopts.IgnoreFields(post{}, "PostID"), 271 cmpopts.IgnoreFields(state{}, "ID"), 272 cmpopts.IgnoreFields(category{}, "ID"), 273 } 274 } 275 276 func deepMutations(t *testing.T) { 277 deepMutationsTest(t, postExecutor) 278 } 279 280 func deepMutationsTest(t *testing.T, executeRequest requestExecutor) { 281 newCountry := addCountry(t, executeRequest) 282 283 auth := &author{ 284 Name: "New Author", 285 Country: newCountry, 286 Posts: []*post{ 287 { 288 Title: "A New Post", 289 Text: "Text of new post", 290 Tags: []string{}, 291 Category: &category{Name: "A Category"}, 292 }, 293 { 294 Title: "Another New Post", 295 Text: "Text of other new post", 296 Tags: []string{}, 297 }, 298 }, 299 } 300 301 newAuth := addMultipleAuthorFromRef(t, []*author{auth}, executeRequest)[0] 302 requireAuthor(t, newAuth.ID, newAuth, executeRequest) 303 304 anotherCountry := addCountry(t, executeRequest) 305 306 patchSet := &author{ 307 Posts: []*post{ 308 { 309 Title: "Creating in an update", 310 Text: "Text of new post", 311 Category: newAuth.Posts[0].Category, 312 Tags: []string{}, 313 }, 314 }, 315 // Country: anotherCountry, 316 // FIXME: Won't work till https://github.com/dgraph-io/dgraph/pull/4411 is merged 317 } 318 319 patchRemove := &author{ 320 Posts: []*post{newAuth.Posts[0]}, 321 } 322 323 expectedAuthor := &author{ 324 Name: "New Author", 325 // Country: anotherCountry, 326 Country: newCountry, 327 Posts: []*post{newAuth.Posts[1], patchSet.Posts[0]}, 328 } 329 330 updateAuthorParams := &GraphQLParams{ 331 Query: `mutation updateAuthor($id: ID!, $set: AuthorPatch!, $remove: AuthorPatch!) { 332 updateAuthor( 333 input: { 334 filter: {id: [$id]}, 335 set: $set, 336 remove: $remove 337 } 338 ) { 339 author { 340 id 341 name 342 country { 343 id 344 name 345 } 346 posts { 347 title 348 text 349 tags 350 category { 351 id 352 name 353 } 354 } 355 } 356 } 357 }`, 358 Variables: map[string]interface{}{ 359 "id": newAuth.ID, 360 "set": patchSet, 361 "remove": patchRemove, 362 }, 363 } 364 365 gqlResponse := executeRequest(t, graphqlURL, updateAuthorParams) 366 requireNoGQLErrors(t, gqlResponse) 367 368 var result struct { 369 UpdateAuthor struct { 370 Author []*author 371 } 372 } 373 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 374 require.NoError(t, err) 375 require.Len(t, result.UpdateAuthor.Author, 1) 376 377 if diff := 378 cmp.Diff(expectedAuthor, result.UpdateAuthor.Author[0], ignoreOpts()...); diff != "" { 379 t.Errorf("result mismatch (-want +got):\n%s", diff) 380 } 381 382 requireAuthor(t, newAuth.ID, expectedAuthor, executeRequest) 383 p := &post{ 384 PostID: newAuth.Posts[0].PostID, 385 Title: newAuth.Posts[0].Title, 386 Text: newAuth.Posts[0].Text, 387 Tags: []string{}, 388 Author: nil, 389 } 390 requirePost(t, newAuth.Posts[0].PostID, p, false, executeRequest) 391 392 cleanUp(t, 393 []*country{newCountry, anotherCountry}, 394 []*author{newAuth}, 395 []*post{newAuth.Posts[0], newAuth.Posts[0], patchSet.Posts[0]}) 396 } 397 398 func testMultipleMutations(t *testing.T) { 399 newCountry := addCountry(t, postExecutor) 400 401 auth1 := &author{ 402 Name: "New Author1", 403 Country: newCountry, 404 Posts: []*post{ 405 { 406 Title: "A New Post", 407 Text: "Text of new post", 408 Tags: []string{}, 409 }, 410 { 411 Title: "Another New Post", 412 Text: "Text of other new post", 413 Tags: []string{}, 414 }, 415 }, 416 } 417 418 auth2 := &author{ 419 Name: "New Author2", 420 Country: newCountry, 421 Posts: []*post{ 422 { 423 Title: "A Wonder Post", 424 Text: "Text of wonder post", 425 Tags: []string{}, 426 }, 427 { 428 Title: "Another Wonder Post", 429 Text: "Text of other wonder post", 430 Tags: []string{}, 431 }, 432 }, 433 } 434 435 expectedAuthors := []*author{auth1, auth2} 436 newAuths := addMultipleAuthorFromRef(t, expectedAuthors, postExecutor) 437 438 for _, auth := range newAuths { 439 postSort := func(i, j int) bool { 440 return auth.Posts[i].Title < auth.Posts[j].Title 441 } 442 sort.Slice(auth.Posts, postSort) 443 } 444 445 for i := range expectedAuthors { 446 for j := range expectedAuthors[i].Posts { 447 expectedAuthors[i].Posts[j].PostID = newAuths[i].Posts[j].PostID 448 } 449 } 450 451 for i := range newAuths { 452 requireAuthor(t, newAuths[i].ID, expectedAuthors[i], postExecutor) 453 require.Equal(t, len(newAuths[i].Posts), 2) 454 for j := range newAuths[i].Posts { 455 expectedAuthors[i].Posts[j].Author = &author{ 456 ID: newAuths[i].ID, 457 Name: expectedAuthors[i].Name, 458 Dob: expectedAuthors[i].Dob, 459 Country: expectedAuthors[i].Country, 460 } 461 requirePost(t, newAuths[i].Posts[j].PostID, expectedAuthors[i].Posts[j], 462 true, postExecutor) 463 } 464 } 465 466 cleanUp(t, 467 []*country{newCountry}, 468 newAuths, 469 append(newAuths[0].Posts, newAuths[1].Posts...)) 470 } 471 472 func addMultipleAuthorFromRef(t *testing.T, newAuthor []*author, 473 executeRequest requestExecutor) []*author { 474 addAuthorParams := &GraphQLParams{ 475 Query: `mutation addAuthor($author: [AddAuthorInput!]!) { 476 addAuthor(input: $author) { 477 author { 478 id 479 name 480 reputation 481 country { 482 id 483 name 484 } 485 posts(order: { asc: title }) { 486 postID 487 title 488 text 489 tags 490 category { 491 id 492 name 493 } 494 } 495 } 496 } 497 }`, 498 Variables: map[string]interface{}{"author": newAuthor}, 499 } 500 501 gqlResponse := executeRequest(t, graphqlURL, addAuthorParams) 502 requireNoGQLErrors(t, gqlResponse) 503 504 var result struct { 505 AddAuthor struct { 506 Author []*author 507 } 508 } 509 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 510 require.NoError(t, err) 511 512 for i := range result.AddAuthor.Author { 513 requireUID(t, result.AddAuthor.Author[i].ID) 514 } 515 516 authorSort := func(i, j int) bool { 517 return result.AddAuthor.Author[i].Name < result.AddAuthor.Author[j].Name 518 } 519 sort.Slice(result.AddAuthor.Author, authorSort) 520 if diff := cmp.Diff(newAuthor, result.AddAuthor.Author, ignoreOpts()...); diff != "" { 521 t.Errorf("result mismatch (-want +got):\n%s", diff) 522 } 523 524 return result.AddAuthor.Author 525 526 } 527 528 func deepXIDMutations(t *testing.T) { 529 deepXIDTest(t, postExecutor) 530 } 531 532 func deepXIDTest(t *testing.T, executeRequest requestExecutor) { 533 newCountry := &country{ 534 Name: "A Country", 535 States: []*state{ 536 {Name: "Alphabet", Code: "ABC"}, 537 {Name: "A State", Code: "XYZ"}, 538 }, 539 } 540 541 // mutations get run serially, each in their own transaction, so the addState 542 // sets up the "XZY" xid that's used by the following mutation. 543 addCountryParams := &GraphQLParams{ 544 Query: `mutation addCountry($input: AddCountryInput!) { 545 addState(input: [{ xcode: "XYZ", name: "A State" }]) { 546 state { id xcode name } 547 } 548 549 addCountry(input: [$input]) 550 { 551 country { 552 id 553 name 554 states(order: { asc: xcode }) { 555 id 556 xcode 557 name 558 } 559 } 560 } 561 }`, 562 Variables: map[string]interface{}{"input": newCountry}, 563 } 564 565 gqlResponse := executeRequest(t, graphqlURL, addCountryParams) 566 requireNoGQLErrors(t, gqlResponse) 567 568 var addResult struct { 569 AddState struct { 570 State []*state 571 } 572 AddCountry struct { 573 Country []*country 574 } 575 } 576 err := json.Unmarshal([]byte(gqlResponse.Data), &addResult) 577 require.NoError(t, err) 578 579 require.NotNil(t, addResult) 580 require.NotNil(t, addResult.AddState) 581 require.NotNil(t, addResult.AddCountry) 582 583 // because the two mutations are linked by an XID, the addCountry mutation shouldn't 584 // have created a new state for "XYZ", so the UIDs should be the same 585 require.Equal(t, addResult.AddState.State[0].ID, addResult.AddCountry.Country[0].States[1].ID) 586 587 if diff := cmp.Diff(newCountry, addResult.AddCountry.Country[0], ignoreOpts()...); diff != "" { 588 t.Errorf("result mismatch (-want +got):\n%s", diff) 589 } 590 591 patchSet := &country{ 592 States: []*state{{Code: "DEF", Name: "Definitely A State"}}, 593 } 594 595 patchRemove := &country{ 596 States: []*state{{Code: "XYZ"}}, 597 } 598 599 expectedCountry := &country{ 600 Name: "A Country", 601 States: []*state{newCountry.States[0], patchSet.States[0]}, 602 } 603 604 updateCountryParams := &GraphQLParams{ 605 Query: `mutation updateCountry($id: ID!, $set: CountryPatch!, $remove: CountryPatch!) { 606 addState(input: [{ xcode: "DEF", name: "Definitely A State" }]) { 607 state { id } 608 } 609 610 updateCountry( 611 input: { 612 filter: {id: [$id]}, 613 set: $set, 614 remove: $remove 615 } 616 ) { 617 country { 618 id 619 name 620 states(order: { asc: xcode }) { 621 id 622 xcode 623 name 624 } 625 } 626 } 627 }`, 628 Variables: map[string]interface{}{ 629 "id": addResult.AddCountry.Country[0].ID, 630 "set": patchSet, 631 "remove": patchRemove, 632 }, 633 } 634 635 gqlResponse = executeRequest(t, graphqlURL, updateCountryParams) 636 requireNoGQLErrors(t, gqlResponse) 637 638 var updResult struct { 639 AddState struct { 640 State []*state 641 } 642 UpdateCountry struct { 643 Country []*country 644 } 645 } 646 err = json.Unmarshal([]byte(gqlResponse.Data), &updResult) 647 require.NoError(t, err) 648 require.Len(t, updResult.UpdateCountry.Country, 1) 649 650 if diff := 651 cmp.Diff(expectedCountry, updResult.UpdateCountry.Country[0], ignoreOpts()...); diff != "" { 652 t.Errorf("result mismatch (-want +got):\n%s", diff) 653 } 654 655 requireCountry(t, addResult.AddCountry.Country[0].ID, expectedCountry, true, executeRequest) 656 657 // The "XYZ" state should have its country set back to null like it was before it was 658 // linked to the country 659 requireState(t, addResult.AddState.State[0].ID, addResult.AddState.State[0], executeRequest) 660 661 // No need to cleanup states ATM because, beyond this test, 662 // there's no queries that rely on them 663 cleanUp(t, []*country{addResult.AddCountry.Country[0]}, []*author{}, []*post{}) 664 } 665 666 func addPost(t *testing.T, authorID, countryID string, 667 executeRequest requestExecutor) *post { 668 669 addPostParams := &GraphQLParams{ 670 Query: `mutation addPost($post: AddPostInput!) { 671 addPost(input: [$post]) { 672 post { 673 postID 674 title 675 text 676 isPublished 677 tags 678 numLikes 679 author { 680 id 681 name 682 country { 683 id 684 name 685 } 686 } 687 } 688 } 689 }`, 690 Variables: map[string]interface{}{"post": map[string]interface{}{ 691 "title": "Test Post", 692 "text": "This post is just a test.", 693 "isPublished": true, 694 "numLikes": 1000, 695 "tags": []string{"example", "test"}, 696 "author": map[string]interface{}{"id": authorID}, 697 }}, 698 } 699 700 addPostExpected := fmt.Sprintf(`{ "addPost": { 701 "post": [{ 702 "postID": "_UID_", 703 "title": "Test Post", 704 "text": "This post is just a test.", 705 "isPublished": true, 706 "tags": ["example", "test"], 707 "numLikes": 1000, 708 "author": { 709 "id": "%s", 710 "name": "Test Author", 711 "country": { 712 "id": "%s", 713 "name": "Testland" 714 } 715 } 716 }] 717 } }`, authorID, countryID) 718 719 gqlResponse := executeRequest(t, graphqlURL, addPostParams) 720 requireNoGQLErrors(t, gqlResponse) 721 722 var expected, result struct { 723 AddPost struct { 724 Post []*post 725 } 726 } 727 err := json.Unmarshal([]byte(addPostExpected), &expected) 728 require.NoError(t, err) 729 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 730 require.NoError(t, err) 731 732 requireUID(t, result.AddPost.Post[0].PostID) 733 734 opt := cmpopts.IgnoreFields(post{}, "PostID") 735 if diff := cmp.Diff(expected, result, opt); diff != "" { 736 t.Errorf("result mismatch (-want +got):\n%s", diff) 737 } 738 739 return result.AddPost.Post[0] 740 } 741 742 func requirePost( 743 t *testing.T, 744 postID string, 745 expectedPost *post, 746 getAuthor bool, 747 executeRequest requestExecutor) { 748 749 params := &GraphQLParams{ 750 Query: `query getPost($id: ID!, $getAuthor: Boolean!) { 751 getPost(postID: $id) { 752 postID 753 title 754 text 755 isPublished 756 tags 757 numLikes 758 author @include(if: $getAuthor) { 759 id 760 name 761 country { 762 id 763 name 764 } 765 } 766 } 767 }`, 768 Variables: map[string]interface{}{ 769 "id": postID, 770 "getAuthor": getAuthor, 771 }, 772 } 773 774 gqlResponse := executeRequest(t, graphqlURL, params) 775 requireNoGQLErrors(t, gqlResponse) 776 777 var result struct { 778 GetPost *post 779 } 780 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 781 require.NoError(t, err) 782 783 if diff := cmp.Diff(expectedPost, result.GetPost); diff != "" { 784 t.Errorf("result mismatch (-want +got):\n%s", diff) 785 } 786 } 787 788 func updateMutationByIds(t *testing.T) { 789 newCountry := addCountry(t, postExecutor) 790 anotherCountry := addCountry(t, postExecutor) 791 792 t.Run("update Country", func(t *testing.T) { 793 filter := map[string]interface{}{ 794 "id": []string{newCountry.ID, anotherCountry.ID}, 795 } 796 newName := "updated name" 797 updateCountry(t, filter, newName, true) 798 newCountry.Name = newName 799 anotherCountry.Name = newName 800 801 requireCountry(t, newCountry.ID, newCountry, false, postExecutor) 802 requireCountry(t, anotherCountry.ID, anotherCountry, false, postExecutor) 803 }) 804 805 cleanUp(t, []*country{newCountry, anotherCountry}, []*author{}, []*post{}) 806 } 807 808 func nameRegexFilter(name string) map[string]interface{} { 809 return map[string]interface{}{ 810 "name": map[string]interface{}{ 811 "regexp": "/" + name + "/", 812 }, 813 } 814 } 815 816 func updateMutationByName(t *testing.T) { 817 // Create two countries, update name of the first. Then do a conditional mutation which 818 // should only update the name of the second country. 819 newCountry := addCountry(t, postExecutor) 820 t.Run("update Country", func(t *testing.T) { 821 filter := nameRegexFilter(newCountry.Name) 822 newName := "updated name" 823 updateCountry(t, filter, newName, true) 824 newCountry.Name = newName 825 requireCountry(t, newCountry.ID, newCountry, false, postExecutor) 826 }) 827 828 anotherCountry := addCountry(t, postExecutor) 829 // Update name for country where name is anotherCountry.Name 830 t.Run("update country by name", func(t *testing.T) { 831 filter := nameRegexFilter(anotherCountry.Name) 832 anotherCountry.Name = "updated another country name" 833 updateCountry(t, filter, anotherCountry.Name, true) 834 }) 835 836 t.Run("check updated Country", func(t *testing.T) { 837 // newCountry should not have been updated. 838 requireCountry(t, newCountry.ID, newCountry, false, postExecutor) 839 requireCountry(t, anotherCountry.ID, anotherCountry, false, postExecutor) 840 }) 841 842 cleanUp(t, []*country{newCountry, anotherCountry}, []*author{}, []*post{}) 843 } 844 845 func updateMutationByNameNoMatch(t *testing.T) { 846 // The countries shouldn't get updated as the query shouldn't match any nodes. 847 newCountry := addCountry(t, postExecutor) 848 anotherCountry := addCountry(t, postExecutor) 849 t.Run("update Country", func(t *testing.T) { 850 filter := nameRegexFilter("no match") 851 updateCountry(t, filter, "new name", false) 852 requireCountry(t, newCountry.ID, newCountry, false, postExecutor) 853 requireCountry(t, anotherCountry.ID, anotherCountry, false, postExecutor) 854 }) 855 856 cleanUp(t, []*country{newCountry, anotherCountry}, []*author{}, []*post{}) 857 } 858 859 func updateDelete(t *testing.T) { 860 newCountry := addCountry(t, postExecutor) 861 newAuthor := addAuthor(t, newCountry.ID, postExecutor) 862 newPost := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) 863 864 filter := map[string]interface{}{ 865 "postID": []string{newPost.PostID}, 866 } 867 delPatch := map[string]interface{}{ 868 "text": "This post is just a test.", 869 "isPublished": nil, 870 "tags": []string{"test", "notatag"}, 871 "numLikes": 999, 872 } 873 874 updateParams := &GraphQLParams{ 875 Query: `mutation updPost($filter: PostFilter!, $del: PostPatch!) { 876 updatePost(input: { filter: $filter, remove: $del }) { 877 post { 878 text 879 isPublished 880 tags 881 numLikes 882 } 883 } 884 }`, 885 Variables: map[string]interface{}{"filter": filter, "del": delPatch}, 886 } 887 888 gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) 889 requireNoGQLErrors(t, gqlResponse) 890 891 require.JSONEq(t, `{ 892 "updatePost": { 893 "post": [ 894 { 895 "text": null, 896 "isPublished": null, 897 "tags": ["example"], 898 "numLikes": 1000 899 } 900 ] 901 } 902 }`, 903 string([]byte(gqlResponse.Data))) 904 905 newPost.Text = "" // was deleted because the given val was correct 906 newPost.Tags = []string{"example"} // the intersection of the tags was deleted 907 newPost.IsPublished = false // must have been deleted because was set to nil in the patch 908 // newPost.NumLikes stays the same because the value in the patch was wrong 909 requirePost(t, newPost.PostID, newPost, true, postExecutor) 910 911 cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{newPost}) 912 } 913 914 func updateCountry(t *testing.T, filter map[string]interface{}, newName string, shouldUpdate bool) { 915 updateParams := &GraphQLParams{ 916 Query: `mutation newName($filter: CountryFilter!, $newName: String!) { 917 updateCountry(input: { filter: $filter, set: { name: $newName } }) { 918 country { 919 id 920 name 921 } 922 } 923 }`, 924 Variables: map[string]interface{}{"filter": filter, "newName": newName}, 925 } 926 927 gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) 928 requireNoGQLErrors(t, gqlResponse) 929 930 var result struct { 931 UpdateCountry struct { 932 Country []*country 933 } 934 } 935 936 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 937 require.NoError(t, err) 938 if shouldUpdate { 939 require.NotEqual(t, 0, len(result.UpdateCountry.Country)) 940 } 941 for _, c := range result.UpdateCountry.Country { 942 require.NotNil(t, c.ID) 943 require.Equal(t, newName, c.Name) 944 } 945 } 946 947 func filterInUpdate(t *testing.T) { 948 countries := make([]country, 0, 4) 949 for i := 0; i < 4; i++ { 950 country := addCountry(t, postExecutor) 951 country.Name = "updatedValue" 952 countries = append(countries, *country) 953 } 954 countries[3].Name = "Testland" 955 956 cases := map[string]struct { 957 Filter map[string]interface{} 958 FilterCountries map[string]interface{} 959 Expected int 960 Countries []*country 961 }{ 962 "Eq filter": { 963 Filter: map[string]interface{}{ 964 "name": map[string]interface{}{ 965 "eq": "Testland", 966 }, 967 "and": map[string]interface{}{ 968 "id": []string{countries[0].ID, countries[1].ID}, 969 }, 970 }, 971 FilterCountries: map[string]interface{}{ 972 "id": []string{countries[1].ID}, 973 }, 974 Expected: 1, 975 Countries: []*country{&countries[0], &countries[1]}, 976 }, 977 978 "ID Filter": { 979 Filter: map[string]interface{}{ 980 "id": []string{countries[2].ID}, 981 }, 982 FilterCountries: map[string]interface{}{ 983 "id": []string{countries[2].ID, countries[3].ID}, 984 }, 985 Expected: 1, 986 Countries: []*country{&countries[2], &countries[3]}, 987 }, 988 } 989 990 for name, test := range cases { 991 t.Run(name, func(t *testing.T) { 992 updateParams := &GraphQLParams{ 993 Query: `mutation newName($filter: CountryFilter!, $newName: String!, 994 $filterCountries: CountryFilter!) { 995 updateCountry(input: { filter: $filter, set: { name: $newName } }) { 996 country(filter: $filterCountries) { 997 id 998 name 999 } 1000 } 1001 }`, 1002 Variables: map[string]interface{}{ 1003 "filter": test.Filter, 1004 "newName": "updatedValue", 1005 "filterCountries": test.FilterCountries, 1006 }, 1007 } 1008 1009 gqlResponse := updateParams.ExecuteAsPost(t, graphqlURL) 1010 requireNoGQLErrors(t, gqlResponse) 1011 1012 var result struct { 1013 UpdateCountry struct { 1014 Country []*country 1015 } 1016 } 1017 1018 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 1019 require.NoError(t, err) 1020 1021 require.Equal(t, len(result.UpdateCountry.Country), test.Expected) 1022 for i := 0; i < test.Expected; i++ { 1023 require.Equal(t, result.UpdateCountry.Country[i].Name, "updatedValue") 1024 } 1025 1026 for _, country := range test.Countries { 1027 requireCountry(t, country.ID, country, false, postExecutor) 1028 } 1029 cleanUp(t, test.Countries, nil, nil) 1030 }) 1031 } 1032 } 1033 1034 func deleteMutationWithMultipleIds(t *testing.T) { 1035 country := addCountry(t, postExecutor) 1036 anotherCountry := addCountry(t, postExecutor) 1037 t.Run("delete Country", func(t *testing.T) { 1038 deleteCountryExpected := `{"deleteCountry" : { "msg": "Deleted" } }` 1039 filter := map[string]interface{}{"id": []string{country.ID, anotherCountry.ID}} 1040 deleteCountry(t, filter, deleteCountryExpected, nil) 1041 }) 1042 1043 t.Run("check Country is deleted", func(t *testing.T) { 1044 requireCountry(t, country.ID, nil, false, postExecutor) 1045 requireCountry(t, anotherCountry.ID, nil, false, postExecutor) 1046 }) 1047 } 1048 1049 func deleteMutationWithSingleID(t *testing.T) { 1050 newCountry := addCountry(t, postExecutor) 1051 anotherCountry := addCountry(t, postExecutor) 1052 t.Run("delete Country", func(t *testing.T) { 1053 deleteCountryExpected := `{"deleteCountry" : { "msg": "Deleted" } }` 1054 filter := map[string]interface{}{"id": []string{newCountry.ID}} 1055 deleteCountry(t, filter, deleteCountryExpected, nil) 1056 }) 1057 1058 // In this case anotherCountry shouldn't be deleted. 1059 t.Run("check Country is deleted", func(t *testing.T) { 1060 requireCountry(t, newCountry.ID, nil, false, postExecutor) 1061 requireCountry(t, anotherCountry.ID, anotherCountry, false, postExecutor) 1062 }) 1063 cleanUp(t, []*country{anotherCountry}, nil, nil) 1064 } 1065 1066 func deleteMutationByName(t *testing.T) { 1067 newCountry := addCountry(t, postExecutor) 1068 anotherCountry := addCountry(t, postExecutor) 1069 anotherCountry.Name = "New country" 1070 filter := map[string]interface{}{ 1071 "id": []string{anotherCountry.ID}, 1072 } 1073 updateCountry(t, filter, anotherCountry.Name, true) 1074 1075 deleteCountryExpected := `{"deleteCountry" : { "msg": "Deleted" } }` 1076 t.Run("delete Country", func(t *testing.T) { 1077 filter := map[string]interface{}{ 1078 "name": map[string]interface{}{ 1079 "regexp": "/" + newCountry.Name + "/", 1080 }, 1081 } 1082 deleteCountry(t, filter, deleteCountryExpected, nil) 1083 }) 1084 1085 // In this case anotherCountry shouldn't be deleted. 1086 t.Run("check Country is deleted", func(t *testing.T) { 1087 requireCountry(t, newCountry.ID, nil, false, postExecutor) 1088 requireCountry(t, anotherCountry.ID, anotherCountry, false, postExecutor) 1089 }) 1090 cleanUp(t, []*country{anotherCountry}, nil, nil) 1091 } 1092 1093 func deleteCountry( 1094 t *testing.T, 1095 filter map[string]interface{}, 1096 deleteCountryExpected string, 1097 expectedErrors x.GqlErrorList) { 1098 1099 deleteCountryParams := &GraphQLParams{ 1100 Query: `mutation deleteCountry($filter: CountryFilter!) { 1101 deleteCountry(filter: $filter) { msg } 1102 }`, 1103 Variables: map[string]interface{}{"filter": filter}, 1104 } 1105 1106 gqlResponse := deleteCountryParams.ExecuteAsPost(t, graphqlURL) 1107 require.JSONEq(t, deleteCountryExpected, string(gqlResponse.Data)) 1108 1109 if diff := cmp.Diff(expectedErrors, gqlResponse.Errors); diff != "" { 1110 t.Errorf("errors mismatch (-want +got):\n%s", diff) 1111 } 1112 } 1113 1114 func deleteAuthor( 1115 t *testing.T, 1116 authorID string, 1117 deleteAuthorExpected string, 1118 expectedErrors x.GqlErrorList) { 1119 1120 deleteAuthorParams := &GraphQLParams{ 1121 Query: `mutation deleteAuthor($filter: AuthorFilter!) { 1122 deleteAuthor(filter: $filter) { msg } 1123 }`, 1124 Variables: map[string]interface{}{ 1125 "filter": map[string]interface{}{ 1126 "id": []string{authorID}, 1127 }, 1128 }, 1129 } 1130 1131 gqlResponse := deleteAuthorParams.ExecuteAsPost(t, graphqlURL) 1132 1133 require.JSONEq(t, deleteAuthorExpected, string(gqlResponse.Data)) 1134 1135 if diff := cmp.Diff(expectedErrors, gqlResponse.Errors); diff != "" { 1136 t.Errorf("errors mismatch (-want +got):\n%s", diff) 1137 } 1138 } 1139 1140 func deletePost( 1141 t *testing.T, 1142 postID string, 1143 deletePostExpected string, 1144 expectedErrors x.GqlErrorList) { 1145 1146 deletePostParams := &GraphQLParams{ 1147 Query: `mutation deletePost($filter: PostFilter!) { 1148 deletePost(filter: $filter) { msg } 1149 }`, 1150 Variables: map[string]interface{}{"filter": map[string]interface{}{ 1151 "postID": []string{postID}, 1152 }}, 1153 } 1154 1155 gqlResponse := deletePostParams.ExecuteAsPost(t, graphqlURL) 1156 1157 require.JSONEq(t, deletePostExpected, string(gqlResponse.Data)) 1158 1159 if diff := cmp.Diff(expectedErrors, gqlResponse.Errors); diff != "" { 1160 t.Errorf("errors mismatch (-want +got):\n%s", diff) 1161 } 1162 } 1163 1164 func deleteWrongID(t *testing.T) { 1165 t.Skip() 1166 // Skipping the test for now because wrong type of node while deleting is not an error. 1167 // After Dgraph returns the number of nodes modified from upsert, modify this test to check 1168 // count of nodes modified is 0. 1169 // 1170 // FIXME: Test cases : with a wrongID, a malformed ID "blah", and maybe a filter that 1171 // doesn't match anything. 1172 newCountry := addCountry(t, postExecutor) 1173 newAuthor := addAuthor(t, newCountry.ID, postExecutor) 1174 1175 expectedData := `{ "deleteCountry": null }` 1176 expectedErrors := x.GqlErrorList{ 1177 &x.GqlError{Message: `input: couldn't complete deleteCountry because ` + 1178 fmt.Sprintf(`input: Node with id %s is not of type Country`, newAuthor.ID)}} 1179 1180 filter := map[string]interface{}{"id": []string{newAuthor.ID}} 1181 deleteCountry(t, filter, expectedData, expectedErrors) 1182 1183 cleanUp(t, []*country{newCountry}, []*author{newAuthor}, []*post{}) 1184 } 1185 1186 func manyMutations(t *testing.T) { 1187 newCountry := addCountry(t, postExecutor) 1188 multiMutationParams := &GraphQLParams{ 1189 Query: `mutation addCountries($name1: String!, $filter: CountryFilter!, $name2: String!) { 1190 add1: addCountry(input: [{ name: $name1 }]) { 1191 country { 1192 id 1193 name 1194 } 1195 } 1196 1197 deleteCountry(filter: $filter) { msg } 1198 1199 add2: addCountry(input: [{ name: $name2 }]) { 1200 country { 1201 id 1202 name 1203 } 1204 } 1205 }`, 1206 Variables: map[string]interface{}{ 1207 "name1": "Testland1", "filter": map[string]interface{}{ 1208 "id": []string{newCountry.ID}}, "name2": "Testland2"}, 1209 } 1210 multiMutationExpected := `{ 1211 "add1": { "country": [{ "id": "_UID_", "name": "Testland1" }] }, 1212 "deleteCountry" : { "msg": "Deleted" }, 1213 "add2": { "country": [{ "id": "_UID_", "name": "Testland2" }] } 1214 }` 1215 1216 gqlResponse := multiMutationParams.ExecuteAsPost(t, graphqlURL) 1217 requireNoGQLErrors(t, gqlResponse) 1218 1219 var expected, result struct { 1220 Add1 struct { 1221 Country []*country 1222 } 1223 DeleteCountry struct { 1224 Msg string 1225 } 1226 Add2 struct { 1227 Country []*country 1228 } 1229 } 1230 err := json.Unmarshal([]byte(multiMutationExpected), &expected) 1231 require.NoError(t, err) 1232 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 1233 require.NoError(t, err) 1234 1235 opt := cmpopts.IgnoreFields(country{}, "ID") 1236 if diff := cmp.Diff(expected, result, opt); diff != "" { 1237 t.Errorf("result mismatch (-want +got):\n%s", diff) 1238 } 1239 1240 t.Run("country deleted", func(t *testing.T) { 1241 requireCountry(t, newCountry.ID, nil, false, postExecutor) 1242 }) 1243 1244 cleanUp(t, append(result.Add1.Country, result.Add2.Country...), []*author{}, []*post{}) 1245 } 1246 1247 func testSelectionInAddObject(t *testing.T) { 1248 newCountry := addCountry(t, postExecutor) 1249 newAuth := addAuthor(t, newCountry.ID, postExecutor) 1250 1251 post1 := &post{ 1252 Title: "Test1", 1253 Author: newAuth, 1254 } 1255 1256 post2 := &post{ 1257 Title: "Test2", 1258 Author: newAuth, 1259 } 1260 1261 cases := map[string]struct { 1262 Filter map[string]interface{} 1263 First int 1264 Offset int 1265 Sort map[string]interface{} 1266 Expected []*post 1267 }{ 1268 "Pagination": { 1269 First: 1, 1270 Offset: 1, 1271 Sort: map[string]interface{}{ 1272 "desc": "title", 1273 }, 1274 Expected: []*post{post1}, 1275 }, 1276 "Filter": { 1277 Filter: map[string]interface{}{ 1278 "title": map[string]interface{}{ 1279 "anyoftext": "Test1", 1280 }, 1281 }, 1282 Expected: []*post{post1}, 1283 }, 1284 "Sort": { 1285 Sort: map[string]interface{}{ 1286 "desc": "title", 1287 }, 1288 Expected: []*post{post2, post1}, 1289 }, 1290 } 1291 1292 for name, test := range cases { 1293 t.Run(name, func(t *testing.T) { 1294 addPostParams := &GraphQLParams{ 1295 Query: `mutation addPost($posts: [AddPostInput!]!, $filter: 1296 PostFilter, $first: Int, $offset: Int, $sort: PostOrder) { 1297 addPost(input: $posts) { 1298 post (first:$first, offset:$offset, filter:$filter, order:$sort){ 1299 postID 1300 title 1301 } 1302 } 1303 }`, 1304 Variables: map[string]interface{}{ 1305 "posts": []*post{post1, post2}, 1306 "first": test.First, 1307 "offset": test.Offset, 1308 "sort": test.Sort, 1309 "filter": test.Filter, 1310 }, 1311 } 1312 1313 gqlResponse := postExecutor(t, graphqlURL, addPostParams) 1314 requireNoGQLErrors(t, gqlResponse) 1315 var result struct { 1316 AddPost struct { 1317 Post []*post 1318 } 1319 } 1320 1321 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 1322 require.NoError(t, err) 1323 1324 opt := cmpopts.IgnoreFields(post{}, "PostID", "Author") 1325 if diff := cmp.Diff(test.Expected, result.AddPost.Post, opt); diff != "" { 1326 t.Errorf("result mismatch (-want +got):\n%s", diff) 1327 } 1328 1329 cleanUp(t, []*country{}, []*author{}, result.AddPost.Post) 1330 }) 1331 1332 } 1333 1334 cleanUp(t, []*country{newCountry}, []*author{newAuth}, []*post{}) 1335 1336 } 1337 1338 // After a successful mutation, the following query is executed. That query can 1339 // contain any depth or filtering that makes sense for the schema. 1340 // 1341 // I this case, we set up an author with existing posts, then add another post. 1342 // The filter is down inside post->author->posts and finds just one of the 1343 // author's posts. 1344 func mutationWithDeepFilter(t *testing.T) { 1345 1346 newCountry := addCountry(t, postExecutor) 1347 newAuthor := addAuthor(t, newCountry.ID, postExecutor) 1348 1349 // Make sure they have a post not found by the filter 1350 newPost := addPost(t, newAuthor.ID, newCountry.ID, postExecutor) 1351 1352 addPostParams := &GraphQLParams{ 1353 Query: `mutation addPost($post: AddPostInput!) { 1354 addPost(input: [$post]) { 1355 post { 1356 postID 1357 author { 1358 posts(filter: { title: { allofterms: "find me" }}) { 1359 title 1360 } 1361 } 1362 } 1363 } 1364 }`, 1365 Variables: map[string]interface{}{"post": map[string]interface{}{ 1366 "title": "find me : a test of deep search after mutation", 1367 "author": map[string]interface{}{"id": newAuthor.ID}, 1368 }}, 1369 } 1370 1371 // Expect the filter to find just the new post, not any of the author's existing posts. 1372 addPostExpected := `{ "addPost": { 1373 "post": [{ 1374 "postID": "_UID_", 1375 "author": { 1376 "posts": [ { "title": "find me : a test of deep search after mutation" } ] 1377 } 1378 }] 1379 } }` 1380 1381 gqlResponse := addPostParams.ExecuteAsPost(t, graphqlURL) 1382 requireNoGQLErrors(t, gqlResponse) 1383 1384 var expected, result struct { 1385 AddPost struct { 1386 Post []*post 1387 } 1388 } 1389 err := json.Unmarshal([]byte(addPostExpected), &expected) 1390 require.NoError(t, err) 1391 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 1392 require.NoError(t, err) 1393 1394 requireUID(t, result.AddPost.Post[0].PostID) 1395 1396 opt := cmpopts.IgnoreFields(post{}, "PostID") 1397 if diff := cmp.Diff(expected, result, opt); diff != "" { 1398 t.Errorf("result mismatch (-want +got):\n%s", diff) 1399 } 1400 1401 cleanUp(t, []*country{newCountry}, []*author{newAuthor}, 1402 []*post{newPost, result.AddPost.Post[0]}) 1403 } 1404 1405 // TestManyMutationsWithQueryError : If there are multiple mutations and an error 1406 // occurs in the mutation, then then following mutations aren't executed. That's 1407 // tested by TestManyMutationsWithError in the resolver tests. 1408 // 1409 // However, there can also be an error in the query following a mutation, but 1410 // that shouldn't stop the following mutations because the actual mutation 1411 // went through without error. 1412 func manyMutationsWithQueryError(t *testing.T) { 1413 newCountry := addCountry(t, postExecutor) 1414 1415 // delete the country's name. 1416 // The schema states type Country `{ ... name: String! ... }` 1417 // so a query error will be raised if we ask for the country's name in a 1418 // query. Don't think a GraphQL update can do this ATM, so do through Dgraph. 1419 d, err := grpc.Dial(alphagRPC, grpc.WithInsecure()) 1420 require.NoError(t, err) 1421 client := dgo.NewDgraphClient(api.NewDgraphClient(d)) 1422 mu := &api.Mutation{ 1423 CommitNow: true, 1424 DelNquads: []byte(fmt.Sprintf("<%s> <Country.name> * .", newCountry.ID)), 1425 } 1426 _, err = client.NewTxn().Mutate(context.Background(), mu) 1427 require.NoError(t, err) 1428 1429 // add1 - should succeed 1430 // add2 - should succeed and also return an error (country doesn't have a name) 1431 // add3 - should succeed 1432 multiMutationParams := &GraphQLParams{ 1433 Query: `mutation addCountries($countryID: ID!) { 1434 add1: addAuthor(input: [{ name: "A. N. Author", country: { id: $countryID }}]) { 1435 author { 1436 id 1437 name 1438 country { 1439 id 1440 } 1441 } 1442 } 1443 1444 add2: addAuthor(input: [{ name: "Ann Other Author", country: { id: $countryID }}]) { 1445 author { 1446 id 1447 name 1448 country { 1449 id 1450 name 1451 } 1452 } 1453 } 1454 1455 add3: addCountry(input: [{ name: "abc" }]) { 1456 country { 1457 id 1458 name 1459 } 1460 } 1461 }`, 1462 Variables: map[string]interface{}{"countryID": newCountry.ID}, 1463 } 1464 expectedData := fmt.Sprintf(`{ 1465 "add1": { "author": [{ "id": "_UID_", "name": "A. N. Author", "country": { "id": "%s" } }] }, 1466 "add2": { "author": [{ "id": "_UID_", "name": "Ann Other Author", "country": null }] }, 1467 "add3": { "country": [{ "id": "_UID_", "name": "abc" }] } 1468 }`, newCountry.ID) 1469 1470 expectedErrors := x.GqlErrorList{ 1471 &x.GqlError{Message: `Non-nullable field 'name' (type String!) was not present ` + 1472 `in result from Dgraph. GraphQL error propagation triggered.`, 1473 Locations: []x.Location{{Line: 18, Column: 7}}, 1474 Path: []interface{}{"add2", "author", float64(0), "country", "name"}}} 1475 1476 gqlResponse := multiMutationParams.ExecuteAsPost(t, graphqlURL) 1477 1478 if diff := cmp.Diff(expectedErrors, gqlResponse.Errors); diff != "" { 1479 t.Errorf("errors mismatch (-want +got):\n%s", diff) 1480 } 1481 1482 var expected, result struct { 1483 Add1 struct { 1484 Author []*author 1485 } 1486 Add2 struct { 1487 Author []*author 1488 } 1489 Add3 struct { 1490 Country []*country 1491 } 1492 } 1493 err = json.Unmarshal([]byte(expectedData), &expected) 1494 require.NoError(t, err) 1495 1496 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 1497 require.NoError(t, err) 1498 1499 opt1 := cmpopts.IgnoreFields(author{}, "ID") 1500 opt2 := cmpopts.IgnoreFields(country{}, "ID") 1501 if diff := cmp.Diff(expected, result, opt1, opt2); diff != "" { 1502 t.Errorf("result mismatch (-want +got):\n%s", diff) 1503 } 1504 1505 cleanUp(t, 1506 []*country{newCountry, result.Add3.Country[0]}, 1507 []*author{result.Add1.Author[0], result.Add2.Author[0]}, 1508 []*post{}) 1509 } 1510 1511 func cleanUp(t *testing.T, countries []*country, authors []*author, posts []*post) { 1512 t.Run("cleaning up", func(t *testing.T) { 1513 for _, post := range posts { 1514 deletePost(t, post.PostID, `{"deletePost" : { "msg": "Deleted" } }`, nil) 1515 } 1516 1517 for _, author := range authors { 1518 deleteAuthor(t, author.ID, `{"deleteAuthor" : { "msg": "Deleted" } }`, nil) 1519 } 1520 1521 for _, country := range countries { 1522 filter := map[string]interface{}{"id": []string{country.ID}} 1523 deleteCountry(t, filter, `{"deleteCountry" : { "msg": "Deleted" } }`, nil) 1524 } 1525 }) 1526 } 1527 1528 type starship struct { 1529 ID string `json:"id"` 1530 Name string `json:"name"` 1531 Length float64 `json:"length"` 1532 } 1533 1534 func addStarship(t *testing.T) *starship { 1535 addStarshipParams := &GraphQLParams{ 1536 Query: `mutation addStarship($starship: AddStarshipInput!) { 1537 addStarship(input: [$starship]) { 1538 starship { 1539 id 1540 name 1541 length 1542 } 1543 } 1544 }`, 1545 Variables: map[string]interface{}{"starship": map[string]interface{}{ 1546 "name": "Millennium Falcon", 1547 "length": 2, 1548 }}, 1549 } 1550 1551 gqlResponse := addStarshipParams.ExecuteAsPost(t, graphqlURL) 1552 requireNoGQLErrors(t, gqlResponse) 1553 1554 addStarshipExpected := fmt.Sprintf(`{"addStarship":{ 1555 "starship":[{ 1556 "name":"Millennium Falcon", 1557 "length":2 1558 }] 1559 }}`) 1560 1561 var expected, result struct { 1562 AddStarship struct { 1563 Starship []*starship 1564 } 1565 } 1566 err := json.Unmarshal([]byte(addStarshipExpected), &expected) 1567 require.NoError(t, err) 1568 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 1569 require.NoError(t, err) 1570 1571 requireUID(t, result.AddStarship.Starship[0].ID) 1572 1573 opt := cmpopts.IgnoreFields(starship{}, "ID") 1574 if diff := cmp.Diff(expected, result, opt); diff != "" { 1575 t.Errorf("result mismatch (-want +got):\n%s", diff) 1576 } 1577 1578 return result.AddStarship.Starship[0] 1579 } 1580 1581 func addHuman(t *testing.T, starshipID string) string { 1582 addHumanParams := &GraphQLParams{ 1583 Query: `mutation addHuman($human: AddHumanInput!) { 1584 addHuman(input: [$human]) { 1585 human { 1586 id 1587 } 1588 } 1589 }`, 1590 Variables: map[string]interface{}{"human": map[string]interface{}{ 1591 "name": "Han", 1592 "ename": "Han_employee", 1593 "totalCredits": 10, 1594 "appearsIn": []string{"EMPIRE"}, 1595 "starships": []map[string]interface{}{{ 1596 "id": starshipID, 1597 }}, 1598 }}, 1599 } 1600 1601 gqlResponse := addHumanParams.ExecuteAsPost(t, graphqlURL) 1602 requireNoGQLErrors(t, gqlResponse) 1603 1604 var result struct { 1605 AddHuman struct { 1606 Human []struct { 1607 ID string 1608 } 1609 } 1610 } 1611 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 1612 require.NoError(t, err) 1613 1614 requireUID(t, result.AddHuman.Human[0].ID) 1615 return result.AddHuman.Human[0].ID 1616 } 1617 1618 func addDroid(t *testing.T) string { 1619 addDroidParams := &GraphQLParams{ 1620 Query: `mutation addDroid($droid: AddDroidInput!) { 1621 addDroid(input: [$droid]) { 1622 droid { 1623 id 1624 } 1625 } 1626 }`, 1627 Variables: map[string]interface{}{"droid": map[string]interface{}{ 1628 "name": "R2-D2", 1629 "primaryFunction": "Robot", 1630 "appearsIn": []string{"EMPIRE"}, 1631 }}, 1632 } 1633 1634 gqlResponse := addDroidParams.ExecuteAsPost(t, graphqlURL) 1635 requireNoGQLErrors(t, gqlResponse) 1636 1637 var result struct { 1638 AddDroid struct { 1639 Droid []struct { 1640 ID string 1641 } 1642 } 1643 } 1644 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 1645 require.NoError(t, err) 1646 1647 requireUID(t, result.AddDroid.Droid[0].ID) 1648 return result.AddDroid.Droid[0].ID 1649 } 1650 1651 func updateCharacter(t *testing.T, id string) { 1652 updateCharacterParams := &GraphQLParams{ 1653 Query: `mutation updateCharacter($character: UpdateCharacterInput!) { 1654 updateCharacter(input: $character) { 1655 character { 1656 name 1657 } 1658 } 1659 }`, 1660 Variables: map[string]interface{}{"character": map[string]interface{}{ 1661 "filter": map[string]interface{}{ 1662 "id": []string{id}, 1663 }, 1664 "set": map[string]interface{}{ 1665 "name": "Han Solo", 1666 }, 1667 }}, 1668 } 1669 1670 gqlResponse := updateCharacterParams.ExecuteAsPost(t, graphqlURL) 1671 requireNoGQLErrors(t, gqlResponse) 1672 } 1673 1674 func queryInterfaceAfterAddMutation(t *testing.T) { 1675 newStarship := addStarship(t) 1676 humanID := addHuman(t, newStarship.ID) 1677 droidID := addDroid(t) 1678 updateCharacter(t, humanID) 1679 1680 t.Run("test query all characters", func(t *testing.T) { 1681 queryCharacterParams := &GraphQLParams{ 1682 Query: `query { 1683 queryCharacter { 1684 name 1685 appearsIn 1686 ... on Human { 1687 starships { 1688 name 1689 length 1690 } 1691 totalCredits 1692 } 1693 ... on Droid { 1694 primaryFunction 1695 } 1696 } 1697 }`, 1698 } 1699 1700 gqlResponse := queryCharacterParams.ExecuteAsPost(t, graphqlURL) 1701 requireNoGQLErrors(t, gqlResponse) 1702 1703 expected := `{ 1704 "queryCharacter": [ 1705 { 1706 "name": "Han Solo", 1707 "appearsIn": ["EMPIRE"], 1708 "starships": [ 1709 { 1710 "name": "Millennium Falcon", 1711 "length": 2 1712 } 1713 ], 1714 "totalCredits": 10 1715 }, 1716 { 1717 "name": "R2-D2", 1718 "appearsIn": ["EMPIRE"], 1719 "primaryFunction": "Robot" 1720 } 1721 ] 1722 }` 1723 1724 testutil.CompareJSON(t, expected, string(gqlResponse.Data)) 1725 }) 1726 1727 t.Run("test query characters by name", func(t *testing.T) { 1728 queryCharacterByNameParams := &GraphQLParams{ 1729 Query: `query { 1730 queryCharacter(filter: { name: { eq: "Han Solo" } }) { 1731 name 1732 appearsIn 1733 ... on Human { 1734 starships { 1735 name 1736 length 1737 } 1738 totalCredits 1739 } 1740 ... on Droid { 1741 primaryFunction 1742 } 1743 } 1744 }`, 1745 } 1746 1747 gqlResponse := queryCharacterByNameParams.ExecuteAsPost(t, graphqlURL) 1748 requireNoGQLErrors(t, gqlResponse) 1749 1750 expected := `{ 1751 "queryCharacter": [ 1752 { 1753 "name": "Han Solo", 1754 "appearsIn": ["EMPIRE"], 1755 "starships": [ 1756 { 1757 "name": "Millennium Falcon", 1758 "length": 2 1759 } 1760 ], 1761 "totalCredits": 10 1762 } 1763 ] 1764 }` 1765 testutil.CompareJSON(t, expected, string(gqlResponse.Data)) 1766 }) 1767 1768 t.Run("test query all humans", func(t *testing.T) { 1769 queryHumanParams := &GraphQLParams{ 1770 Query: `query { 1771 queryHuman { 1772 name 1773 appearsIn 1774 starships { 1775 name 1776 length 1777 } 1778 totalCredits 1779 } 1780 }`, 1781 } 1782 1783 gqlResponse := queryHumanParams.ExecuteAsPost(t, graphqlURL) 1784 requireNoGQLErrors(t, gqlResponse) 1785 1786 expected := `{ 1787 "queryHuman": [ 1788 { 1789 "name": "Han Solo", 1790 "appearsIn": ["EMPIRE"], 1791 "starships": [ 1792 { 1793 "name": "Millennium Falcon", 1794 "length": 2 1795 } 1796 ], 1797 "totalCredits": 10 1798 } 1799 ] 1800 }` 1801 testutil.CompareJSON(t, expected, string(gqlResponse.Data)) 1802 }) 1803 1804 t.Run("test query humans by name", func(t *testing.T) { 1805 queryHumanParamsByName := &GraphQLParams{ 1806 Query: `query { 1807 queryHuman(filter: { name: { eq: "Han Solo" } }) { 1808 name 1809 appearsIn 1810 starships { 1811 name 1812 length 1813 } 1814 totalCredits 1815 } 1816 }`, 1817 } 1818 1819 gqlResponse := queryHumanParamsByName.ExecuteAsPost(t, graphqlURL) 1820 requireNoGQLErrors(t, gqlResponse) 1821 1822 expected := `{ 1823 "queryHuman": [ 1824 { 1825 "name": "Han Solo", 1826 "appearsIn": ["EMPIRE"], 1827 "starships": [ 1828 { 1829 "name": "Millennium Falcon", 1830 "length": 2 1831 } 1832 ], 1833 "totalCredits": 10 1834 } 1835 ] 1836 }` 1837 1838 testutil.CompareJSON(t, expected, string(gqlResponse.Data)) 1839 }) 1840 1841 cleanupStarwars(t, newStarship.ID, humanID, droidID) 1842 } 1843 1844 func cleanupStarwars(t *testing.T, starshipID, humanID, droidID string) { 1845 // Delete everything 1846 multiMutationParams := &GraphQLParams{ 1847 Query: `mutation cleanup($starshipFilter: StarshipFilter!, $humanFilter: HumanFilter!, 1848 $droidFilter: DroidFilter!) { 1849 deleteStarship(filter: $starshipFilter) { msg } 1850 1851 deleteHuman(filter: $humanFilter) { msg } 1852 1853 deleteDroid(filter: $droidFilter) { msg } 1854 }`, 1855 Variables: map[string]interface{}{ 1856 "starshipFilter": map[string]interface{}{ 1857 "id": []string{starshipID}, 1858 }, 1859 "humanFilter": map[string]interface{}{ 1860 "id": []string{humanID}, 1861 }, 1862 "droidFilter": map[string]interface{}{ 1863 "id": []string{droidID}, 1864 }, 1865 }, 1866 } 1867 multiMutationExpected := `{ 1868 "deleteStarship": { "msg": "Deleted" }, 1869 "deleteHuman" : { "msg": "Deleted" }, 1870 "deleteDroid": { "msg": "Deleted" } 1871 }` 1872 1873 gqlResponse := multiMutationParams.ExecuteAsPost(t, graphqlURL) 1874 requireNoGQLErrors(t, gqlResponse) 1875 1876 var expected, result struct { 1877 DeleteStarhip struct { 1878 Msg string 1879 } 1880 DeleteHuman struct { 1881 Msg string 1882 } 1883 DeleteDroid struct { 1884 Msg string 1885 } 1886 } 1887 1888 err := json.Unmarshal([]byte(multiMutationExpected), &expected) 1889 require.NoError(t, err) 1890 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 1891 require.NoError(t, err) 1892 1893 if diff := cmp.Diff(expected, result); diff != "" { 1894 t.Errorf("result mismatch (-want +got):\n%s", diff) 1895 } 1896 } 1897 1898 func requireState(t *testing.T, uid string, expectedState *state, 1899 executeRequest requestExecutor) { 1900 1901 params := &GraphQLParams{ 1902 Query: `query getState($id: ID!) { 1903 getState(id: $id) { 1904 id 1905 xcode 1906 name 1907 country { 1908 id 1909 name 1910 } 1911 } 1912 }`, 1913 Variables: map[string]interface{}{"id": uid}, 1914 } 1915 gqlResponse := executeRequest(t, graphqlURL, params) 1916 requireNoGQLErrors(t, gqlResponse) 1917 1918 var result struct { 1919 GetState *state 1920 } 1921 err := json.Unmarshal([]byte(gqlResponse.Data), &result) 1922 require.NoError(t, err) 1923 1924 if diff := cmp.Diff(expectedState, result.GetState); diff != "" { 1925 t.Errorf("result mismatch (-want +got):\n%s", diff) 1926 } 1927 } 1928 1929 func addState(t *testing.T, name string, executeRequest requestExecutor) *state { 1930 addStateParams := &GraphQLParams{ 1931 Query: `mutation addState($xcode: String!, $name: String!) { 1932 addState(input: [{ xcode: $xcode, name: $name }]) { 1933 state { 1934 id 1935 xcode 1936 name 1937 } 1938 } 1939 }`, 1940 Variables: map[string]interface{}{"name": name, "xcode": "cal"}, 1941 } 1942 addStateExpected := ` 1943 { "addState": { "state": [{ "id": "_UID_", "name": "` + name + `", "xcode": "cal" } ]} }` 1944 1945 gqlResponse := executeRequest(t, graphqlURL, addStateParams) 1946 requireNoGQLErrors(t, gqlResponse) 1947 1948 var expected, result struct { 1949 AddState struct { 1950 State []*state 1951 } 1952 } 1953 err := json.Unmarshal([]byte(addStateExpected), &expected) 1954 require.NoError(t, err) 1955 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 1956 require.NoError(t, err) 1957 1958 requireUID(t, result.AddState.State[0].ID) 1959 1960 // Always ignore the ID of the object that was just created. That ID is 1961 // minted by Dgraph. 1962 opt := cmpopts.IgnoreFields(state{}, "ID") 1963 if diff := cmp.Diff(expected, result, opt); diff != "" { 1964 t.Errorf("result mismatch (-want +got):\n%s", diff) 1965 } 1966 1967 return result.AddState.State[0] 1968 } 1969 1970 func deleteState( 1971 t *testing.T, 1972 filter map[string]interface{}, 1973 deleteStateExpected string, 1974 expectedErrors x.GqlErrorList) { 1975 1976 deleteStateParams := &GraphQLParams{ 1977 Query: `mutation deleteState($filter: StateFilter!) { 1978 deleteState(filter: $filter) { msg } 1979 }`, 1980 Variables: map[string]interface{}{"filter": filter}, 1981 } 1982 1983 gqlResponse := deleteStateParams.ExecuteAsPost(t, graphqlURL) 1984 require.JSONEq(t, deleteStateExpected, string(gqlResponse.Data)) 1985 1986 if diff := cmp.Diff(expectedErrors, gqlResponse.Errors); diff != "" { 1987 t.Errorf("errors mismatch (-want +got):\n%s", diff) 1988 } 1989 } 1990 1991 func addMutationWithXid(t *testing.T, executeRequest requestExecutor) { 1992 newState := addState(t, "California", executeRequest) 1993 requireState(t, newState.ID, newState, executeRequest) 1994 1995 // Try add again, it should fail this time. 1996 name := "Calgary" 1997 addStateParams := &GraphQLParams{ 1998 Query: `mutation addState($xcode: String!, $name: String!) { 1999 addState(input: [{ xcode: $xcode, name: $name }]) { 2000 state { 2001 id 2002 xcode 2003 name 2004 } 2005 } 2006 }`, 2007 Variables: map[string]interface{}{"name": name, "xcode": "cal"}, 2008 } 2009 2010 gqlResponse := executeRequest(t, graphqlURL, addStateParams) 2011 require.NotNil(t, gqlResponse.Errors) 2012 require.Contains(t, gqlResponse.Errors[0].Error(), 2013 "because id cal already exists for type State") 2014 2015 deleteStateExpected := `{"deleteState" : { "msg": "Deleted" } }` 2016 filter := map[string]interface{}{"xcode": map[string]interface{}{"eq": "cal"}} 2017 deleteState(t, filter, deleteStateExpected, nil) 2018 } 2019 2020 func addMutationWithXID(t *testing.T) { 2021 addMutationWithXid(t, postExecutor) 2022 } 2023 2024 func addMultipleMutationWithOneError(t *testing.T) { 2025 newCountry := addCountry(t, postExecutor) 2026 newAuth := addAuthor(t, newCountry.ID, postExecutor) 2027 2028 badAuth := &author{ 2029 ID: "0x0", 2030 } 2031 2032 goodPost := &post{ 2033 Title: "Test Post", 2034 Text: "This post is just a test.", 2035 IsPublished: true, 2036 NumLikes: 1000, 2037 Author: newAuth, 2038 } 2039 2040 badPost := &post{ 2041 Title: "Test Post", 2042 Text: "This post is just a test.", 2043 IsPublished: true, 2044 NumLikes: 1000, 2045 Author: badAuth, 2046 } 2047 2048 anotherGoodPost := &post{ 2049 Title: "Another Test Post", 2050 Text: "This is just another post", 2051 IsPublished: true, 2052 NumLikes: 1000, 2053 Author: newAuth, 2054 } 2055 2056 addPostParams := &GraphQLParams{ 2057 Query: `mutation addPost($posts: [AddPostInput!]!) { 2058 addPost(input: $posts) { 2059 post { 2060 postID 2061 title 2062 author { 2063 id 2064 } 2065 } 2066 } 2067 }`, 2068 Variables: map[string]interface{}{"posts": []*post{goodPost, badPost, 2069 anotherGoodPost}}, 2070 } 2071 2072 gqlResponse := postExecutor(t, graphqlURL, addPostParams) 2073 2074 addPostExpected := fmt.Sprintf(`{ "addPost": { 2075 "post": [{ 2076 "title": "Text Post", 2077 "author": { 2078 "id": "%s" 2079 } 2080 }, { 2081 "title": "Another Test Post", 2082 "author": { 2083 "id": "%s" 2084 } 2085 }] 2086 } }`, newAuth.ID, newAuth.ID) 2087 2088 var expected, result struct { 2089 AddPost struct { 2090 Post []*post 2091 } 2092 } 2093 err := json.Unmarshal([]byte(addPostExpected), &expected) 2094 require.NoError(t, err) 2095 err = json.Unmarshal([]byte(gqlResponse.Data), &result) 2096 require.NoError(t, err) 2097 2098 require.Contains(t, gqlResponse.Errors[0].Error(), 2099 `couldn't rewrite query for mutation addPost because ID "0x0" isn't a Author`) 2100 2101 cleanUp(t, []*country{newCountry}, []*author{newAuth}, result.AddPost.Post) 2102 }