go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gerrit/gerrit_test.go (about) 1 // Copyright 2017 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 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "net/http" 23 "net/http/httptest" 24 "net/url" 25 "testing" 26 27 "go.chromium.org/luci/common/retry" 28 29 . "github.com/smartystreets/goconvey/convey" 30 ) 31 32 func TestGerritURL(t *testing.T) { 33 t.Parallel() 34 Convey("Malformed", t, func() { 35 f := func(arg string) { 36 So(ValidateGerritURL(arg), ShouldNotBeNil) 37 _, err := NormalizeGerritURL(arg) 38 So(err, ShouldNotBeNil) 39 } 40 41 f("what/\\is\this") 42 f("https://example.com/") 43 f("http://bad-protocol-review.googlesource.com/") 44 f("no-protocol-review.googlesource.com/") 45 f("https://a-review.googlesource.com/path-and#fragment") 46 f("https://a-review.googlesource.com/any-path-actually") 47 }) 48 49 Convey("OK", t, func() { 50 f := func(arg, exp string) { 51 So(ValidateGerritURL(arg), ShouldBeNil) 52 act, err := NormalizeGerritURL(arg) 53 So(err, ShouldBeNil) 54 So(act, ShouldEqual, exp) 55 } 56 f("https://a-review.googlesource.com", "https://a-review.googlesource.com/") 57 f("https://a-review.googlesource.com/", "https://a-review.googlesource.com/") 58 f("https://chromium-review.googlesource.com/", "https://chromium-review.googlesource.com/") 59 f("https://chromium-review.googlesource.com", "https://chromium-review.googlesource.com/") 60 }) 61 } 62 63 func TestNewClient(t *testing.T) { 64 t.Parallel() 65 Convey("Malformed", t, func() { 66 f := func(arg string) { 67 _, err := NewClient(http.DefaultClient, arg) 68 So(err, ShouldNotBeNil) 69 } 70 f("badurl") 71 f("http://a.googlesource.com") 72 f("https://a/") 73 }) 74 Convey("OK", t, func() { 75 f := func(arg string) { 76 _, err := NewClient(http.DefaultClient, arg) 77 So(err, ShouldBeNil) 78 } 79 f("https://a-review.googlesource.com/") 80 f("https://a-review.googlesource.com") 81 }) 82 } 83 84 func TestQuery(t *testing.T) { 85 t.Parallel() 86 ctx := context.Background() 87 88 Convey("ChangeQuery", t, func() { 89 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 90 w.WriteHeader(200) 91 w.Header().Set("Content-Type", "application/json") 92 fmt.Fprintf(w, ")]}'\n[%s]\n", fakeCL1Str) 93 }) 94 defer srv.Close() 95 96 Convey("Basic", func() { 97 cls, more, err := c.ChangeQuery(ctx, 98 ChangeQueryParams{ 99 Query: "some_query", 100 }) 101 So(err, ShouldBeNil) 102 So(len(cls), ShouldEqual, 1) 103 So(cls[0].Owner.AccountID, ShouldEqual, 1118104) 104 So(more, ShouldBeFalse) 105 }) 106 }) 107 108 Convey("ChangeQuery with more changes", t, func() { 109 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 110 w.WriteHeader(200) 111 w.Header().Set("Content-Type", "application/json") 112 fmt.Fprintf(w, ")]}'\n[%s]\n", fakeCL2Str) 113 }) 114 defer srv.Close() 115 116 Convey("Basic", func() { 117 cls, more, err := c.ChangeQuery(ctx, 118 ChangeQueryParams{ 119 Query: "4efbec9a685b238fced35b81b7f3444dc60150b1", 120 }) 121 So(err, ShouldBeNil) 122 So(len(cls), ShouldEqual, 1) 123 So(cls[0].Owner.AccountID, ShouldEqual, 1178184) 124 So(more, ShouldBeFalse) 125 }) 126 }) 127 128 Convey("ChangeQuery returns no changes", t, func() { 129 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 130 w.WriteHeader(200) 131 w.Header().Set("Content-Type", "application/json") 132 fmt.Fprint(w, ")]}'\n[]\n", fakeCL2Str) 133 }) 134 defer srv.Close() 135 136 Convey("Basic", func() { 137 cls, more, err := c.ChangeQuery(ctx, 138 ChangeQueryParams{ 139 Query: "4efbec9a685b238fced35b81b7f3444dc60150b1", 140 }) 141 So(err, ShouldBeNil) 142 So(cls, ShouldResemble, []*Change{}) 143 So(more, ShouldBeFalse) 144 }) 145 }) 146 } 147 148 func TestChangeDetails(t *testing.T) { 149 t.Parallel() 150 ctx := context.Background() 151 152 Convey("Details", t, func() { 153 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 154 w.WriteHeader(200) 155 w.Header().Set("Content-Type", "application/json") 156 fmt.Fprintf(w, ")]}'\n%s\n", fakeCL3Str) 157 }) 158 defer srv.Close() 159 160 Convey("WithOptions", func() { 161 options := ChangeDetailsParams{Options: []string{"CURRENT_REVISION"}} 162 cl, err := c.ChangeDetails(ctx, "629279", options) 163 So(err, ShouldBeNil) 164 So(cl.RevertOf, ShouldEqual, 629277) 165 So(cl.CurrentRevision, ShouldEqual, "1ee75012c0de") 166 }) 167 168 }) 169 170 Convey("Retry", t, func() { 171 var attempts int 172 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 173 // First attempt fails, second succeeds. 174 if attempts == 0 { 175 w.WriteHeader(500) 176 w.Header().Set("Content-Type", "text/plain") 177 fmt.Fprintf(w, "Internal server error") 178 } else { 179 w.WriteHeader(200) 180 w.Header().Set("Content-Type", "application/json") 181 fmt.Fprintf(w, ")]}'\n%s\n", fakeCL3Str) 182 } 183 attempts++ 184 }) 185 defer srv.Close() 186 187 cl, err := c.ChangeDetails(ctx, "629279", ChangeDetailsParams{}) 188 So(err, ShouldBeNil) 189 So(cl.RevertOf, ShouldEqual, 629277) 190 So(cl.CurrentRevision, ShouldEqual, "1ee75012c0de") 191 So(attempts, ShouldEqual, 2) 192 }) 193 } 194 195 func TestListChangeComments(t *testing.T) { 196 t.Parallel() 197 ctx := context.Background() 198 199 Convey("ListComments", t, func() { 200 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 201 w.WriteHeader(200) 202 w.Header().Set("Content-Type", "application/json") 203 fmt.Fprintf(w, ")]}'\n%s\n", fakeComments1Str) 204 }) 205 defer srv.Close() 206 207 Convey("WithOptions", func() { 208 comments, err := c.ListChangeComments(ctx, "629279", "") 209 So(err, ShouldBeNil) 210 So(comments["foo"][0].Line, ShouldEqual, 3) 211 So(comments["foo"][0].Range.StartLine, ShouldEqual, 3) 212 So(comments["bar"][0].Line, ShouldEqual, 21) 213 }) 214 215 }) 216 217 } 218 219 func TestListRobotComments(t *testing.T) { 220 t.Parallel() 221 ctx := context.Background() 222 223 Convey("ListRobotComments", t, func() { 224 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 225 w.WriteHeader(200) 226 w.Header().Set("Content-Type", "application/json") 227 fmt.Fprintf(w, ")]}'\n%s\n", fakeRobotComments1Str) 228 }) 229 defer srv.Close() 230 231 Convey("WithOptions", func() { 232 comments, err := c.ListRobotComments(ctx, "629279", "deadbeef") 233 So(err, ShouldBeNil) 234 So(comments["foo"][0].Line, ShouldEqual, 3) 235 So(comments["foo"][0].Range.StartLine, ShouldEqual, 3) 236 So(comments["foo"][0].RobotID, ShouldEqual, "somerobot") 237 So(comments["foo"][0].RobotRunID, ShouldEqual, "run1") 238 So(comments["bar"][0].Line, ShouldEqual, 21) 239 }) 240 }) 241 } 242 243 func TestAccountQuery(t *testing.T) { 244 t.Parallel() 245 ctx := context.Background() 246 247 Convey("Account-Query", t, func(c C) { 248 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 249 w.WriteHeader(200) 250 w.Header().Set("Content-Type", "application/json") 251 fmt.Fprintf(w, ")]}'\n%s\n", fakeAccounts1Str) 252 }) 253 defer srv.Close() 254 255 Convey("WithOptions", func() { 256 accounts, more, err := client.AccountQuery(ctx, AccountQueryParams{Query: "email:nobody@example.com"}) 257 So(err, ShouldBeNil) 258 So(more, ShouldEqual, false) 259 So(accounts[0].Name, ShouldEqual, "John Doe") 260 So(accounts[1].Name, ShouldEqual, "Jane Doe") 261 }) 262 }) 263 } 264 265 func TestChangesSubmittedTogether(t *testing.T) { 266 t.Parallel() 267 ctx := context.Background() 268 269 Convey("SubmittedTogether", t, func() { 270 var nonVisibleResp string 271 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 272 w.WriteHeader(200) 273 w.Header().Set("Content-Type", "application/json") 274 fmt.Fprintf(w, ")]}'\n{ \"changes\":[%s,%s]%s}\n", fakeCL1Str, fakeCL6Str, nonVisibleResp) 275 }) 276 defer srv.Close() 277 278 Convey("WithCurrentRevisionOptions", func() { 279 nonVisibleResp = "" 280 options := ChangeDetailsParams{Options: []string{"CURRENT_REVISION"}} 281 cls, err := c.ChangesSubmittedTogether(ctx, "627036", options) 282 So(err, ShouldBeNil) 283 So(cls.Changes[0].CurrentRevision, ShouldEqual, "eb2388b592a9") 284 So(cls.Changes[1].CurrentRevision, ShouldEqual, "d6375c2ea5b0") 285 }) 286 Convey("WithNonVisibleChangesOptions", func() { 287 nonVisibleResp = ",\"non_visible_changes\":1" 288 options := ChangeDetailsParams{Options: []string{"CURRENT_REVISION", "NON_VISIBLE_CHANGES"}} 289 cls, err := c.ChangesSubmittedTogether(ctx, "627036", options) 290 So(err, ShouldBeNil) 291 So(cls.Changes[0].CurrentRevision, ShouldEqual, "eb2388b592a9") 292 So(cls.Changes[1].CurrentRevision, ShouldEqual, "d6375c2ea5b0") 293 So(cls.NonVisibleChanges, ShouldEqual, 1) 294 }) 295 296 }) 297 } 298 299 func TestMergeable(t *testing.T) { 300 t.Parallel() 301 ctx := context.Background() 302 303 Convey("GetMergeable", t, func() { 304 var resp string 305 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 306 w.WriteHeader(200) 307 w.Header().Set("Content-Type", "application/json") 308 fmt.Fprintf(w, ")]}'\n%s\n", resp) 309 }) 310 defer srv.Close() 311 312 Convey("yes", func() { 313 resp = `{ 314 "submit_type": "REBASE_ALWAYS", 315 "strategy": "recursive", 316 "mergeable": true, 317 "commit_merged": false, 318 "content_merged": false 319 }` 320 cls, err := c.GetMergeable(ctx, "627036", "eb2388b592a9") 321 So(err, ShouldBeNil) 322 So(cls.Mergeable, ShouldEqual, true) 323 }) 324 325 Convey("no", func() { 326 resp = `{ 327 "submit_type": "REBASE_ALWAYS", 328 "strategy": "recursive", 329 "mergeable": false, 330 "commit_merged": false, 331 "content_merged": false 332 }` 333 cls, err := c.GetMergeable(ctx, "646267", "d6375c2ea5b0") 334 So(err, ShouldBeNil) 335 So(cls.Mergeable, ShouldEqual, false) 336 }) 337 338 }) 339 } 340 341 func TestChangeLabels(t *testing.T) { 342 t.Parallel() 343 ctx := context.Background() 344 345 Convey("Labels", t, func() { 346 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 347 w.WriteHeader(200) 348 w.Header().Set("Content-Type", "application/json") 349 fmt.Fprintf(w, ")]}'\n%s\n", fakeCL5Str) 350 }) 351 defer srv.Close() 352 353 Convey("All", func() { 354 options := ChangeDetailsParams{Options: []string{"DETAILED_LABELS"}} 355 cl, err := c.ChangeDetails(ctx, "629279", options) 356 So(err, ShouldBeNil) 357 So(len(cl.Labels["Code-Review"].All), ShouldEqual, 2) 358 So(cl.Labels["Code-Review"].All[0].Value, ShouldEqual, -1) 359 So(cl.Labels["Code-Review"].All[0].Username, ShouldEqual, "jdoe") 360 So(cl.Labels["Code-Review"].All[1].Value, ShouldEqual, 1) 361 So(cl.Labels["Code-Review"].All[1].Username, ShouldEqual, "jroe") 362 So(len(cl.Labels["Code-Review"].Values), ShouldEqual, 5) 363 So(len(cl.Labels["Verified"].Values), ShouldEqual, 3) 364 }) 365 366 }) 367 368 } 369 370 func TestCreateChange(t *testing.T) { 371 t.Parallel() 372 ctx := context.Background() 373 374 Convey("CreateChange", t, func(c C) { 375 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 376 defer r.Body.Close() 377 378 var ci ChangeInput 379 err := json.NewDecoder(r.Body).Decode(&ci) 380 c.So(err, ShouldBeNil) 381 382 w.WriteHeader(200) 383 w.Header().Set("Content-Type", "application/json") 384 change := Change{ 385 ID: fmt.Sprintf("%s~%s~I8473b95934b5732ac55d26311a706c9c2bde9941", ci.Project, ci.Branch), 386 ChangeID: "I8473b95934b5732ac55d26311a706c9c2bde9941", 387 Project: ci.Project, 388 Branch: ci.Branch, 389 Subject: ci.Subject, 390 Topic: ci.Topic, 391 Status: "NEW", 392 // the rest omitted for brevity... 393 } 394 var buffer bytes.Buffer 395 err = json.NewEncoder(&buffer).Encode(&change) 396 c.So(err, ShouldBeNil) 397 fmt.Fprintf(w, ")]}'\n%s\n", buffer.String()) 398 }) 399 defer srv.Close() 400 401 Convey("Basic", func() { 402 ci := ChangeInput{ 403 Project: "infra/luci-go", 404 Branch: "master", 405 Subject: "Let's make a thing. Yeah, a thing.", 406 Topic: "something-something", 407 } 408 change, err := client.CreateChange(ctx, &ci) 409 So(err, ShouldBeNil) 410 So(change.Project, ShouldResemble, ci.Project) 411 So(change.Branch, ShouldResemble, ci.Branch) 412 So(change.Subject, ShouldResemble, ci.Subject) 413 So(change.Topic, ShouldResemble, ci.Topic) 414 So(change.Status, ShouldResemble, "NEW") 415 }) 416 417 }) 418 419 Convey("CreateChange but project non-existent", t, func() { 420 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 421 w.WriteHeader(404) 422 w.Header().Set("Content-Type", "text/plain") 423 fmt.Fprintf(w, "No such project: blah") 424 }) 425 defer srv.Close() 426 427 Convey("Basic", func() { 428 ci := ChangeInput{ 429 Project: "blah", 430 Branch: "master", 431 Subject: "beep bop boop I'm a robot", 432 Topic: "haha", 433 } 434 _, err := c.CreateChange(ctx, &ci) 435 So(err, ShouldNotBeNil) 436 }) 437 438 }) 439 } 440 441 func TestAbandonChange(t *testing.T) { 442 t.Parallel() 443 ctx := context.Background() 444 445 Convey("AbandonChange", t, func(c C) { 446 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 447 defer r.Body.Close() 448 449 var ai AbandonInput 450 err := json.NewDecoder(r.Body).Decode(&ai) 451 c.So(err, ShouldBeNil) 452 453 w.WriteHeader(200) 454 w.Header().Set("Content-Type", "application/json") 455 fmt.Fprintf(w, ")]}'\n%s\n", fakeCL4Str) 456 }) 457 defer srv.Close() 458 459 Convey("Basic", func() { 460 change, err := client.AbandonChange(ctx, "629279", nil) 461 So(err, ShouldBeNil) 462 So(change.Status, ShouldResemble, "ABANDONED") 463 }) 464 465 Convey("Basic with message", func() { 466 ai := AbandonInput{ 467 Message: "duplicate", 468 } 469 change, err := client.AbandonChange(ctx, "629279", &ai) 470 So(err, ShouldBeNil) 471 So(change.Status, ShouldResemble, "ABANDONED") 472 }) 473 }) 474 475 Convey("AbandonChange but change non-existent", t, func() { 476 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 477 w.WriteHeader(404) 478 w.Header().Set("Content-Type", "text/plain") 479 fmt.Fprintf(w, "No such change: 629279") 480 }) 481 defer srv.Close() 482 483 Convey("Basic", func() { 484 _, err := c.AbandonChange(ctx, "629279", nil) 485 So(err, ShouldNotBeNil) 486 }) 487 488 }) 489 } 490 491 func TestRebase(t *testing.T) { 492 t.Parallel() 493 ctx := context.Background() 494 495 Convey("RebaseChange", t, func(c C) { 496 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 497 defer r.Body.Close() 498 499 var ri RestoreInput 500 err := json.NewDecoder(r.Body).Decode(&ri) 501 c.So(err, ShouldBeNil) 502 503 w.WriteHeader(200) 504 w.Header().Set("Content-Type", "application/json") 505 fmt.Fprintf(w, ")]}'\n%s\n", fakeCL1Str) 506 }) 507 defer srv.Close() 508 509 Convey("Basic", func() { 510 change, err := client.RebaseChange(ctx, "627036", nil) 511 So(err, ShouldBeNil) 512 So(change.Status, ShouldResemble, "NEW") 513 }) 514 515 Convey("Basic with overridden base revision", func() { 516 ri := RebaseInput{ 517 Base: "abc123", 518 OnBehalfOfUploader: true, 519 AllowConflicts: false, 520 } 521 change, err := client.RebaseChange(ctx, "627036", &ri) 522 So(err, ShouldBeNil) 523 So(change.Status, ShouldResemble, "NEW") 524 }) 525 }) 526 527 Convey("RebaseChange with nontrivial merge conflict", t, func(c C) { 528 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 529 defer r.Body.Close() 530 531 var ri RebaseInput 532 err := json.NewDecoder(r.Body).Decode(&ri) 533 c.So(err, ShouldBeNil) 534 535 w.WriteHeader(409) 536 w.Header().Set("Content-Type", "text/plain") 537 fmt.Fprintf(w, "change has conflicts") 538 }) 539 defer srv.Close() 540 541 Convey("Basic", func() { 542 _, err := client.RebaseChange(ctx, "627036", nil) 543 So(err, ShouldNotBeNil) 544 }) 545 }) 546 } 547 548 func TestRestoreChange(t *testing.T) { 549 t.Parallel() 550 ctx := context.Background() 551 552 Convey("RestoreChange", t, func(c C) { 553 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 554 defer r.Body.Close() 555 556 var ri RestoreInput 557 err := json.NewDecoder(r.Body).Decode(&ri) 558 c.So(err, ShouldBeNil) 559 560 w.WriteHeader(200) 561 w.Header().Set("Content-Type", "application/json") 562 fmt.Fprintf(w, ")]}'\n%s\n", fakeCL1Str) 563 }) 564 defer srv.Close() 565 566 Convey("Basic", func() { 567 change, err := client.RestoreChange(ctx, "627036", nil) 568 So(err, ShouldBeNil) 569 So(change.Status, ShouldResemble, "NEW") 570 }) 571 572 Convey("Basic with message", func() { 573 ri := RestoreInput{ 574 Message: "restored", 575 } 576 change, err := client.RestoreChange(ctx, "627036", &ri) 577 So(err, ShouldBeNil) 578 So(change.Status, ShouldResemble, "NEW") 579 }) 580 }) 581 582 Convey("RestoreChange but change not abandoned", t, func(c C) { 583 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 584 defer r.Body.Close() 585 586 var ri RestoreInput 587 err := json.NewDecoder(r.Body).Decode(&ri) 588 c.So(err, ShouldBeNil) 589 590 w.WriteHeader(409) 591 w.Header().Set("Content-Type", "text/plain") 592 fmt.Fprintf(w, "change is new") 593 }) 594 defer srv.Close() 595 596 Convey("Basic", func() { 597 _, err := client.RestoreChange(ctx, "627036", nil) 598 So(err, ShouldNotBeNil) 599 }) 600 }) 601 602 Convey("RestoreChange but change non-existent", t, func(c C) { 603 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 604 defer r.Body.Close() 605 606 var ri RestoreInput 607 err := json.NewDecoder(r.Body).Decode(&ri) 608 c.So(err, ShouldBeNil) 609 610 w.WriteHeader(404) 611 w.Header().Set("Content-Type", "text/plain") 612 fmt.Fprintf(w, "No such change: 629279") 613 }) 614 defer srv.Close() 615 616 Convey("Basic", func() { 617 _, err := client.RestoreChange(ctx, "629279", nil) 618 So(err, ShouldNotBeNil) 619 }) 620 }) 621 } 622 623 func TestCreateBranch(t *testing.T) { 624 t.Parallel() 625 ctx := context.Background() 626 bi := BranchInput{ 627 Ref: "branch", 628 Revision: "08a8326653eaa5f7aeea30348b63bf5e9595dc11", 629 } 630 631 Convey("CreateBranch", t, func(c C) { 632 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 633 defer r.Body.Close() 634 635 var bi BranchInput 636 err := json.NewDecoder(r.Body).Decode(&bi) 637 c.So(err, ShouldBeNil) 638 639 w.WriteHeader(200) 640 w.Header().Set("Content-Type", "application/json") 641 info := BranchInfo{ 642 Ref: "branch", 643 Revision: "08a8326653eaa5f7aeea30348b63bf5e9595dc11", 644 } 645 var buffer bytes.Buffer 646 err = json.NewEncoder(&buffer).Encode(&info) 647 c.So(err, ShouldBeNil) 648 fmt.Fprintf(w, ")]}'\n%s\n", buffer.String()) 649 }) 650 defer srv.Close() 651 Convey("Basic", func() { 652 info, err := client.CreateBranch(ctx, "project", &bi) 653 So(err, ShouldBeNil) 654 So(info.Ref, ShouldEqual, "branch") 655 So(info.Revision, ShouldEqual, "08a8326653eaa5f7aeea30348b63bf5e9595dc11") 656 }) 657 }) 658 659 Convey("Not authorized", t, func(c C) { 660 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 661 defer r.Body.Close() 662 663 var bi BranchInput 664 err := json.NewDecoder(r.Body).Decode(&bi) 665 c.So(err, ShouldBeNil) 666 667 w.WriteHeader(403) 668 w.Header().Set("Content-Type", "text/plain") 669 fmt.Fprintf(w, "Not authorized to create ref") 670 }) 671 defer srv.Close() 672 673 Convey("Basic", func() { 674 _, err := client.CreateBranch(ctx, "project", &bi) 675 So(err, ShouldNotBeNil) 676 }) 677 }) 678 } 679 680 func TestIsPureRevert(t *testing.T) { 681 t.Parallel() 682 ctx := context.Background() 683 684 Convey("IsPureRevert", t, func() { 685 Convey("Bad change id", func() { 686 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 687 w.WriteHeader(404) 688 w.Header().Set("Content-Type", "text/plain") 689 fmt.Fprintf(w, "Not found: 629277") 690 }) 691 defer srv.Close() 692 693 _, err := c.IsChangePureRevert(ctx, "629277") 694 So(err, ShouldNotBeNil) 695 }) 696 Convey("Not revert", func() { 697 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 698 w.WriteHeader(400) 699 w.Header().Set("Content-Type", "text/plain") 700 fmt.Fprintf(w, "No ID was provided and change isn't a revert") 701 }) 702 defer srv.Close() 703 704 r, err := c.IsChangePureRevert(ctx, "629277") 705 So(err, ShouldBeNil) 706 So(r, ShouldBeFalse) 707 }) 708 Convey("Not pure revert", func() { 709 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 710 w.WriteHeader(200) 711 w.Header().Set("Content-Type", "application/json") 712 fmt.Fprintf(w, ")]}'\n%s\n", "{\"is_pure_revert\":false}") 713 }) 714 defer srv.Close() 715 716 r, err := c.IsChangePureRevert(ctx, "629277") 717 So(err, ShouldBeNil) 718 So(r, ShouldBeFalse) 719 }) 720 Convey("Pure revert", func() { 721 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 722 w.WriteHeader(200) 723 w.Header().Set("Content-Type", "application/json") 724 fmt.Fprintf(w, ")]}'\n%s\n", "{\"is_pure_revert\":true}") 725 }) 726 defer srv.Close() 727 728 r, err := c.IsChangePureRevert(ctx, "629277") 729 So(err, ShouldBeNil) 730 So(r, ShouldBeTrue) 731 }) 732 }) 733 } 734 735 func TestDirectSetReview(t *testing.T) { 736 t.Parallel() 737 ctx := context.Background() 738 739 Convey("SetReview", t, func(c C) { 740 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 741 defer r.Body.Close() 742 743 var ri ReviewInput 744 err := json.NewDecoder(r.Body).Decode(&ri) 745 c.So(err, ShouldBeNil) 746 747 var rr ReviewResult 748 rr.Labels = ri.Labels 749 rr.Reviewers = make(map[string]AddReviewerResult, len(ri.Reviewers)) 750 for _, reviewer := range ri.Reviewers { 751 result := AddReviewerResult{ 752 Input: reviewer.Reviewer, 753 } 754 info := ReviewerInfo{AccountInfo: AccountInfo{AccountID: 12345}} 755 switch reviewer.State { 756 case "REVIEWER": 757 result.Reviewers = []ReviewerInfo{info} 758 case "CC": 759 result.CCs = []ReviewerInfo{info} 760 } 761 rr.Reviewers[reviewer.Reviewer] = result 762 } 763 764 w.WriteHeader(200) 765 w.Header().Set("Content-Type", "application/json") 766 767 var buffer bytes.Buffer 768 err = json.NewEncoder(&buffer).Encode(&rr) 769 c.So(err, ShouldBeNil) 770 fmt.Fprintf(w, ")]}'\n%s\n", buffer.String()) 771 }) 772 defer srv.Close() 773 774 Convey("Set review", func() { 775 _, err := client.SetReview(ctx, "629279", "current", &ReviewInput{}) 776 So(err, ShouldBeNil) 777 }) 778 779 Convey("Set label", func() { 780 ri := ReviewInput{Labels: map[string]int{"Code-Review": 1}} 781 result, err := client.SetReview(ctx, "629279", "current", &ri) 782 So(err, ShouldBeNil) 783 So(result.Labels, ShouldResemble, ri.Labels) 784 }) 785 786 Convey("Set reviewers", func() { 787 ri := ReviewInput{ 788 Reviewers: []ReviewerInput{ 789 { 790 Reviewer: "test@example.com", 791 State: "REVIEWER", 792 }, 793 { 794 Reviewer: "test2@example.com", 795 State: "CC", 796 }, 797 }, 798 } 799 result, err := client.SetReview(ctx, "629279", "current", &ri) 800 So(err, ShouldBeNil) 801 So(len(result.Reviewers), ShouldEqual, 2) 802 So(len(result.Reviewers["test@example.com"].Reviewers), ShouldEqual, 1) 803 So(len(result.Reviewers["test2@example.com"].CCs), ShouldEqual, 1) 804 }) 805 }) 806 807 Convey("SetReview but change non-existent", t, func() { 808 srv, c := newMockClient(func(w http.ResponseWriter, r *http.Request) { 809 w.WriteHeader(404) 810 w.Header().Set("Content-Type", "text/plain") 811 fmt.Fprintf(w, "No such change: 629279") 812 }) 813 defer srv.Close() 814 815 Convey("Basic", func() { 816 _, err := c.SetReview(ctx, "629279", "current", &ReviewInput{}) 817 So(err, ShouldNotBeNil) 818 }) 819 820 }) 821 } 822 823 func TestSubmit(t *testing.T) { 824 t.Parallel() 825 ctx := context.Background() 826 827 Convey("Submit", t, func(c C) { 828 srv, client := newMockClient(func(w http.ResponseWriter, r *http.Request) { 829 defer r.Body.Close() 830 var si SubmitInput 831 err := json.NewDecoder(r.Body).Decode(&si) 832 c.So(err, ShouldBeNil) 833 var cr Change 834 w.WriteHeader(200) 835 w.Header().Set("Content-Type", "application/json") 836 var buffer bytes.Buffer 837 err = json.NewEncoder(&buffer).Encode(&cr) 838 c.So(err, ShouldBeNil) 839 fmt.Fprintf(w, ")]}'\n%s\n", buffer.String()) 840 }) 841 defer srv.Close() 842 843 Convey("Submit", func() { 844 _, err := client.Submit(ctx, "629279", &SubmitInput{}) 845 So(err, ShouldBeNil) 846 }) 847 }) 848 } 849 850 //////////////////////////////////////////////////////////////////////////////// 851 852 var ( 853 fakeCL1Str = `{ 854 "id": "infra%2Fluci%2Fluci-go~master~I4c01b6686740f15844dc86aab73ee4ce00b90fe3", 855 "project": "infra/luci/luci-go", 856 "branch": "master", 857 "hashtags": [], 858 "change_id": "I4c01b6686740f15844dc86aab73ee4ce00b90fe3", 859 "subject": "gitiles: Implement forward log.", 860 "status": "NEW", 861 "current_revision": "eb2388b592a9", 862 "created": "2017-08-22 18:46:58.000000000", 863 "updated": "2017-08-23 22:33:34.000000000", 864 "submit_type": "REBASE_ALWAYS", 865 "mergeable": true, 866 "insertions": 154, 867 "deletions": 23, 868 "unresolved_comment_count": 3, 869 "has_review_started": true, 870 "_number": 627036, 871 "owner": { 872 "_account_id": 1118104 873 }, 874 "reviewers": { 875 "CC": [ 876 {"_account_id": 1118110}, 877 {"_account_id": 1118111}, 878 {"_account_id": 1118112} 879 ], 880 "REVIEWER": [ 881 {"_account_id": 1118120}, 882 {"_account_id": 1118121}, 883 {"_account_id": 1118122} 884 ], 885 "REMOVED": [ 886 {"_account_id": 1118130}, 887 {"_account_id": 1118131}, 888 {"_account_id": 1118132} 889 ] 890 } 891 }` 892 fakeCL2Str = `{ 893 "id": "infra%2Finfra~master~Ia292f77ae6bd94afbd746da0b08500f738904d15", 894 "project": "infra/infra", 895 "branch": "master", 896 "hashtags": [], 897 "change_id": "Ia292f77ae6bd94afbd746da0b08500f738904d15", 898 "subject": "[Findit] Add flake analyzer forced rerun instructions to makefile.", 899 "status": "MERGED", 900 "created": "2017-08-23 17:25:40.000000000", 901 "updated": "2017-08-23 22:51:03.000000000", 902 "submitted": "2017-08-23 22:51:03.000000000", 903 "insertions": 4, 904 "deletions": 1, 905 "unresolved_comment_count": 0, 906 "has_review_started": true, 907 "_number": 629277, 908 "owner": { 909 "_account_id": 1178184 910 }, 911 "_has_more_changes": true 912 }` 913 fakeCL3Str = `{ 914 "id": "infra%2Finfra~master~Ia292f77ae6bd94a000046da0b08500f738904d15", 915 "project": "infra/infra", 916 "branch": "master", 917 "hashtags": [], 918 "change_id": "Ia292f77ae6bd94a000046da0b08500f738904d15", 919 "subject": "Revert of [Findit] Add flake analyzer forced rerun instructions to makefile.", 920 "status": "MERGED", 921 "current_revision" : "1ee75012c0de", 922 "revert_of": 629277, 923 "created": "2017-08-23 18:25:40.000000000", 924 "updated": "2017-08-23 23:51:03.000000000", 925 "submitted": "2017-08-23 23:51:03.000000000", 926 "insertions": 1, 927 "deletions": 4, 928 "unresolved_comment_count": 0, 929 "has_review_started": true, 930 "_number": 629279, 931 "owner": { 932 "_account_id": 1178184 933 } 934 }` 935 fakeCL4Str = `{ 936 "id": "infra%2Finfra~master~Ia292f77ae6bd94afbd746da0b08500f738904d15", 937 "project": "infra/infra", 938 "branch": "master", 939 "hashtags": [], 940 "change_id": "Ia292f77ae6bd94afbd746da0b08500f738904d15", 941 "subject": "[Findit] Add flake analyzer forced rerun instructions to makefile.", 942 "status": "ABANDONED", 943 "created": "2017-08-23 17:25:40.000000000", 944 "updated": "2017-08-23 22:51:03.000000000", 945 "submitted": "2017-08-23 22:51:03.000000000", 946 "insertions": 4, 947 "deletions": 1, 948 "unresolved_comment_count": 0, 949 "has_review_started": true, 950 "_number": 629279, 951 "owner": { 952 "_account_id": 1178184 953 }, 954 "_has_more_changes": true 955 }` 956 fakeCL5Str = `{ 957 "id": "infra%2Finfra~master~Ia292f77ae6bd94afbd746da0b08500f738904d15", 958 "project": "infra/infra", 959 "branch": "master", 960 "hashtags": [], 961 "change_id": "Ia292f77ae6bd94afbd746da0b08500f738904d15", 962 "subject": "[Findit] Add flake analyzer forced rerun instructions to makefile.", 963 "status": "ABANDONED", 964 "created": "2017-08-23 17:25:40.000000000", 965 "updated": "2017-08-23 22:51:03.000000000", 966 "submitted": "2017-08-23 22:51:03.000000000", 967 "insertions": 4, 968 "deletions": 1, 969 "unresolved_comment_count": 0, 970 "has_review_started": true, 971 "_number": 629279, 972 "owner": { 973 "_account_id": 1178184 974 }, 975 "labels": { 976 "Verified": { 977 "all": [{ 978 "value": 0, 979 "_account_id": 1000096, 980 "name": "John Doe", 981 "email": "john.doe@example.com", 982 "username": "jdoe" 983 }, 984 { 985 "value": 0, 986 "_account_id": 1000097, 987 "name": "Jane Roe", 988 "email": "jane.roe@example.com", 989 "username": "jroe" 990 }], 991 "values": { 992 "-1": "Fails", 993 " 0": "No score", 994 "+1": "Verified" 995 } 996 997 }, 998 "Code-Review": { 999 "disliked": { 1000 "_account_id": 1000096, 1001 "name": "John Doe", 1002 "email": "john.doe@example.com", 1003 "username": "jdoe" 1004 }, 1005 "all": [{ 1006 "value": -1, 1007 "_account_id": 1000096, 1008 "name": "John Doe", 1009 "email": "john.doe@example.com", 1010 "username": "jdoe" 1011 }, 1012 { 1013 "value": 1, 1014 "_account_id": 1000097, 1015 "name": "Jane Roe", 1016 "email": "jane.roe@example.com", 1017 "username": "jroe" 1018 }], 1019 "values": { 1020 "-2": "This shall not be merged", 1021 "-1": "I would prefer this is not merged as is", 1022 " 0": "No score", 1023 "+1": "Looks good to me, but someone else must approve", 1024 "+2": "Looks good to me, approved" 1025 } 1026 } 1027 } 1028 }` 1029 fakeCL6Str = `{ 1030 "id": "infra%2Fluci%2Fluci-go~master~Id37e51c3b84bfc41bc88fa237ddf722f934f4fa4", 1031 "project": "infra/luci/luci-go", 1032 "branch": "master", 1033 "hashtags": [], 1034 "change_id": "Id37e51c3b84bfc41bc88fa237ddf722f934f4fa4", 1035 "subject": "[vpython]: Re-add deprecated \"-spec\" flag.", 1036 "status": "NEW", 1037 "current_revision": "d6375c2ea5b0", 1038 "created": "2017-08-21 18:46:58.000000000", 1039 "updated": "2017-08-22 22:33:34.000000000", 1040 "submit_type": "REBASE_ALWAYS", 1041 "mergeable": true, 1042 "insertions": 6, 1043 "deletions": 0, 1044 "unresolved_comment_count": 0, 1045 "has_review_started": true, 1046 "_number": 646267, 1047 "owner": { 1048 "_account_id": 1118104 1049 }, 1050 "reviewers": { 1051 "CC": [ 1052 {"_account_id": 1118110}, 1053 {"_account_id": 1118111}, 1054 {"_account_id": 1118112} 1055 ], 1056 "REVIEWER": [ 1057 {"_account_id": 1118120}, 1058 {"_account_id": 1118121}, 1059 {"_account_id": 1118122} 1060 ], 1061 "REMOVED": [ 1062 {"_account_id": 1118130}, 1063 {"_account_id": 1118131}, 1064 {"_account_id": 1118132} 1065 ] 1066 } 1067 }` 1068 fakeComments1Str = `{ 1069 "foo": [ 1070 { 1071 "id": "61d1fbfb_63e8c695", 1072 "author": { 1073 "_account_id": 1002228, 1074 "name": "John Doe", 1075 "email": "johndoe@example.com" 1076 }, 1077 "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e", 1078 "patch_set": 1, 1079 "line": 3, 1080 "range": { 1081 "start_line": 3, 1082 "start_character": 7, 1083 "end_line": 3, 1084 "end_character": 55 1085 }, 1086 "updated": "2020-07-28 14:04:31.000000000", 1087 "message": "", 1088 "unresolved": true, 1089 "in_reply_to": "", 1090 "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11" 1091 } 1092 ], 1093 "bar": [ 1094 { 1095 "id": "63e8c695_61d1fbfb", 1096 "author": { 1097 "_account_id": 1002228, 1098 "name": "John Doe", 1099 "email": "johndoe@example.com" 1100 }, 1101 "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e", 1102 "patch_set": 1, 1103 "line": 21, 1104 "updated": "2020-07-28 14:04:31.000000000", 1105 "message": "", 1106 "unresolved": true, 1107 "in_reply_to": "", 1108 "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11" 1109 } 1110 ] 1111 }` 1112 fakeRobotComments1Str = `{ 1113 "foo": [ 1114 { 1115 "id": "61d1fbfb_63e8c695", 1116 "author": { 1117 "_account_id": 1001234, 1118 "name": "A Robot", 1119 "email": "robot@example.com" 1120 }, 1121 "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e", 1122 "patch_set": 1, 1123 "line": 3, 1124 "range": { 1125 "start_line": 3, 1126 "start_character": 7, 1127 "end_line": 3, 1128 "end_character": 55 1129 }, 1130 "updated": "2020-07-28 14:04:31.000000000", 1131 "message": "", 1132 "in_reply_to": "", 1133 "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11", 1134 "robot_id": "somerobot", 1135 "robot_run_id": "run1" 1136 } 1137 ], 1138 "bar": [ 1139 { 1140 "id": "63e8c695_61d1fbfb", 1141 "author": { 1142 "_account_id": 1001234, 1143 "name": "A Robot", 1144 "email": "robot@example.com" 1145 }, 1146 "change_message_id": "c24215a84fdc9cec42c2d5eec4f488d172d39d7e", 1147 "patch_set": 1, 1148 "line": 21, 1149 "updated": "2020-07-28 14:04:31.000000000", 1150 "message": "", 1151 "in_reply_to": "", 1152 "commit_id": "08a8326653eaa5f7aeea30348b63bf5e9595dc11", 1153 "robot_id": "somerobot", 1154 "robot_run_id": "run1" 1155 } 1156 ] 1157 }` 1158 fakeAccounts1Str = `[ 1159 { 1160 "_account_id": 1002228, 1161 "name": "John Doe", 1162 "email": "johndoe@example.com" 1163 }, 1164 { 1165 "_account_id": 1002228, 1166 "name": "Jane Doe", 1167 "email": "janedoe@example.com" 1168 } 1169 ]` 1170 ) 1171 1172 //////////////////////////////////////////////////////////////////////////////// 1173 1174 func newMockClient(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, *Client) { 1175 srv := httptest.NewServer(http.HandlerFunc(handler)) 1176 pu, _ := url.Parse(srv.URL) 1177 // Tests shouldn't sleep, so make sure we don't wait between request 1178 // attempts. 1179 retryStrategy := func() retry.Iterator { 1180 return &retry.Limited{Retries: 10, Delay: 0} 1181 } 1182 c := &Client{http.DefaultClient, *pu, retryStrategy} 1183 return srv, c 1184 }