github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/view/view_test.go (about) 1 package view 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "os" 10 "testing" 11 12 "github.com/cli/cli/api" 13 "github.com/cli/cli/internal/ghrepo" 14 "github.com/cli/cli/internal/run" 15 "github.com/cli/cli/pkg/cmd/pr/shared" 16 "github.com/cli/cli/pkg/cmdutil" 17 "github.com/cli/cli/pkg/httpmock" 18 "github.com/cli/cli/pkg/iostreams" 19 "github.com/cli/cli/test" 20 "github.com/google/shlex" 21 "github.com/stretchr/testify/assert" 22 "github.com/stretchr/testify/require" 23 ) 24 25 func Test_NewCmdView(t *testing.T) { 26 tests := []struct { 27 name string 28 args string 29 isTTY bool 30 want ViewOptions 31 wantErr string 32 }{ 33 { 34 name: "number argument", 35 args: "123", 36 isTTY: true, 37 want: ViewOptions{ 38 SelectorArg: "123", 39 BrowserMode: false, 40 }, 41 }, 42 { 43 name: "no argument", 44 args: "", 45 isTTY: true, 46 want: ViewOptions{ 47 SelectorArg: "", 48 BrowserMode: false, 49 }, 50 }, 51 { 52 name: "web mode", 53 args: "123 -w", 54 isTTY: true, 55 want: ViewOptions{ 56 SelectorArg: "123", 57 BrowserMode: true, 58 }, 59 }, 60 { 61 name: "no argument with --repo override", 62 args: "-R owner/repo", 63 isTTY: true, 64 wantErr: "argument required when using the --repo flag", 65 }, 66 { 67 name: "comments", 68 args: "123 -c", 69 isTTY: true, 70 want: ViewOptions{ 71 SelectorArg: "123", 72 Comments: true, 73 }, 74 }, 75 } 76 for _, tt := range tests { 77 t.Run(tt.name, func(t *testing.T) { 78 io, _, _, _ := iostreams.Test() 79 io.SetStdoutTTY(tt.isTTY) 80 io.SetStdinTTY(tt.isTTY) 81 io.SetStderrTTY(tt.isTTY) 82 83 f := &cmdutil.Factory{ 84 IOStreams: io, 85 } 86 87 var opts *ViewOptions 88 cmd := NewCmdView(f, func(o *ViewOptions) error { 89 opts = o 90 return nil 91 }) 92 cmd.PersistentFlags().StringP("repo", "R", "", "") 93 94 argv, err := shlex.Split(tt.args) 95 require.NoError(t, err) 96 cmd.SetArgs(argv) 97 98 cmd.SetIn(&bytes.Buffer{}) 99 cmd.SetOut(ioutil.Discard) 100 cmd.SetErr(ioutil.Discard) 101 102 _, err = cmd.ExecuteC() 103 if tt.wantErr != "" { 104 require.EqualError(t, err, tt.wantErr) 105 return 106 } else { 107 require.NoError(t, err) 108 } 109 110 assert.Equal(t, tt.want.SelectorArg, opts.SelectorArg) 111 }) 112 } 113 } 114 115 func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) { 116 io, _, stdout, stderr := iostreams.Test() 117 io.SetStdoutTTY(isTTY) 118 io.SetStdinTTY(isTTY) 119 io.SetStderrTTY(isTTY) 120 121 browser := &cmdutil.TestBrowser{} 122 factory := &cmdutil.Factory{ 123 IOStreams: io, 124 Browser: browser, 125 } 126 127 cmd := NewCmdView(factory, nil) 128 129 argv, err := shlex.Split(cli) 130 if err != nil { 131 return nil, err 132 } 133 cmd.SetArgs(argv) 134 135 cmd.SetIn(&bytes.Buffer{}) 136 cmd.SetOut(ioutil.Discard) 137 cmd.SetErr(ioutil.Discard) 138 139 _, err = cmd.ExecuteC() 140 return &test.CmdOut{ 141 OutBuf: stdout, 142 ErrBuf: stderr, 143 BrowsedURL: browser.BrowsedURL(), 144 }, err 145 } 146 147 // hack for compatibility with old JSON fixture files 148 func prFromFixtures(fixtures map[string]string) (*api.PullRequest, error) { 149 var response struct { 150 Data struct { 151 Repository struct { 152 PullRequest *api.PullRequest 153 } 154 } 155 } 156 157 ff, err := os.Open(fixtures["PullRequestByNumber"]) 158 if err != nil { 159 return nil, err 160 } 161 defer ff.Close() 162 163 dec := json.NewDecoder(ff) 164 err = dec.Decode(&response) 165 if err != nil { 166 return nil, err 167 } 168 169 for name := range fixtures { 170 switch name { 171 case "PullRequestByNumber": 172 case "ReviewsForPullRequest", "CommentsForPullRequest": 173 ff, err := os.Open(fixtures[name]) 174 if err != nil { 175 return nil, err 176 } 177 defer ff.Close() 178 dec := json.NewDecoder(ff) 179 err = dec.Decode(&response) 180 if err != nil { 181 return nil, err 182 } 183 default: 184 return nil, fmt.Errorf("unrecognized fixture type: %q", name) 185 } 186 } 187 188 return response.Data.Repository.PullRequest, nil 189 } 190 191 func TestPRView_Preview_nontty(t *testing.T) { 192 tests := map[string]struct { 193 branch string 194 args string 195 fixtures map[string]string 196 expectedOutputs []string 197 }{ 198 "Open PR without metadata": { 199 branch: "master", 200 args: "12", 201 fixtures: map[string]string{ 202 "PullRequestByNumber": "./fixtures/prViewPreview.json", 203 }, 204 expectedOutputs: []string{ 205 `title:\tBlueberries are from a fork\n`, 206 `state:\tOPEN\n`, 207 `author:\tnobody\n`, 208 `labels:\t\n`, 209 `assignees:\t\n`, 210 `reviewers:\t\n`, 211 `projects:\t\n`, 212 `milestone:\t\n`, 213 `url:\thttps://github.com/OWNER/REPO/pull/12\n`, 214 `additions:\t100\n`, 215 `deletions:\t10\n`, 216 `number:\t12\n`, 217 `blueberries taste good`, 218 }, 219 }, 220 "Open PR with metadata by number": { 221 branch: "master", 222 args: "12", 223 fixtures: map[string]string{ 224 "PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json", 225 }, 226 expectedOutputs: []string{ 227 `title:\tBlueberries are from a fork\n`, 228 `reviewers:\t1 \(Requested\)\n`, 229 `assignees:\tmarseilles, monaco\n`, 230 `labels:\tone, two, three, four, five\n`, 231 `projects:\tProject 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`, 232 `milestone:\tuluru\n`, 233 `\*\*blueberries taste good\*\*`, 234 }, 235 }, 236 "Open PR with reviewers by number": { 237 branch: "master", 238 args: "12", 239 fixtures: map[string]string{ 240 "PullRequestByNumber": "./fixtures/prViewPreviewWithReviewersByNumber.json", 241 "ReviewsForPullRequest": "./fixtures/prViewPreviewManyReviews.json", 242 }, 243 expectedOutputs: []string{ 244 `title:\tBlueberries are from a fork\n`, 245 `state:\tOPEN\n`, 246 `author:\tnobody\n`, 247 `labels:\t\n`, 248 `assignees:\t\n`, 249 `projects:\t\n`, 250 `milestone:\t\n`, 251 `additions:\t100\n`, 252 `deletions:\t10\n`, 253 `reviewers:\tDEF \(Commented\), def \(Changes requested\), ghost \(Approved\), hubot \(Commented\), xyz \(Approved\), 123 \(Requested\), abc \(Requested\), my-org\/team-1 \(Requested\)\n`, 254 `\*\*blueberries taste good\*\*`, 255 }, 256 }, 257 "Closed PR": { 258 branch: "master", 259 args: "12", 260 fixtures: map[string]string{ 261 "PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json", 262 }, 263 expectedOutputs: []string{ 264 `state:\tCLOSED\n`, 265 `author:\tnobody\n`, 266 `labels:\t\n`, 267 `assignees:\t\n`, 268 `reviewers:\t\n`, 269 `projects:\t\n`, 270 `milestone:\t\n`, 271 `additions:\t100\n`, 272 `deletions:\t10\n`, 273 `\*\*blueberries taste good\*\*`, 274 }, 275 }, 276 "Merged PR": { 277 branch: "master", 278 args: "12", 279 fixtures: map[string]string{ 280 "PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json", 281 }, 282 expectedOutputs: []string{ 283 `state:\tMERGED\n`, 284 `author:\tnobody\n`, 285 `labels:\t\n`, 286 `assignees:\t\n`, 287 `reviewers:\t\n`, 288 `projects:\t\n`, 289 `milestone:\t\n`, 290 `additions:\t100\n`, 291 `deletions:\t10\n`, 292 `\*\*blueberries taste good\*\*`, 293 }, 294 }, 295 "Draft PR": { 296 branch: "master", 297 args: "12", 298 fixtures: map[string]string{ 299 "PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json", 300 }, 301 expectedOutputs: []string{ 302 `title:\tBlueberries are from a fork\n`, 303 `state:\tDRAFT\n`, 304 `author:\tnobody\n`, 305 `labels:`, 306 `assignees:`, 307 `reviewers:`, 308 `projects:`, 309 `milestone:`, 310 `additions:\t100\n`, 311 `deletions:\t10\n`, 312 `\*\*blueberries taste good\*\*`, 313 }, 314 }, 315 } 316 317 for name, tc := range tests { 318 t.Run(name, func(t *testing.T) { 319 http := &httpmock.Registry{} 320 defer http.Verify(t) 321 322 pr, err := prFromFixtures(tc.fixtures) 323 require.NoError(t, err) 324 shared.RunCommandFinder("12", pr, ghrepo.New("OWNER", "REPO")) 325 326 output, err := runCommand(http, tc.branch, false, tc.args) 327 if err != nil { 328 t.Errorf("error running command `%v`: %v", tc.args, err) 329 } 330 331 assert.Equal(t, "", output.Stderr()) 332 333 //nolint:staticcheck // prefer exact matchers over ExpectLines 334 test.ExpectLines(t, output.String(), tc.expectedOutputs...) 335 }) 336 } 337 } 338 339 func TestPRView_Preview(t *testing.T) { 340 tests := map[string]struct { 341 branch string 342 args string 343 fixtures map[string]string 344 expectedOutputs []string 345 }{ 346 "Open PR without metadata": { 347 branch: "master", 348 args: "12", 349 fixtures: map[string]string{ 350 "PullRequestByNumber": "./fixtures/prViewPreview.json", 351 }, 352 expectedOutputs: []string{ 353 `Blueberries are from a fork #12`, 354 `Open.*nobody wants to merge 12 commits into master from blueberries.+100.-10`, 355 `blueberries taste good`, 356 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 357 }, 358 }, 359 "Open PR with metadata by number": { 360 branch: "master", 361 args: "12", 362 fixtures: map[string]string{ 363 "PullRequestByNumber": "./fixtures/prViewPreviewWithMetadataByNumber.json", 364 }, 365 expectedOutputs: []string{ 366 `Blueberries are from a fork #12`, 367 `Open.*nobody wants to merge 12 commits into master from blueberries.+100.-10`, 368 `Reviewers:.*1 \(.*Requested.*\)\n`, 369 `Assignees:.*marseilles, monaco\n`, 370 `Labels:.*one, two, three, four, five\n`, 371 `Projects:.*Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`, 372 `Milestone:.*uluru\n`, 373 `blueberries taste good`, 374 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 375 }, 376 }, 377 "Open PR with reviewers by number": { 378 branch: "master", 379 args: "12", 380 fixtures: map[string]string{ 381 "PullRequestByNumber": "./fixtures/prViewPreviewWithReviewersByNumber.json", 382 "ReviewsForPullRequest": "./fixtures/prViewPreviewManyReviews.json", 383 }, 384 expectedOutputs: []string{ 385 `Blueberries are from a fork #12`, 386 `Reviewers: DEF \(Commented\), def \(Changes requested\), ghost \(Approved\), hubot \(Commented\), xyz \(Approved\), 123 \(Requested\), abc \(Requested\), my-org\/team-1 \(Requested\)`, 387 `blueberries taste good`, 388 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 389 }, 390 }, 391 "Closed PR": { 392 branch: "master", 393 args: "12", 394 fixtures: map[string]string{ 395 "PullRequestByNumber": "./fixtures/prViewPreviewClosedState.json", 396 }, 397 expectedOutputs: []string{ 398 `Blueberries are from a fork #12`, 399 `Closed.*nobody wants to merge 12 commits into master from blueberries.+100.-10`, 400 `blueberries taste good`, 401 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 402 }, 403 }, 404 "Merged PR": { 405 branch: "master", 406 args: "12", 407 fixtures: map[string]string{ 408 "PullRequestByNumber": "./fixtures/prViewPreviewMergedState.json", 409 }, 410 expectedOutputs: []string{ 411 `Blueberries are from a fork #12`, 412 `Merged.*nobody wants to merge 12 commits into master from blueberries.+100.-10`, 413 `blueberries taste good`, 414 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 415 }, 416 }, 417 "Draft PR": { 418 branch: "master", 419 args: "12", 420 fixtures: map[string]string{ 421 "PullRequestByNumber": "./fixtures/prViewPreviewDraftState.json", 422 }, 423 expectedOutputs: []string{ 424 `Blueberries are from a fork #12`, 425 `Draft.*nobody wants to merge 12 commits into master from blueberries.+100.-10`, 426 `blueberries taste good`, 427 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 428 }, 429 }, 430 } 431 432 for name, tc := range tests { 433 t.Run(name, func(t *testing.T) { 434 http := &httpmock.Registry{} 435 defer http.Verify(t) 436 437 pr, err := prFromFixtures(tc.fixtures) 438 require.NoError(t, err) 439 shared.RunCommandFinder("12", pr, ghrepo.New("OWNER", "REPO")) 440 441 output, err := runCommand(http, tc.branch, true, tc.args) 442 if err != nil { 443 t.Errorf("error running command `%v`: %v", tc.args, err) 444 } 445 446 assert.Equal(t, "", output.Stderr()) 447 448 //nolint:staticcheck // prefer exact matchers over ExpectLines 449 test.ExpectLines(t, output.String(), tc.expectedOutputs...) 450 }) 451 } 452 } 453 454 func TestPRView_web_currentBranch(t *testing.T) { 455 http := &httpmock.Registry{} 456 defer http.Verify(t) 457 458 shared.RunCommandFinder("", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/10"}, ghrepo.New("OWNER", "REPO")) 459 460 _, cmdTeardown := run.Stub() 461 defer cmdTeardown(t) 462 463 output, err := runCommand(http, "blueberries", true, "-w") 464 if err != nil { 465 t.Errorf("error running command `pr view`: %v", err) 466 } 467 468 assert.Equal(t, "", output.String()) 469 assert.Equal(t, "Opening github.com/OWNER/REPO/pull/10 in your browser.\n", output.Stderr()) 470 assert.Equal(t, "https://github.com/OWNER/REPO/pull/10", output.BrowsedURL) 471 } 472 473 func TestPRView_web_noResultsForBranch(t *testing.T) { 474 http := &httpmock.Registry{} 475 defer http.Verify(t) 476 477 shared.RunCommandFinder("", nil, nil) 478 479 _, cmdTeardown := run.Stub() 480 defer cmdTeardown(t) 481 482 _, err := runCommand(http, "blueberries", true, "-w") 483 if err == nil || err.Error() != `no pull requests found` { 484 t.Errorf("error running command `pr view`: %v", err) 485 } 486 } 487 488 func TestPRView_tty_Comments(t *testing.T) { 489 tests := map[string]struct { 490 branch string 491 cli string 492 fixtures map[string]string 493 expectedOutputs []string 494 wantsErr bool 495 }{ 496 "without comments flag": { 497 branch: "master", 498 cli: "123", 499 fixtures: map[string]string{ 500 "PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json", 501 "ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json", 502 }, 503 expectedOutputs: []string{ 504 `some title #12`, 505 `1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f}`, 506 `some body`, 507 `———————— Not showing 9 comments ————————`, 508 `marseilles \(Collaborator\) • Jan 9, 2020 • Newest comment`, 509 `4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680}`, 510 `Comment 5`, 511 `Use --comments to view the full conversation`, 512 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 513 }, 514 }, 515 "with comments flag": { 516 branch: "master", 517 cli: "123 --comments", 518 fixtures: map[string]string{ 519 "PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json", 520 "ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json", 521 "CommentsForPullRequest": "./fixtures/prViewPreviewFullComments.json", 522 }, 523 expectedOutputs: []string{ 524 `some title #12`, 525 `some body`, 526 `monalisa • Jan 1, 2020 • Edited`, 527 `1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f} • 4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680} • 7 \x{1f44e} • 8 \x{1f44d}`, 528 `Comment 1`, 529 `sam commented • Jan 2, 2020`, 530 `1 \x{1f44e} • 1 \x{1f44d}`, 531 `Review 1`, 532 `View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-1`, 533 `johnnytest \(Contributor\) • Jan 3, 2020`, 534 `Comment 2`, 535 `matt requested changes \(Owner\) • Jan 4, 2020`, 536 `1 \x{1f615} • 1 \x{1f440}`, 537 `Review 2`, 538 `View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-2`, 539 `elvisp \(Member\) • Jan 5, 2020`, 540 `Comment 3`, 541 `leah approved \(Member\) • Jan 6, 2020 • Edited`, 542 `Review 3`, 543 `View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-3`, 544 `loislane \(Owner\) • Jan 7, 2020`, 545 `Comment 4`, 546 `louise dismissed • Jan 8, 2020`, 547 `Review 4`, 548 `View the full review: https://github.com/OWNER/REPO/pull/12#pullrequestreview-4`, 549 `sam-spam • This comment has been marked as spam`, 550 `marseilles \(Collaborator\) • Jan 9, 2020 • Newest comment`, 551 `Comment 5`, 552 `View this pull request on GitHub: https://github.com/OWNER/REPO/pull/12`, 553 }, 554 }, 555 "with invalid comments flag": { 556 branch: "master", 557 cli: "123 --comments 3", 558 wantsErr: true, 559 }, 560 } 561 for name, tt := range tests { 562 t.Run(name, func(t *testing.T) { 563 http := &httpmock.Registry{} 564 defer http.Verify(t) 565 566 if len(tt.fixtures) > 0 { 567 pr, err := prFromFixtures(tt.fixtures) 568 require.NoError(t, err) 569 shared.RunCommandFinder("123", pr, ghrepo.New("OWNER", "REPO")) 570 } else { 571 shared.RunCommandFinder("123", nil, nil) 572 } 573 574 output, err := runCommand(http, tt.branch, true, tt.cli) 575 if tt.wantsErr { 576 assert.Error(t, err) 577 return 578 } 579 assert.NoError(t, err) 580 assert.Equal(t, "", output.Stderr()) 581 //nolint:staticcheck // prefer exact matchers over ExpectLines 582 test.ExpectLines(t, output.String(), tt.expectedOutputs...) 583 }) 584 } 585 } 586 587 func TestPRView_nontty_Comments(t *testing.T) { 588 tests := map[string]struct { 589 branch string 590 cli string 591 fixtures map[string]string 592 expectedOutputs []string 593 wantsErr bool 594 }{ 595 "without comments flag": { 596 branch: "master", 597 cli: "123", 598 fixtures: map[string]string{ 599 "PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json", 600 "ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json", 601 }, 602 expectedOutputs: []string{ 603 `title:\tsome title`, 604 `state:\tOPEN`, 605 `author:\tnobody`, 606 `url:\thttps://github.com/OWNER/REPO/pull/12`, 607 `some body`, 608 }, 609 }, 610 "with comments flag": { 611 branch: "master", 612 cli: "123 --comments", 613 fixtures: map[string]string{ 614 "PullRequestByNumber": "./fixtures/prViewPreviewSingleComment.json", 615 "ReviewsForPullRequest": "./fixtures/prViewPreviewReviews.json", 616 "CommentsForPullRequest": "./fixtures/prViewPreviewFullComments.json", 617 }, 618 expectedOutputs: []string{ 619 `author:\tmonalisa`, 620 `association:\tnone`, 621 `edited:\ttrue`, 622 `status:\tnone`, 623 `Comment 1`, 624 `author:\tsam`, 625 `association:\tnone`, 626 `edited:\tfalse`, 627 `status:\tcommented`, 628 `Review 1`, 629 `author:\tjohnnytest`, 630 `association:\tcontributor`, 631 `edited:\tfalse`, 632 `status:\tnone`, 633 `Comment 2`, 634 `author:\tmatt`, 635 `association:\towner`, 636 `edited:\tfalse`, 637 `status:\tchanges requested`, 638 `Review 2`, 639 `author:\telvisp`, 640 `association:\tmember`, 641 `edited:\tfalse`, 642 `status:\tnone`, 643 `Comment 3`, 644 `author:\tleah`, 645 `association:\tmember`, 646 `edited:\ttrue`, 647 `status:\tapproved`, 648 `Review 3`, 649 `author:\tloislane`, 650 `association:\towner`, 651 `edited:\tfalse`, 652 `status:\tnone`, 653 `Comment 4`, 654 `author:\tlouise`, 655 `association:\tnone`, 656 `edited:\tfalse`, 657 `status:\tdismissed`, 658 `Review 4`, 659 `author:\tmarseilles`, 660 `association:\tcollaborator`, 661 `edited:\tfalse`, 662 `status:\tnone`, 663 `Comment 5`, 664 }, 665 }, 666 "with invalid comments flag": { 667 branch: "master", 668 cli: "123 --comments 3", 669 wantsErr: true, 670 }, 671 } 672 for name, tt := range tests { 673 t.Run(name, func(t *testing.T) { 674 http := &httpmock.Registry{} 675 defer http.Verify(t) 676 677 if len(tt.fixtures) > 0 { 678 pr, err := prFromFixtures(tt.fixtures) 679 require.NoError(t, err) 680 shared.RunCommandFinder("123", pr, ghrepo.New("OWNER", "REPO")) 681 } else { 682 shared.RunCommandFinder("123", nil, nil) 683 } 684 685 output, err := runCommand(http, tt.branch, false, tt.cli) 686 if tt.wantsErr { 687 assert.Error(t, err) 688 return 689 } 690 assert.NoError(t, err) 691 assert.Equal(t, "", output.Stderr()) 692 //nolint:staticcheck // prefer exact matchers over ExpectLines 693 test.ExpectLines(t, output.String(), tt.expectedOutputs...) 694 }) 695 } 696 }