go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gerrit/rest_test.go (about) 1 // Copyright 2018 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package gerrit 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "io" 22 "net/http" 23 "net/http/httptest" 24 "net/url" 25 "testing" 26 "time" 27 28 "google.golang.org/grpc" 29 "google.golang.org/grpc/codes" 30 "google.golang.org/protobuf/types/known/timestamppb" 31 32 gerritpb "go.chromium.org/luci/common/proto/gerrit" 33 "go.chromium.org/luci/grpc/grpcutil" 34 35 . "github.com/smartystreets/goconvey/convey" 36 . "go.chromium.org/luci/common/testing/assertions" 37 ) 38 39 func TestBuildURL(t *testing.T) { 40 t.Parallel() 41 42 Convey("buildURL works correctly", t, func() { 43 cPB, err := NewRESTClient(nil, "x-review.googlesource.com", true) 44 So(err, ShouldBeNil) 45 c, ok := cPB.(*client) 46 So(ok, ShouldBeTrue) 47 48 So(c.buildURL("/changes/project~123", nil, nil), ShouldResemble, 49 "https://x-review.googlesource.com/a/changes/project~123") 50 So(c.buildURL("/changes/project~123", url.Values{"o": []string{"ONE", "TWO"}}, nil), ShouldResemble, 51 "https://x-review.googlesource.com/a/changes/project~123?o=ONE&o=TWO") 52 53 opt := UseGerritMirror(func(host string) string { return "mirror-" + host }) 54 So(c.buildURL("/changes/project~123", nil, []grpc.CallOption{opt}), ShouldResemble, 55 "https://mirror-x-review.googlesource.com/a/changes/project~123") 56 57 c.auth = false 58 So(c.buildURL("/path", nil, nil), ShouldResemble, 59 "https://x-review.googlesource.com/path") 60 }) 61 } 62 63 func TestListChanges(t *testing.T) { 64 t.Parallel() 65 ctx := context.Background() 66 67 Convey("ListChanges", t, func() { 68 Convey("Validates Limit number", func() { 69 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {}) 70 defer srv.Close() 71 72 _, err := c.ListChanges(ctx, &gerritpb.ListChangesRequest{ 73 Query: "label:Commit-Queue", 74 Limit: -1, 75 }) 76 So(err, ShouldErrLike, "must be nonnegative") 77 78 _, err = c.ListChanges(ctx, &gerritpb.ListChangesRequest{ 79 Query: "label:Commit-Queue", 80 Limit: 1001, 81 }) 82 So(err, ShouldErrLike, "should be at most") 83 }) 84 85 req := &gerritpb.ListChangesRequest{ 86 Query: "label:Code-Review", 87 Limit: 1, 88 } 89 90 Convey("OK case with one change, _more_changes set in response", func() { 91 expectedResponse := &gerritpb.ListChangesResponse{ 92 Changes: []*gerritpb.ChangeInfo{ 93 { 94 Number: 1, 95 Owner: &gerritpb.AccountInfo{ 96 AccountId: 1000096, 97 Name: "John Doe", 98 Email: "jdoe@example.com", 99 Username: "jdoe", 100 }, 101 Status: gerritpb.ChangeStatus_MERGED, 102 Project: "example/repo", 103 Ref: "refs/heads/master", 104 Created: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 105 Updated: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 106 Submitted: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 107 Branch: "master", 108 }, 109 }, 110 MoreChanges: true, 111 } 112 var actualRequest *http.Request 113 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 114 actualRequest = r 115 w.WriteHeader(200) 116 w.Header().Set("Content-Type", "application/json") 117 fmt.Fprint(w, `)]}'[ 118 { 119 "_number": 1, 120 "owner": { 121 "_account_id": 1000096, 122 "name": "John Doe", 123 "email": "jdoe@example.com", 124 "username": "jdoe" 125 }, 126 "status": "MERGED", 127 "project": "example/repo", 128 "branch": "master", 129 "created": "2014-05-05 07:15:44.639000000", 130 "updated": "2014-05-05 07:15:44.639000000", 131 "submitted": "2014-05-05 07:15:44.639000000", 132 "_more_changes": true 133 } 134 ]`) 135 }) 136 defer srv.Close() 137 138 Convey("Response and request are as expected", func() { 139 res, err := c.ListChanges(ctx, req) 140 So(err, ShouldBeNil) 141 So(res, ShouldResemble, expectedResponse) 142 So(actualRequest.URL.Query()["q"], ShouldResemble, []string{"label:Code-Review"}) 143 So(actualRequest.URL.Query()["S"], ShouldResemble, []string{"0"}) 144 So(actualRequest.URL.Query()["n"], ShouldResemble, []string{"1"}) 145 }) 146 147 Convey("Options are included in the request", func() { 148 req.Options = append(req.Options, gerritpb.QueryOption_DETAILED_ACCOUNTS, gerritpb.QueryOption_ALL_COMMITS) 149 _, err := c.ListChanges(ctx, req) 150 So(err, ShouldBeNil) 151 So( 152 actualRequest.URL.Query()["o"], 153 ShouldResemble, 154 []string{"DETAILED_ACCOUNTS", "ALL_COMMITS"}, 155 ) 156 }) 157 }) 158 }) 159 } 160 161 func TestGetChange(t *testing.T) { 162 t.Parallel() 163 ctx := context.Background() 164 165 Convey("GetChange", t, func() { 166 Convey("Validate args", func() { 167 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {}) 168 defer srv.Close() 169 170 _, err := c.GetChange(ctx, &gerritpb.GetChangeRequest{}) 171 So(err, ShouldErrLike, "number must be positive") 172 }) 173 174 req := &gerritpb.GetChangeRequest{Number: 1} 175 176 Convey("OK", func() { 177 expectedChange := &gerritpb.ChangeInfo{ 178 Number: 1, 179 Owner: &gerritpb.AccountInfo{ 180 AccountId: 1000096, 181 Name: "John Doe", 182 Email: "jdoe@example.com", 183 SecondaryEmails: []string{"johndoe@chromium.org"}, 184 Username: "jdoe", 185 Tags: []string{"SERVICE_USER"}, 186 }, 187 Project: "example/repo", 188 Ref: "refs/heads/master", 189 Subject: "Added new feature", 190 Status: gerritpb.ChangeStatus_NEW, 191 CurrentRevision: "deadbeef", 192 Submittable: true, 193 IsPrivate: true, 194 MetaRevId: "cafecafe", 195 Hashtags: []string{"example_tag"}, 196 Revisions: map[string]*gerritpb.RevisionInfo{ 197 "deadbeef": { 198 Number: 1, 199 Kind: gerritpb.RevisionInfo_REWORK, 200 Uploader: &gerritpb.AccountInfo{ 201 AccountId: 1000096, 202 Name: "John Doe", 203 Email: "jdoe@example.com", 204 SecondaryEmails: []string{"johndoe@chromium.org"}, 205 Username: "jdoe", 206 Tags: []string{"SERVICE_USER"}, 207 }, 208 Ref: "refs/changes/123", 209 Created: timestamppb.New(parseTime("2016-03-29T17:47:23.751000000Z")), 210 Description: "first upload", 211 Files: map[string]*gerritpb.FileInfo{ 212 "go/to/file.go": { 213 LinesInserted: 32, 214 LinesDeleted: 44, 215 SizeDelta: -567, 216 Size: 11984, 217 }, 218 }, 219 Commit: &gerritpb.CommitInfo{ 220 Id: "", // Gerrit doesn't set it, as it duplicates key in revisions map. 221 Message: "Title.\n\nBody is here.\n\nChange-Id: I100deadbeef", 222 Parents: []*gerritpb.CommitInfo_Parent{ 223 {Id: "deadbeef00"}, 224 }, 225 Author: &gerritpb.GitPersonInfo{ 226 Name: "John Doe", 227 Email: "jdoe@example.com", 228 }, 229 }, 230 }, 231 }, 232 Labels: map[string]*gerritpb.LabelInfo{ 233 "Code-Review": { 234 Approved: &gerritpb.AccountInfo{ 235 Name: "Rubber Stamper", 236 Email: "rubberstamper@example.com", 237 }, 238 }, 239 "Commit-Queue": { 240 Optional: true, 241 DefaultValue: 0, 242 Values: map[int32]string{0: "Not ready", 1: "Dry run", 2: "Commit"}, 243 All: []*gerritpb.ApprovalInfo{ 244 { 245 User: &gerritpb.AccountInfo{ 246 AccountId: 1010101, 247 Name: "Dry Runner", 248 Email: "dry-runner@example.com", 249 }, 250 Value: 1, 251 PermittedVotingRange: &gerritpb.VotingRangeInfo{Min: 0, Max: 2}, 252 Date: timestamppb.New(parseTime("2020-12-13T18:32:35.000000000Z")), 253 }, 254 }, 255 }, 256 }, 257 Created: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 258 Updated: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 259 Submitted: timestamppb.New(parseTime("0001-01-01T00:00:00.00000000Z")), 260 Messages: []*gerritpb.ChangeMessageInfo{ 261 { 262 Id: "YH-egE", 263 Author: &gerritpb.AccountInfo{ 264 AccountId: 1000096, 265 Name: "John Doe", 266 Email: "john.doe@example.com", 267 Username: "jdoe", 268 }, 269 Date: timestamppb.New(parseTime("2013-03-23T21:34:02.419000000Z")), 270 Message: "Patch Set 1:\n\nThis is the first message.", 271 Tag: "autogenerated:gerrit:test", 272 }, 273 { 274 Id: "WEEdhU", 275 Author: &gerritpb.AccountInfo{ 276 AccountId: 1000097, 277 Name: "Jane Roe", 278 Email: "jane.roe@example.com", 279 Username: "jroe", 280 }, 281 Date: timestamppb.New(parseTime("2013-03-23T21:36:52.332000000Z")), 282 Message: "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.", 283 }, 284 }, 285 Requirements: []*gerritpb.Requirement{ 286 { 287 Status: gerritpb.Requirement_REQUIREMENT_STATUS_OK, 288 FallbackText: "nothing more required", 289 Type: "alpha-numer1c-type", 290 }, 291 }, 292 SubmitRequirements: []*gerritpb.SubmitRequirementResultInfo{ 293 { 294 Name: "Verified", 295 Description: "Submit requirement for the 'Verified' label", 296 Status: gerritpb.SubmitRequirementResultInfo_NOT_APPLICABLE, 297 IsLegacy: false, 298 ApplicabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{ 299 Fulfilled: false, 300 }, 301 }, 302 { 303 Name: "Code-Owners", 304 Description: "Code Owners overrides approval", 305 Status: gerritpb.SubmitRequirementResultInfo_SATISFIED, 306 IsLegacy: false, 307 ApplicabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{ 308 Fulfilled: true, 309 }, 310 SubmittabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{ 311 Expression: "has:approval_code-owners", 312 Fulfilled: true, 313 PassingAtoms: []string{"has:approval_code-owners"}, 314 FailingAtoms: []string{}, 315 }, 316 OverrideExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{ 317 Expression: "label:Owners-Override=+1", 318 Fulfilled: false, 319 PassingAtoms: []string{}, 320 FailingAtoms: []string{"label:Owners-Override=+1"}, 321 }, 322 }, 323 { 324 Name: "Code-Review", 325 Description: "Submit requirement for the 'Code-Review' label", 326 Status: gerritpb.SubmitRequirementResultInfo_UNSATISFIED, 327 IsLegacy: false, 328 SubmittabilityExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{ 329 Expression: "label:Code-Review=MAX,user=non_uploader -label:Code-Review=MIN", 330 Fulfilled: false, 331 PassingAtoms: []string{}, 332 FailingAtoms: []string{ 333 "label:Code-Review=MAX,user=non_uploader", 334 "label:Code-Review=MIN", 335 }, 336 }, 337 OverrideExpressionResult: &gerritpb.SubmitRequirementExpressionInfo{ 338 Expression: "label:Bot-Commit=+1", 339 Fulfilled: false, 340 PassingAtoms: []string{}, 341 FailingAtoms: []string{"label:Bot-Commit=+1"}, 342 }, 343 }, 344 }, 345 Reviewers: &gerritpb.ReviewerStatusMap{ 346 Reviewers: []*gerritpb.AccountInfo{ 347 { 348 AccountId: 1000096, 349 Name: "John Doe", 350 Email: "john.doe@example.com", 351 Username: "jdoe", 352 }, 353 { 354 AccountId: 1000097, 355 Name: "Jane Roe", 356 Email: "jane.roe@example.com", 357 Username: "jroe", 358 }, 359 }, 360 }, 361 Branch: "master", 362 } 363 var actualRequest *http.Request 364 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 365 actualRequest = r 366 w.WriteHeader(200) 367 w.Header().Set("Content-Type", "application/json") 368 fmt.Fprint(w, `)]}'{ 369 "_number": 1, 370 "status": "NEW", 371 "owner": { 372 "_account_id": 1000096, 373 "name": "John Doe", 374 "email": "jdoe@example.com", 375 "secondary_emails": ["johndoe@chromium.org"], 376 "username": "jdoe", 377 "tags": ["SERVICE_USER"] 378 }, 379 "created": "2014-05-05 07:15:44.639000000", 380 "updated": "2014-05-05 07:15:44.639000000", 381 "project": "example/repo", 382 "branch": "master", 383 "current_revision": "deadbeef", 384 "submittable": true, 385 "is_private": true, 386 "meta_rev_id": "cafecafe", 387 "hashtags": ["example_tag"], 388 "subject": "Added new feature", 389 "revisions": { 390 "deadbeef": { 391 "_number": 1, 392 "kind": "REWORK", 393 "ref": "refs/changes/123", 394 "uploader": { 395 "_account_id": 1000096, 396 "name": "John Doe", 397 "email": "jdoe@example.com", 398 "secondary_emails": ["johndoe@chromium.org"], 399 "username": "jdoe", 400 "tags": ["SERVICE_USER"] 401 }, 402 "created": "2016-03-29 17:47:23.751000000", 403 "description": "first upload", 404 "files": { 405 "go/to/file.go": { 406 "lines_inserted": 32, 407 "lines_deleted": 44, 408 "size_delta": -567, 409 "size": 11984 410 } 411 }, 412 "commit": { 413 "parents": [{"commit": "deadbeef00"}], 414 "author": { 415 "name": "John Doe", 416 "email": "jdoe@example.com", 417 "date": "2014-05-05 07:15:44.639000000", 418 "tz": 60 419 }, 420 "committer": { 421 "name": "John Doe", 422 "email": "jdoe@example.com", 423 "date": "2014-05-05 07:15:44.639000000", 424 "tz": 60 425 }, 426 "subject": "Title.", 427 "message": "Title.\n\nBody is here.\n\nChange-Id: I100deadbeef" 428 } 429 } 430 }, 431 "labels": { 432 "Code-Review": { 433 "approved": { 434 "name": "Rubber Stamper", 435 "email": "rubberstamper@example.com" 436 } 437 }, 438 "Commit-Queue": { 439 "all": [ 440 { 441 "value": 1, 442 "date": "2020-12-13 18:32:35.000000000", 443 "permitted_voting_range": { 444 "min": 0, 445 "max": 2 446 }, 447 "_account_id": 1010101, 448 "name": "Dry Runner", 449 "email": "dry-runner@example.com", 450 "avatars": [ 451 { 452 "url": "https://example.com/photo.jpg", 453 "height": 32 454 } 455 ] 456 } 457 ], 458 "values": { 459 " 0": "Not ready", 460 "+1": "Dry run", 461 "+2": "Commit" 462 }, 463 "default_value": 0, 464 "optional": true 465 } 466 }, 467 "messages": [ 468 { 469 "id": "YH-egE", 470 "author": { 471 "_account_id": 1000096, 472 "name": "John Doe", 473 "email": "john.doe@example.com", 474 "username": "jdoe" 475 }, 476 "date": "2013-03-23 21:34:02.419000000", 477 "message": "Patch Set 1:\n\nThis is the first message.", 478 "_revision_number": 1, 479 "tag": "autogenerated:gerrit:test" 480 }, 481 { 482 "id": "WEEdhU", 483 "author": { 484 "_account_id": 1000097, 485 "name": "Jane Roe", 486 "email": "jane.roe@example.com", 487 "username": "jroe" 488 }, 489 "date": "2013-03-23 21:36:52.332000000", 490 "message": "Patch Set 1:\n\nThis is the second message.\n\nWith a line break.", 491 "_revision_number": 1 492 } 493 ], 494 "requirements": [ 495 { 496 "status": "OK", 497 "fallback_text": "nothing more required", 498 "type": "alpha-numer1c-type" 499 } 500 ], 501 "submit_requirements": [ 502 { 503 "name": "Verified", 504 "description": "Submit requirement for the 'Verified' label", 505 "status": "NOT_APPLICABLE", 506 "is_legacy": false, 507 "applicability_expression_result": { 508 "fulfilled": false 509 } 510 }, 511 { 512 "name": "Code-Owners", 513 "description": "Code Owners overrides approval", 514 "status": "SATISFIED", 515 "is_legacy": false, 516 "applicability_expression_result": { 517 "fulfilled": true 518 }, 519 "submittability_expression_result": { 520 "expression": "has:approval_code-owners", 521 "fulfilled": true, 522 "passing_atoms": ["has:approval_code-owners"], 523 "failing_atoms": [] 524 }, 525 "override_expression_result": { 526 "expression": "label:Owners-Override=+1", 527 "fulfilled": false, 528 "passing_atoms": [], 529 "failing_atoms": ["label:Owners-Override=+1"] 530 } 531 }, 532 { 533 "name": "Code-Review", 534 "description": "Submit requirement for the 'Code-Review' label", 535 "status": "UNSATISFIED", 536 "is_legacy": false, 537 "submittability_expression_result": { 538 "expression": "label:Code-Review=MAX,user=non_uploader -label:Code-Review=MIN", 539 "fulfilled": false, 540 "passing_atoms": [], 541 "failing_atoms": [ 542 "label:Code-Review=MAX,user=non_uploader", 543 "label:Code-Review=MIN" 544 ] 545 }, 546 "override_expression_result": { 547 "expression": "label:Bot-Commit=+1", 548 "fulfilled": false, 549 "passing_atoms": [], 550 "failing_atoms": ["label:Bot-Commit=+1"] 551 } 552 } 553 ], 554 "reviewers": { 555 "REVIEWER": [ 556 { 557 "_account_id": 1000096, 558 "name": "John Doe", 559 "email": "john.doe@example.com", 560 "username": "jdoe" 561 }, 562 { 563 "_account_id": 1000097, 564 "name": "Jane Roe", 565 "email": "jane.roe@example.com", 566 "username": "jroe" 567 } 568 ] 569 } 570 }`) 571 }) 572 defer srv.Close() 573 574 Convey("Basic", func() { 575 res, err := c.GetChange(ctx, req) 576 So(err, ShouldBeNil) 577 So(res, ShouldResemble, expectedChange) 578 }) 579 580 Convey("With project", func() { 581 req.Project = "infra/luci" 582 res, err := c.GetChange(ctx, req) 583 So(err, ShouldBeNil) 584 So(res, ShouldResembleProto, expectedChange) 585 So(actualRequest.URL.EscapedPath(), ShouldEqual, "/changes/infra%2Fluci~1") 586 }) 587 588 Convey("Options", func() { 589 req.Options = append(req.Options, gerritpb.QueryOption_DETAILED_ACCOUNTS, gerritpb.QueryOption_ALL_COMMITS) 590 _, err := c.GetChange(ctx, req) 591 So(err, ShouldBeNil) 592 So( 593 actualRequest.URL.Query()["o"], 594 ShouldResemble, 595 []string{"DETAILED_ACCOUNTS", "ALL_COMMITS"}, 596 ) 597 }) 598 599 Convey("Meta", func() { 600 req.Meta = "deadbeef" 601 _, err := c.GetChange(ctx, req) 602 So(err, ShouldBeNil) 603 So( 604 actualRequest.URL.Query()["meta"], 605 ShouldResemble, 606 []string{"deadbeef"}, 607 ) 608 }) 609 }) 610 }) 611 } 612 613 func TestRestCreateChange(t *testing.T) { 614 t.Parallel() 615 ctx := context.Background() 616 617 Convey("CreateChange basic", t, func() { 618 var actualBody []byte 619 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 620 // ignore errors here, but verify body later. 621 actualBody, _ = io.ReadAll(r.Body) 622 w.WriteHeader(201) 623 w.Header().Set("Content-Type", "application/json") 624 fmt.Fprint(w, `)]}'`) 625 json.NewEncoder(w).Encode(map[string]any{ 626 "_number": 1, 627 "project": "example/repo", 628 "branch": "master", 629 "change_id": "c1", 630 "status": "NEW", 631 "created": "2014-05-05 07:15:44.639000000", 632 "updated": "2014-05-05 07:15:44.639000000", 633 }) 634 }) 635 defer srv.Close() 636 637 req := gerritpb.CreateChangeRequest{ 638 Project: "example/repo", 639 Ref: "refs/heads/master", 640 Subject: "example subject", 641 BaseCommit: "someOpaqueHash", 642 } 643 res, err := c.CreateChange(ctx, &req) 644 So(err, ShouldBeNil) 645 So(res, ShouldResemble, &gerritpb.ChangeInfo{ 646 Number: 1, 647 Project: "example/repo", 648 Ref: "refs/heads/master", 649 Status: gerritpb.ChangeStatus_NEW, 650 Submittable: false, 651 Created: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 652 Updated: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 653 Submitted: timestamppb.New(parseTime("0001-01-01T00:00:00.00000000Z")), 654 Branch: "master", 655 }) 656 657 var ci changeInput 658 err = json.Unmarshal(actualBody, &ci) 659 So(err, ShouldBeNil) 660 So(ci, ShouldResemble, changeInput{ 661 Project: "example/repo", 662 Branch: "refs/heads/master", 663 Subject: "example subject", 664 BaseCommit: "someOpaqueHash", 665 }) 666 }) 667 } 668 669 func TestSubmitRevision(t *testing.T) { 670 t.Parallel() 671 ctx := context.Background() 672 673 Convey("SubmitRevision", t, func() { 674 var actualURL *url.URL 675 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 676 actualURL = r.URL 677 w.WriteHeader(200) 678 w.Header().Set("Content-Type", "application/json") 679 fmt.Fprint(w, `)]}'`) 680 json.NewEncoder(w).Encode(map[string]any{ 681 "status": "MERGED", 682 }) 683 }) 684 defer srv.Close() 685 686 req := &gerritpb.SubmitRevisionRequest{ 687 Number: 42, 688 RevisionId: "someRevision", 689 Project: "someProject", 690 } 691 res, err := c.SubmitRevision(ctx, req) 692 So(err, ShouldBeNil) 693 So(res, ShouldResembleProto, &gerritpb.SubmitInfo{ 694 Status: gerritpb.ChangeStatus_MERGED, 695 }) 696 So(actualURL.Path, ShouldEqual, "/changes/someProject~42/revisions/someRevision/submit") 697 }) 698 } 699 700 func TestRestChangeEditFileContent(t *testing.T) { 701 t.Parallel() 702 ctx := context.Background() 703 704 Convey("ChangeEditFileContent basic", t, func() { 705 // large enough? 706 var actualBody []byte 707 var actualURL *url.URL 708 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 709 actualURL = r.URL 710 // ignore errors here, but verify body later. 711 actualBody, _ = io.ReadAll(r.Body) 712 // API returns 204 on success. 713 w.WriteHeader(204) 714 }) 715 defer srv.Close() 716 717 _, err := c.ChangeEditFileContent(ctx, &gerritpb.ChangeEditFileContentRequest{ 718 Number: 42, 719 Project: "some/project", 720 FilePath: "some/path+foo", 721 Content: []byte("changed file"), 722 }) 723 So(err, ShouldBeNil) 724 So(actualURL.RawPath, ShouldEqual, "/changes/some%2Fproject~42/edit/some%2Fpath%2Bfoo") 725 So(actualURL.Path, ShouldEqual, "/changes/some/project~42/edit/some/path+foo") 726 So(actualBody, ShouldResemble, []byte("changed file")) 727 }) 728 } 729 730 // TODO (yulanlin): Assert body verbatim without decoding 731 func TestAddReviewer(t *testing.T) { 732 t.Parallel() 733 ctx := context.Background() 734 735 Convey("Add reviewer to cc basic", t, func() { 736 var actualURL *url.URL 737 var actualBody []byte 738 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 739 actualURL = r.URL 740 // ignore the error because body contents will be checked 741 actualBody, _ = io.ReadAll(r.Body) 742 w.WriteHeader(200) 743 w.Header().Set("Content-Type", "application/json") 744 fmt.Fprint(w, `)]}' 745 { 746 "input": "ccer@test.com", 747 "ccs": [ 748 { 749 "_account_id": 10001, 750 "name": "Reviewer Review", 751 "approvals": { 752 "Code-Review": " 0" 753 } 754 } 755 ] 756 }`) 757 }) 758 defer srv.Close() 759 760 req := &gerritpb.AddReviewerRequest{ 761 Number: 42, 762 Project: "someproject", 763 Reviewer: "ccer@test.com", 764 State: gerritpb.AddReviewerRequest_ADD_REVIEWER_STATE_CC, 765 Confirmed: true, 766 Notify: gerritpb.Notify_NOTIFY_OWNER, 767 } 768 res, err := c.AddReviewer(ctx, req) 769 So(err, ShouldBeNil) 770 771 // assert the request was as expected 772 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/reviewers") 773 var body addReviewerRequest 774 err = json.Unmarshal(actualBody, &body) 775 if err != nil { 776 t.Logf("failed to decode req body: %v\n", err) 777 } 778 So(body, ShouldResemble, addReviewerRequest{ 779 Reviewer: "ccer@test.com", 780 State: "CC", 781 Confirmed: true, 782 Notify: "OWNER", 783 }) 784 785 // assert the result was as expected 786 So(res, ShouldResemble, &gerritpb.AddReviewerResult{ 787 Input: "ccer@test.com", 788 Reviewers: []*gerritpb.ReviewerInfo{}, 789 Ccs: []*gerritpb.ReviewerInfo{ 790 { 791 Account: &gerritpb.AccountInfo{ 792 Name: "Reviewer Review", 793 AccountId: 10001, 794 }, 795 Approvals: map[string]int32{ 796 "Code-Review": 0, 797 }, 798 }, 799 }, 800 }) 801 }) 802 } 803 804 func TestDeleteReviewer(t *testing.T) { 805 t.Parallel() 806 ctx := context.Background() 807 808 Convey("Delete reviewer", t, func() { 809 var actualURL *url.URL 810 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 811 actualURL = r.URL 812 // API returns 204 on success. 813 w.WriteHeader(204) 814 }) 815 defer srv.Close() 816 817 _, err := c.DeleteReviewer(ctx, &gerritpb.DeleteReviewerRequest{ 818 Number: 42, 819 Project: "someproject", 820 AccountId: "jdoe@example.com", 821 }) 822 So(err, ShouldBeNil) 823 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/reviewers/jdoe@example.com/delete") 824 }) 825 } 826 827 func TestSetReview(t *testing.T) { 828 t.Parallel() 829 ctx := context.Background() 830 831 Convey("Set Review", t, func() { 832 var actualURL *url.URL 833 var actualRawBody []byte 834 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 835 actualURL = r.URL 836 actualRawBody, _ = io.ReadAll(r.Body) 837 w.WriteHeader(200) 838 w.Header().Set("Content-Type", "application/json") 839 fmt.Fprint(w, `)]}' 840 { 841 "labels": { 842 "Code-Review": -1 843 } 844 }`) 845 }) 846 defer srv.Close() 847 848 res, err := c.SetReview(ctx, &gerritpb.SetReviewRequest{ 849 Number: 42, 850 Project: "someproject", 851 RevisionId: "somerevision", 852 Message: "This is a message", 853 Labels: map[string]int32{ 854 "Code-Review": -1, 855 }, 856 Tag: "autogenerated:cq", 857 Notify: gerritpb.Notify_NOTIFY_OWNER, 858 NotifyDetails: &gerritpb.NotifyDetails{ 859 Recipients: []*gerritpb.NotifyDetails_Recipient{ 860 { 861 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO, 862 Info: &gerritpb.NotifyDetails_Info{ 863 Accounts: []int64{4, 5, 3}, 864 }, 865 }, 866 { 867 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO, 868 Info: &gerritpb.NotifyDetails_Info{ 869 Accounts: []int64{2, 3, 1}, 870 // 3 is overlapping with the first recipient, 871 }, 872 }, 873 { 874 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_BCC, 875 Info: &gerritpb.NotifyDetails_Info{ 876 Accounts: []int64{6, 1}, 877 }, 878 }, 879 }, 880 }, 881 OnBehalfOf: 10001, 882 Ready: true, 883 AddToAttentionSet: []*gerritpb.AttentionSetInput{ 884 {User: "10002", Reason: "passed presubmit"}, 885 }, 886 RemoveFromAttentionSet: []*gerritpb.AttentionSetInput{ 887 {User: "10001", Reason: "passed presubmit"}, 888 }, 889 IgnoreAutomaticAttentionSetRules: true, 890 }) 891 So(err, ShouldBeNil) 892 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/review") 893 894 var actualBody, expectedBody map[string]any 895 So(json.Unmarshal(actualRawBody, &actualBody), ShouldBeNil) 896 So(json.Unmarshal([]byte(`{ 897 "message": "This is a message", 898 "labels": { 899 "Code-Review": -1 900 }, 901 "tag": "autogenerated:cq", 902 "notify": "OWNER", 903 "notify_details": { 904 "TO": {"accounts": [1, 2, 3, 4, 5]}, 905 "BCC": {"accounts": [1, 6]} 906 }, 907 "on_behalf_of": 10001, 908 "ready": true, 909 "add_to_attention_set": [ 910 {"user": "10002", "reason": "passed presubmit"} 911 ], 912 "remove_from_attention_set": [ 913 {"user": "10001", "reason": "passed presubmit"} 914 ], 915 "ignore_automatic_attention_set_rules": true 916 }`), &expectedBody), ShouldBeNil) 917 So(actualBody, ShouldResemble, expectedBody) 918 919 So(res, ShouldResembleProto, &gerritpb.ReviewResult{ 920 Labels: map[string]int32{ 921 "Code-Review": -1, 922 }, 923 }) 924 }) 925 926 Convey("Set Review can add reviewers", t, func() { 927 var actualURL *url.URL 928 var actualRawBody []byte 929 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 930 actualURL = r.URL 931 actualRawBody, _ = io.ReadAll(r.Body) 932 w.WriteHeader(200) 933 w.Header().Set("Content-Type", "application/json") 934 fmt.Fprint(w, `)]}' 935 { 936 "reviewers": { 937 "jdoe@example.com": { 938 "input": "jdoe@example.com", 939 "reviewers": [ 940 { 941 "_account_id": 10001, 942 "name": "John Doe", 943 "email": "jdoe@example.com", 944 "approvals": { 945 "Verified": "0", 946 "Code-Review": "0" 947 } 948 } 949 ] 950 }, 951 "10003": { 952 "input": "10003", 953 "ccs": [ 954 { 955 "_account_id": 10003, 956 "name": "Eve Smith", 957 "email": "esmith@example.com", 958 "approvals": { 959 "Verified": "0", 960 "Code-Review": "0" 961 } 962 } 963 ] 964 } 965 } 966 }`) 967 }) 968 defer srv.Close() 969 970 res, err := c.SetReview(ctx, &gerritpb.SetReviewRequest{ 971 Number: 42, 972 Project: "someproject", 973 RevisionId: "somerevision", 974 Message: "This is a message", 975 Reviewers: []*gerritpb.ReviewerInput{ 976 { 977 Reviewer: "jdoe@example.com", 978 }, 979 { 980 Reviewer: "10003", 981 State: gerritpb.ReviewerInput_REVIEWER_INPUT_STATE_CC, 982 }, 983 }, 984 }) 985 So(err, ShouldBeNil) 986 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/review") 987 988 var actualBody, expectedBody map[string]any 989 So(json.Unmarshal(actualRawBody, &actualBody), ShouldBeNil) 990 So(json.Unmarshal([]byte(`{ 991 "message": "This is a message", 992 "reviewers": [ 993 {"reviewer": "jdoe@example.com"}, 994 {"reviewer": "10003", "state": "CC"} 995 ] 996 }`), &expectedBody), ShouldBeNil) 997 So(actualBody, ShouldResemble, expectedBody) 998 999 So(res, ShouldResembleProto, &gerritpb.ReviewResult{ 1000 Reviewers: map[string]*gerritpb.AddReviewerResult{ 1001 "jdoe@example.com": { 1002 Input: "jdoe@example.com", 1003 Reviewers: []*gerritpb.ReviewerInfo{ 1004 { 1005 Account: &gerritpb.AccountInfo{ 1006 Name: "John Doe", 1007 Email: "jdoe@example.com", 1008 AccountId: 10001, 1009 }, 1010 Approvals: map[string]int32{ 1011 "Verified": 0, 1012 "Code-Review": 0, 1013 }, 1014 }, 1015 }, 1016 }, 1017 "10003": { 1018 Input: "10003", 1019 Ccs: []*gerritpb.ReviewerInfo{ 1020 { 1021 Account: &gerritpb.AccountInfo{ 1022 Name: "Eve Smith", 1023 Email: "esmith@example.com", 1024 AccountId: 10003, 1025 }, 1026 Approvals: map[string]int32{ 1027 "Verified": 0, 1028 "Code-Review": 0, 1029 }, 1030 }, 1031 }, 1032 }, 1033 }, 1034 }) 1035 }) 1036 } 1037 1038 func TestAddToAttentionSet(t *testing.T) { 1039 t.Parallel() 1040 ctx := context.Background() 1041 1042 Convey("Add to attention set", t, func() { 1043 var actualURL *url.URL 1044 var actualBody []byte 1045 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1046 actualURL = r.URL 1047 // ignore the error because body contents will be checked 1048 actualBody, _ = io.ReadAll(r.Body) 1049 w.WriteHeader(200) 1050 w.Header().Set("Content-Type", "application/json") 1051 fmt.Fprint(w, `)]}' 1052 { 1053 "_account_id": 10001, 1054 "name": "FYI reviewer", 1055 "email": "fyi@test.com", 1056 "username": "fyi" 1057 }`) 1058 }) 1059 defer srv.Close() 1060 1061 req := &gerritpb.AttentionSetRequest{ 1062 Project: "someproject", 1063 Number: 42, 1064 Input: &gerritpb.AttentionSetInput{ 1065 User: "fyi@test.com", 1066 Reason: "For awareness", 1067 Notify: gerritpb.Notify_NOTIFY_ALL, 1068 }, 1069 } 1070 res, err := c.AddToAttentionSet(ctx, req) 1071 So(err, ShouldBeNil) 1072 1073 // assert the request was as expected 1074 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/attention") 1075 expectedBody, err := json.Marshal(attentionSetInput{ 1076 User: "fyi@test.com", 1077 Reason: "For awareness", 1078 Notify: "ALL", 1079 }) 1080 if err != nil { 1081 t.Logf("failed to encode expected body: %v\n", err) 1082 } 1083 So(actualBody, ShouldResemble, expectedBody) 1084 1085 // assert the result was as expected 1086 So(res, ShouldResemble, &gerritpb.AccountInfo{ 1087 AccountId: 10001, 1088 Name: "FYI reviewer", 1089 Email: "fyi@test.com", 1090 Username: "fyi", 1091 }) 1092 }) 1093 } 1094 1095 func TestRevertChange(t *testing.T) { 1096 t.Parallel() 1097 ctx := context.Background() 1098 1099 Convey("RevertChange", t, func() { 1100 Convey("Validate args", func() { 1101 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {}) 1102 defer srv.Close() 1103 1104 _, err := c.RevertChange(ctx, &gerritpb.RevertChangeRequest{}) 1105 So(err, ShouldErrLike, "number must be positive") 1106 }) 1107 1108 Convey("OK", func() { 1109 req := &gerritpb.RevertChangeRequest{ 1110 Number: 3964, 1111 Message: "This is the message added to the revert CL.", 1112 } 1113 1114 expectedChange := &gerritpb.ChangeInfo{ 1115 Number: 3965, 1116 Owner: &gerritpb.AccountInfo{ 1117 AccountId: 1000096, 1118 Name: "John Doe", 1119 Email: "jdoe@example.com", 1120 SecondaryEmails: []string{"johndoe@chromium.org"}, 1121 Username: "jdoe", 1122 Tags: []string{"SERVICE_USER"}, 1123 }, 1124 Project: "example/repo", 1125 Ref: "refs/heads/master", 1126 Status: gerritpb.ChangeStatus_NEW, 1127 Created: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 1128 Updated: timestamppb.New(parseTime("2014-05-05T07:15:44.639000000Z")), 1129 Submitted: timestamppb.New(parseTime("0001-01-01T00:00:00.00000000Z")), 1130 Messages: []*gerritpb.ChangeMessageInfo{ 1131 { 1132 Id: "YH-egE", 1133 Author: &gerritpb.AccountInfo{ 1134 AccountId: 1000096, 1135 Name: "John Doe", 1136 Email: "john.doe@example.com", 1137 Username: "jdoe", 1138 }, 1139 Date: timestamppb.New(parseTime("2013-03-23T21:34:02.419000000Z")), 1140 Message: "Patch Set 1:\n\nThis is the message added to the revert CL.", 1141 }, 1142 }, 1143 Branch: "master", 1144 } 1145 1146 var actualRequest *http.Request 1147 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1148 actualRequest = r 1149 w.WriteHeader(200) 1150 w.Header().Set("Content-Type", "application/json") 1151 fmt.Fprint(w, `)]}'{ 1152 "_number": 3965, 1153 "status": "NEW", 1154 "owner": { 1155 "_account_id": 1000096, 1156 "name": "John Doe", 1157 "email": "jdoe@example.com", 1158 "secondary_emails": ["johndoe@chromium.org"], 1159 "username": "jdoe", 1160 "tags": ["SERVICE_USER"] 1161 }, 1162 "created": "2014-05-05 07:15:44.639000000", 1163 "updated": "2014-05-05 07:15:44.639000000", 1164 "project": "example/repo", 1165 "branch": "master", 1166 "messages": [ 1167 { 1168 "id": "YH-egE", 1169 "author": { 1170 "_account_id": 1000096, 1171 "name": "John Doe", 1172 "email": "john.doe@example.com", 1173 "username": "jdoe" 1174 }, 1175 "date": "2013-03-23 21:34:02.419000000", 1176 "message": "Patch Set 1:\n\nThis is the message added to the revert CL.", 1177 "_revision_number": 1 1178 } 1179 ] 1180 }`) 1181 }) 1182 defer srv.Close() 1183 1184 Convey("Basic", func() { 1185 res, err := c.RevertChange(ctx, req) 1186 So(err, ShouldBeNil) 1187 So(res, ShouldResemble, expectedChange) 1188 So(actualRequest.URL.EscapedPath(), ShouldEqual, "/changes/3964/revert") 1189 }) 1190 1191 Convey("With project", func() { 1192 req.Project = "infra/luci" 1193 res, err := c.RevertChange(ctx, req) 1194 So(err, ShouldBeNil) 1195 So(res, ShouldResembleProto, expectedChange) 1196 So(actualRequest.URL.EscapedPath(), ShouldEqual, "/changes/infra%2Fluci~3964/revert") 1197 }) 1198 }) 1199 }) 1200 } 1201 1202 func TestGetMergeable(t *testing.T) { 1203 t.Parallel() 1204 ctx := context.Background() 1205 1206 Convey("GetMergeable basic", t, func() { 1207 var actualURL *url.URL 1208 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1209 actualURL = r.URL 1210 w.WriteHeader(200) 1211 w.Header().Set("Content-Type", "application/json") 1212 fmt.Fprint(w, `)]}' 1213 { 1214 "submit_type": "CHERRY_PICK", 1215 "strategy": "simple-two-way-in-core", 1216 "mergeable": true, 1217 "commit_merged": false, 1218 "content_merged": false, 1219 "conflicts": [ 1220 "conflict1", 1221 "conflict2" 1222 ], 1223 "mergeable_into": [ 1224 "my_branch_1" 1225 ] 1226 }`) 1227 }) 1228 defer srv.Close() 1229 1230 mi, err := c.GetMergeable(ctx, &gerritpb.GetMergeableRequest{ 1231 Number: 42, 1232 Project: "someproject", 1233 RevisionId: "somerevision", 1234 }) 1235 So(err, ShouldBeNil) 1236 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/mergeable") 1237 So(mi, ShouldResemble, &gerritpb.MergeableInfo{ 1238 SubmitType: gerritpb.MergeableInfo_CHERRY_PICK, 1239 Strategy: gerritpb.MergeableStrategy_SIMPLE_TWO_WAY_IN_CORE, 1240 Mergeable: true, 1241 CommitMerged: false, 1242 ContentMerged: false, 1243 Conflicts: []string{"conflict1", "conflict2"}, 1244 MergeableInto: []string{"my_branch_1"}, 1245 }) 1246 }) 1247 } 1248 1249 func TestListFiles(t *testing.T) { 1250 t.Parallel() 1251 ctx := context.Background() 1252 1253 Convey("ListFiles basic", t, func() { 1254 var actualURL *url.URL 1255 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1256 actualURL = r.URL 1257 w.WriteHeader(200) 1258 w.Header().Set("Content-Type", "application/json") 1259 fmt.Fprint(w, `)]}' 1260 { 1261 "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": { 1262 "lines_inserted": 123456 1263 }, 1264 "file2": { 1265 "size": 7 1266 } 1267 }`) 1268 }) 1269 defer srv.Close() 1270 1271 mi, err := c.ListFiles(ctx, &gerritpb.ListFilesRequest{ 1272 Number: 42, 1273 Project: "someproject", 1274 RevisionId: "somerevision", 1275 Parent: 999, 1276 }) 1277 So(err, ShouldBeNil) 1278 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/revisions/somerevision/files/") 1279 So(actualURL.Query().Get("parent"), ShouldEqual, "999") 1280 So(mi, ShouldResemble, &gerritpb.ListFilesResponse{ 1281 Files: map[string]*gerritpb.FileInfo{ 1282 "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": { 1283 LinesInserted: 123456, 1284 }, 1285 "file2": { 1286 Size: 7, 1287 }, 1288 }, 1289 }) 1290 }) 1291 } 1292 1293 func TestGetRelatedChanges(t *testing.T) { 1294 t.Parallel() 1295 ctx := context.Background() 1296 1297 Convey("GetRelatedChanges works", t, func() { 1298 var actualURL *url.URL 1299 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1300 actualURL = r.URL 1301 w.WriteHeader(200) 1302 w.Header().Set("Content-Type", "application/json") 1303 // Taken from 1304 // https://chromium-review.googlesource.com/changes/playground%2Fgerrit-cq~1563638/revisions/2/related 1305 fmt.Fprint(w, `)]}' 1306 { 1307 "changes": [ 1308 { 1309 "project": "playground/gerrit-cq", 1310 "change_id": "If00fa4f207440d7f12fbfff8c05afa9077ab0c21", 1311 "commit": { 1312 "commit": "4d048b016cb4df4d5d2805f0d3d1042cb1d80671", 1313 "parents": [ 1314 { 1315 "commit": "cd7db096c014399c369ddddd319708c3f46752f5" 1316 } 1317 ], 1318 "author": { 1319 "name": "Andrii Shyshkalov", 1320 "email": "tandrii@chromium.org", 1321 "date": "2019-04-11 06:41:01.000000000", 1322 "tz": -420 1323 }, 1324 "subject": "p3 change" 1325 }, 1326 "_change_number": 1563639, 1327 "_revision_number": 1, 1328 "_current_revision_number": 1, 1329 "status": "NEW" 1330 }, 1331 { 1332 "project": "playground/gerrit-cq", 1333 "change_id": "I80bf05eb9124dc126490820ec192c77a24938622", 1334 "commit": { 1335 "commit": "bce1f3beea01b8b282001b01bd9ea442730d578e", 1336 "parents": [ 1337 { 1338 "commit": "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f" 1339 } 1340 ], 1341 "author": { 1342 "name": "Andrii Shyshkalov", 1343 "email": "tandrii@chromium.org", 1344 "date": "2019-04-11 06:40:28.000000000", 1345 "tz": -420 1346 }, 1347 "subject": "p2 change" 1348 }, 1349 "_change_number": 1563638, 1350 "_revision_number": 2, 1351 "_current_revision_number": 2, 1352 "status": "NEW" 1353 }, 1354 { 1355 "project": "playground/gerrit-cq", 1356 "change_id": "Icf12c110abc0cbc0c7d01a40dc047683634a62d7", 1357 "commit": { 1358 "commit": "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f", 1359 "parents": [ 1360 { 1361 "commit": "f8e5384ee591cd5105113098d24c60e750b6c4f6" 1362 } 1363 ], 1364 "author": { 1365 "name": "Andrii Shyshkalov", 1366 "email": "tandrii@chromium.org", 1367 "date": "2019-04-11 06:40:18.000000000", 1368 "tz": -420 1369 }, 1370 "subject": "p1 change" 1371 }, 1372 "_change_number": 1563637, 1373 "_revision_number": 1, 1374 "_current_revision_number": 1, 1375 "status": "NEW" 1376 } 1377 ] 1378 } 1379 `) 1380 }) 1381 defer srv.Close() 1382 1383 rcs, err := c.GetRelatedChanges(ctx, &gerritpb.GetRelatedChangesRequest{ 1384 Number: 1563638, 1385 Project: "playground/gerrit-cq", 1386 RevisionId: "2", 1387 }) 1388 So(err, ShouldBeNil) 1389 So(actualURL.EscapedPath(), ShouldEqual, "/changes/playground%2Fgerrit-cq~1563638/revisions/2/related") 1390 So(rcs, ShouldResembleProto, &gerritpb.GetRelatedChangesResponse{ 1391 Changes: []*gerritpb.GetRelatedChangesResponse_ChangeAndCommit{ 1392 { 1393 Project: "playground/gerrit-cq", 1394 Commit: &gerritpb.CommitInfo{ 1395 Id: "4d048b016cb4df4d5d2805f0d3d1042cb1d80671", 1396 Parents: []*gerritpb.CommitInfo_Parent{{Id: "cd7db096c014399c369ddddd319708c3f46752f5"}}, 1397 Author: &gerritpb.GitPersonInfo{ 1398 Name: "Andrii Shyshkalov", 1399 Email: "tandrii@chromium.org", 1400 }, 1401 }, 1402 Number: 1563639, 1403 Patchset: 1, 1404 CurrentPatchset: 1, 1405 Status: gerritpb.ChangeStatus_NEW, 1406 }, 1407 { 1408 Project: "playground/gerrit-cq", 1409 Commit: &gerritpb.CommitInfo{ 1410 Id: "bce1f3beea01b8b282001b01bd9ea442730d578e", 1411 Parents: []*gerritpb.CommitInfo_Parent{{Id: "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f"}}, 1412 Author: &gerritpb.GitPersonInfo{ 1413 Name: "Andrii Shyshkalov", 1414 Email: "tandrii@chromium.org", 1415 }, 1416 }, 1417 Number: 1563638, 1418 Patchset: 2, 1419 CurrentPatchset: 2, 1420 Status: gerritpb.ChangeStatus_NEW, 1421 }, 1422 { 1423 Project: "playground/gerrit-cq", 1424 Commit: &gerritpb.CommitInfo{ 1425 Id: "fdd1f6d3875e68c99303ebfb25dd5d097e91c83f", 1426 Parents: []*gerritpb.CommitInfo_Parent{{Id: "f8e5384ee591cd5105113098d24c60e750b6c4f6"}}, 1427 Author: &gerritpb.GitPersonInfo{ 1428 Name: "Andrii Shyshkalov", 1429 Email: "tandrii@chromium.org", 1430 }, 1431 }, 1432 Number: 1563637, 1433 Patchset: 1, 1434 CurrentPatchset: 1, 1435 Status: gerritpb.ChangeStatus_NEW, 1436 }, 1437 }, 1438 }) 1439 }) 1440 } 1441 1442 func TestGetFileOwners(t *testing.T) { 1443 t.Parallel() 1444 ctx := context.Background() 1445 Convey("Get Owners: ", t, func() { 1446 Convey("Details", func() { 1447 var actualURL *url.URL 1448 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1449 actualURL = r.URL 1450 w.WriteHeader(200) 1451 w.Header().Set("Content-Type", "application/json") 1452 fmt.Fprint(w, `)]}' 1453 {"code_owners":[{ 1454 "account":{ 1455 "_account_id":1000096, 1456 "name":"User Name", 1457 "email":"user@test.com", 1458 "avatars":[{"url":"https://test.com/photo.jpg","height":32}] 1459 }}]}`) 1460 }) 1461 defer srv.Close() 1462 1463 resp, err := c.ListFileOwners(ctx, &gerritpb.ListFileOwnersRequest{ 1464 Project: "projectName", 1465 Ref: "main", 1466 Path: "path/to/file", 1467 Options: &gerritpb.AccountOptions{ 1468 Details: true, 1469 }, 1470 }) 1471 So(err, ShouldBeNil) 1472 So(actualURL.Path, ShouldEqual, "/projects/projectName/branches/main/code_owners/path/to/file") 1473 So(actualURL.Query().Get("o"), ShouldEqual, "DETAILS") 1474 So(resp, ShouldResemble, &gerritpb.ListOwnersResponse{ 1475 Owners: []*gerritpb.OwnerInfo{ 1476 { 1477 Account: &gerritpb.AccountInfo{ 1478 AccountId: 1000096, 1479 Name: "User Name", 1480 Email: "user@test.com", 1481 }, 1482 }, 1483 }, 1484 }) 1485 }) 1486 Convey("All Emails", func() { 1487 var actualURL *url.URL 1488 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1489 actualURL = r.URL 1490 w.WriteHeader(200) 1491 w.Header().Set("Content-Type", "application/json") 1492 fmt.Fprint(w, `)]}' 1493 {"code_owners": [{ 1494 "account": { 1495 "_account_id": 1000096, 1496 "email": "test@test.com", 1497 "secondary_emails": ["alt@test.com"] 1498 }}]}`) 1499 }) 1500 defer srv.Close() 1501 1502 resp, err := c.ListFileOwners(ctx, &gerritpb.ListFileOwnersRequest{ 1503 Project: "projectName", 1504 Ref: "main", 1505 Path: "path/to/file", 1506 Options: &gerritpb.AccountOptions{ 1507 AllEmails: true, 1508 }, 1509 }) 1510 So(err, ShouldBeNil) 1511 So(actualURL.Path, ShouldEqual, "/projects/projectName/branches/main/code_owners/path/to/file") 1512 So(actualURL.Query().Get("o"), ShouldEqual, "ALL_EMAILS") 1513 So(resp, ShouldResemble, &gerritpb.ListOwnersResponse{ 1514 Owners: []*gerritpb.OwnerInfo{ 1515 { 1516 Account: &gerritpb.AccountInfo{ 1517 AccountId: 1000096, 1518 Email: "test@test.com", 1519 SecondaryEmails: []string{"alt@test.com"}, 1520 }, 1521 }, 1522 }, 1523 }) 1524 }) 1525 }) 1526 } 1527 1528 func TestListProjects(t *testing.T) { 1529 t.Parallel() 1530 ctx := context.Background() 1531 1532 Convey("List Projects", t, func() { 1533 Convey("...works for a single ref", func() { 1534 var actualURL *url.URL 1535 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1536 actualURL = r.URL 1537 w.WriteHeader(200) 1538 w.Header().Set("Content-Type", "application/json") 1539 fmt.Fprint(w, `)]}' 1540 { 1541 "android_apks": { 1542 "id": "android_apks", 1543 "state": "ACTIVE", 1544 "branches": { 1545 "main": "82264ea131fcc2a386b83e38b962b370315c7c93" 1546 }, 1547 "web_links": [ 1548 { 1549 "name": "gitiles", 1550 "url": "https://chromium.googlesource.com/android_apks/", 1551 "target": "_blank" 1552 } 1553 ] 1554 } 1555 }`) 1556 }) 1557 defer srv.Close() 1558 1559 projects, err := c.ListProjects(ctx, &gerritpb.ListProjectsRequest{ 1560 Refs: []string{"refs/heads/main"}, 1561 }) 1562 So(err, ShouldBeNil) 1563 So(actualURL.Path, ShouldEqual, "/projects/") 1564 So(actualURL.Query().Get("b"), ShouldEqual, "refs/heads/main") 1565 So(projects, ShouldResemble, &gerritpb.ListProjectsResponse{ 1566 Projects: map[string]*gerritpb.ProjectInfo{ 1567 "android_apks": { 1568 Name: "android_apks", 1569 State: gerritpb.ProjectInfo_PROJECT_STATE_ACTIVE, 1570 Refs: map[string]string{ 1571 "refs/heads/main": "82264ea131fcc2a386b83e38b962b370315c7c93", 1572 }, 1573 WebLinks: []*gerritpb.WebLinkInfo{ 1574 { 1575 Name: "gitiles", 1576 Url: "https://chromium.googlesource.com/android_apks/", 1577 }, 1578 }, 1579 }, 1580 }, 1581 }) 1582 }) 1583 1584 Convey("...works for multiple refs", func() { 1585 var actualURL *url.URL 1586 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1587 actualURL = r.URL 1588 w.WriteHeader(200) 1589 w.Header().Set("Content-Type", "application/json") 1590 fmt.Fprint(w, `)]}' 1591 { 1592 "android_apks": { 1593 "id": "android_apks", 1594 "state": "ACTIVE", 1595 "branches": { 1596 "main": "82264ea131fcc2a386b83e38b962b370315c7c93", 1597 "master": "82264ea131fcc2a386b83e38b962b370315c7c93" 1598 }, 1599 "web_links": [ 1600 { 1601 "name": "gitiles", 1602 "url": "https://chromium.googlesource.com/android_apks/", 1603 "target": "_blank" 1604 } 1605 ] 1606 } 1607 }`) 1608 }) 1609 defer srv.Close() 1610 1611 projects, err := c.ListProjects(ctx, &gerritpb.ListProjectsRequest{ 1612 Refs: []string{"refs/heads/main", "refs/heads/master"}, 1613 }) 1614 So(err, ShouldBeNil) 1615 So(actualURL.Path, ShouldEqual, "/projects/") 1616 So(actualURL.Query()["b"], ShouldResemble, []string{"refs/heads/main", "refs/heads/master"}) 1617 So(projects, ShouldResemble, &gerritpb.ListProjectsResponse{ 1618 Projects: map[string]*gerritpb.ProjectInfo{ 1619 "android_apks": { 1620 Name: "android_apks", 1621 State: gerritpb.ProjectInfo_PROJECT_STATE_ACTIVE, 1622 Refs: map[string]string{ 1623 "refs/heads/main": "82264ea131fcc2a386b83e38b962b370315c7c93", 1624 "refs/heads/master": "82264ea131fcc2a386b83e38b962b370315c7c93", 1625 }, 1626 WebLinks: []*gerritpb.WebLinkInfo{ 1627 { 1628 Name: "gitiles", 1629 Url: "https://chromium.googlesource.com/android_apks/", 1630 }, 1631 }, 1632 }, 1633 }, 1634 }) 1635 }) 1636 }) 1637 } 1638 1639 func TestGetBranchInfo(t *testing.T) { 1640 t.Parallel() 1641 ctx := context.Background() 1642 1643 Convey("Get Branch Info", t, func() { 1644 var actualURL *url.URL 1645 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1646 actualURL = r.URL 1647 w.WriteHeader(200) 1648 w.Header().Set("Content-Type", "application/json") 1649 fmt.Fprint(w, `)]}' 1650 { 1651 "web_links": [ 1652 { 1653 "name": "gitiles", 1654 "url": "https://chromium.googlesource.com/infra/experimental/+/refs/heads/main", 1655 "target": "_blank" 1656 } 1657 ], 1658 "ref": "refs/heads/main", 1659 "revision": "10e5c33f63a843440cbe6c9c6cbc1bf513c598eb", 1660 "can_delete": true 1661 }`) 1662 }) 1663 defer srv.Close() 1664 1665 bi, err := c.GetRefInfo(ctx, &gerritpb.RefInfoRequest{ 1666 Project: "infra/experimental", 1667 Ref: "refs/heads/main", 1668 }) 1669 So(err, ShouldBeNil) 1670 1671 So(actualURL.Path, ShouldEqual, "/projects/infra/experimental/branches/refs/heads/main") 1672 So(bi, ShouldResemble, &gerritpb.RefInfo{ 1673 Ref: "refs/heads/main", 1674 Revision: "10e5c33f63a843440cbe6c9c6cbc1bf513c598eb", 1675 }) 1676 }) 1677 } 1678 1679 func TestGetPureRevert(t *testing.T) { 1680 t.Parallel() 1681 ctx := context.Background() 1682 1683 Convey("Get Pure Revert", t, func() { 1684 var actualURL *url.URL 1685 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1686 actualURL = r.URL 1687 w.WriteHeader(200) 1688 w.Header().Set("Content-Type", "application/json") 1689 fmt.Fprint(w, `)]}' 1690 { 1691 "is_pure_revert" : false 1692 }`) 1693 }) 1694 defer srv.Close() 1695 1696 req := &gerritpb.GetPureRevertRequest{ 1697 Number: 42, 1698 Project: "someproject", 1699 } 1700 res, err := c.GetPureRevert(ctx, req) 1701 So(err, ShouldBeNil) 1702 So(actualURL.Path, ShouldEqual, "/changes/someproject~42/pure_revert") 1703 So(res, ShouldResemble, &gerritpb.PureRevertInfo{ 1704 IsPureRevert: false, 1705 }) 1706 }) 1707 } 1708 1709 func TestGerritError(t *testing.T) { 1710 t.Parallel() 1711 ctx := context.Background() 1712 1713 Convey("Gerrit returns", t, func() { 1714 // All APIs share the same error handling code path, so use SubmitChange as 1715 // an example. 1716 req := &gerritpb.SubmitChangeRequest{Number: 1} 1717 Convey("HTTP 400", func() { 1718 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1719 w.WriteHeader(400) 1720 w.Header().Set("Content-Type", "text/plain") 1721 w.Write([]byte("invalid request: xyz is required")) 1722 }) 1723 defer srv.Close() 1724 _, err := c.SubmitChange(ctx, req) 1725 So(grpcutil.Code(err), ShouldEqual, codes.InvalidArgument) 1726 }) 1727 Convey("HTTP 403", func() { 1728 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1729 w.WriteHeader(403) 1730 }) 1731 defer srv.Close() 1732 _, err := c.SubmitChange(ctx, req) 1733 So(grpcutil.Code(err), ShouldEqual, codes.PermissionDenied) 1734 }) 1735 Convey("HTTP 404 ", func() { 1736 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1737 w.WriteHeader(404) 1738 }) 1739 defer srv.Close() 1740 _, err := c.SubmitChange(ctx, req) 1741 So(grpcutil.Code(err), ShouldEqual, codes.NotFound) 1742 }) 1743 Convey("HTTP 409 ", func() { 1744 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1745 w.WriteHeader(409) 1746 w.Header().Set("Content-Type", "text/plain") 1747 w.Write([]byte("block by Verified")) 1748 }) 1749 defer srv.Close() 1750 _, err := c.SubmitChange(ctx, req) 1751 So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition) 1752 So(err, ShouldErrLike, "block by Verified") 1753 }) 1754 Convey("HTTP 412 ", func() { 1755 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1756 w.WriteHeader(412) 1757 w.Header().Set("Content-Type", "text/plain") 1758 _, _ = w.Write([]byte("precondition failed")) 1759 }) 1760 defer srv.Close() 1761 _, err := c.SubmitChange(ctx, req) 1762 So(grpcutil.Code(err), ShouldEqual, codes.FailedPrecondition) 1763 So(err, ShouldErrLike, "precondition failed") 1764 }) 1765 Convey("HTTP 429 ", func() { 1766 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1767 w.WriteHeader(429) 1768 }) 1769 defer srv.Close() 1770 _, err := c.SubmitChange(ctx, req) 1771 So(grpcutil.Code(err), ShouldEqual, codes.ResourceExhausted) 1772 }) 1773 Convey("HTTP 503 ", func() { 1774 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1775 w.WriteHeader(503) 1776 }) 1777 defer srv.Close() 1778 _, err := c.SubmitChange(ctx, req) 1779 So(grpcutil.Code(err), ShouldEqual, codes.Unavailable) 1780 }) 1781 }) 1782 } 1783 1784 func TestGetMetaDiff(t *testing.T) { 1785 t.Parallel() 1786 ctx := context.Background() 1787 1788 sampleResp := `)]}'{ 1789 "old_change_info": { 1790 "_number": 1, 1791 "project": "example/repo", 1792 "status": "NEW", 1793 "branch": "main", 1794 "attention_set": { 1795 "22222": { 1796 "account": { 1797 "_account_id": 22222 1798 }, 1799 "last_update": "2014-01-02 18:37:10.000000000", 1800 "reason": "ps#1: CQ dry run succeeded." 1801 } 1802 }, 1803 "removed_from_attention_set": { 1804 "11111": { 1805 "account": { 1806 "_account_id": 11111 1807 }, 1808 "last_update": "2022-05-31 19:02:58.000000000", 1809 "reason": "<GERRIT_ACCOUNT_11111> replied on the change", 1810 "reason_account": { 1811 "_account_id": 11111 1812 } 1813 } 1814 }, 1815 "created": "2014-01-01 18:26:55.000000000", 1816 "updated": "2014-01-01 20:23:59.000000000", 1817 "meta_rev_id": "cafeefac", 1818 "owner": { 1819 "_account_id": 1234567 1820 }, 1821 "requirements": [ 1822 { 1823 "status": "OK", 1824 "fallback_text": "Code-Owners", 1825 "type": "code-owners" 1826 } 1827 ], 1828 "submit_records": [ 1829 { 1830 "rule_name": "Code-Owners", 1831 "status": "OK", 1832 "requirements": [ 1833 { 1834 "status": "OK", 1835 "fallback_text": "Code-Owners", 1836 "type": "code-owners" 1837 } 1838 ] 1839 } 1840 ] 1841 }, 1842 "new_change_info": { 1843 "_number": 1, 1844 "project": "example/repo", 1845 "status": "NEW", 1846 "branch": "main", 1847 "attention_set": { 1848 "33333": { 1849 "account": { 1850 "_account_id": 33333 1851 }, 1852 "last_update": "2014-01-02 20:25:28.000000000", 1853 "reason": "<GERRIT_ACCOUNT_33333> replied on the change", 1854 "reason_account": { 1855 "_account_id": 33333 1856 } 1857 } 1858 }, 1859 "removed_from_attention_set": { 1860 "22222": { 1861 "account": { 1862 "_account_id": 22222 1863 }, 1864 "last_update": "2014-01-02 20:25:28.000000000", 1865 "reason": "<GERRIT_ACCOUNT_22222> replied on the change", 1866 "reason_account": { 1867 "_account_id": 22222 1868 } 1869 } 1870 }, 1871 "created": "2014-01-01 18:26:55.000000000", 1872 "updated": "2014-01-01 20:25:28.000000000", 1873 "meta_rev_id": "cafecafe", 1874 "owner": { 1875 "_account_id": 1234567 1876 }, 1877 "requirements": [ 1878 { 1879 "status": "OK", 1880 "fallback_text": "Code-Owners", 1881 "type": "code-owners" 1882 } 1883 ], 1884 "submit_records": [ 1885 { 1886 "rule_name": "Code-Owners", 1887 "status": "OK", 1888 "requirements": [ 1889 { 1890 "status": "OK", 1891 "fallback_text": "Code-Owners", 1892 "type": "code-owners" 1893 } 1894 ] 1895 } 1896 ] 1897 }, 1898 "added": { 1899 "attention_set": { 1900 "33333": { 1901 "account": { 1902 "_account_id": 33333 1903 }, 1904 "last_update": "2022-05-31 20:25:28.000000000", 1905 "reason": "<GERRIT_ACCOUNT_33333> replied on the change", 1906 "reason_account": { 1907 "_account_id": 33333 1908 } 1909 } 1910 }, 1911 "removed_from_attention_set": { 1912 "22222": { 1913 "account": { 1914 "_account_id": 22222 1915 }, 1916 "last_update": "2022-05-31 20:25:28.000000000", 1917 "reason": "<GERRIT_ACCOUNT_22222> replied on the change", 1918 "reason_account": { 1919 "_account_id": 22222 1920 } 1921 } 1922 }, 1923 "updated": "2014-01-02 20:25:28.000000000", 1924 "meta_rev_id": "cafecafe" 1925 }, 1926 "removed": { 1927 "attention_set": { 1928 "1147264": { 1929 "account": { 1930 "_account_id": 1147264 1931 }, 1932 "last_update": "2022-05-31 18:37:10.000000000", 1933 "reason": "ps#1: CQ dry run succeeded." 1934 } 1935 }, 1936 "removed_from_attention_set": { 1937 "22222": { 1938 "account": { 1939 "_account_id": 22222 1940 }, 1941 "last_update": "2014-01-02 19:02:58.000000000", 1942 "reason": "<GERRIT_ACCOUNT_22222> replied on the change", 1943 "reason_account": { 1944 "_account_id": 22222 1945 } 1946 } 1947 }, 1948 "updated": "2014-01-02 20:23:59.000000000", 1949 "meta_rev_id": "cafeefac" 1950 } 1951 }` 1952 1953 Convey("GetMetaDiff", t, func() { 1954 Convey("Validates args", func() { 1955 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) {}) 1956 defer srv.Close() 1957 _, err := c.GetMetaDiff(ctx, &gerritpb.GetMetaDiffRequest{}) 1958 So(err, ShouldErrLike, "number must be positive") 1959 }) 1960 1961 var actualRequest *http.Request 1962 srv, c := newMockPbClient(func(w http.ResponseWriter, r *http.Request) { 1963 actualRequest = r 1964 w.WriteHeader(200) 1965 w.Header().Set("Content-Type", "application/json") 1966 fmt.Fprint(w, sampleResp) 1967 }) 1968 defer srv.Close() 1969 req := &gerritpb.GetMetaDiffRequest{Number: 1} 1970 1971 Convey("Works", func() { 1972 resp, err := c.GetMetaDiff(ctx, req) 1973 So(err, ShouldBeNil) 1974 So(resp.OldChangeInfo.Number, ShouldEqual, 1) 1975 So(resp.NewChangeInfo.Number, ShouldEqual, 1) 1976 So(resp.Added.MetaRevId, ShouldEqual, resp.NewChangeInfo.MetaRevId) 1977 So(resp.Removed.MetaRevId, ShouldEqual, resp.OldChangeInfo.MetaRevId) 1978 }) 1979 1980 Convey("Passes old and meta", func() { 1981 req.Old = "mehmeh" 1982 req.Meta = "booboo" 1983 _, err := c.GetMetaDiff(ctx, req) 1984 So(err, ShouldBeNil) 1985 So(actualRequest.URL.Query()["old"], ShouldResemble, []string{"mehmeh"}) 1986 So(actualRequest.URL.Query()["meta"], ShouldResemble, []string{"booboo"}) 1987 }) 1988 }) 1989 1990 } 1991 1992 func newMockPbClient(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, gerritpb.GerritClient) { 1993 // TODO(tandrii): rename this func once newMockClient name is no longer used in the same package. 1994 srv := httptest.NewServer(http.HandlerFunc(handler)) 1995 return srv, &client{testBaseURL: srv.URL} 1996 } 1997 1998 // parseTime parses a RFC3339Nano formatted timestamp string. 1999 // Panics when error occurs during parse. 2000 func parseTime(t string) time.Time { 2001 ret, err := time.Parse(time.RFC3339Nano, t) 2002 if err != nil { 2003 panic(err) 2004 } 2005 return ret 2006 }