github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/edit/edit_test.go (about) 1 package edit 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "path/filepath" 9 "testing" 10 11 "github.com/cli/cli/api" 12 "github.com/cli/cli/internal/ghrepo" 13 shared "github.com/cli/cli/pkg/cmd/pr/shared" 14 "github.com/cli/cli/pkg/cmdutil" 15 "github.com/cli/cli/pkg/httpmock" 16 "github.com/cli/cli/pkg/iostreams" 17 "github.com/google/shlex" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 ) 21 22 func TestNewCmdEdit(t *testing.T) { 23 tmpFile := filepath.Join(t.TempDir(), "my-body.md") 24 err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600) 25 require.NoError(t, err) 26 27 tests := []struct { 28 name string 29 input string 30 stdin string 31 output EditOptions 32 wantsErr bool 33 }{ 34 { 35 name: "no argument", 36 input: "", 37 output: EditOptions{ 38 SelectorArg: "", 39 Interactive: true, 40 }, 41 wantsErr: false, 42 }, 43 { 44 name: "two arguments", 45 input: "1 2", 46 output: EditOptions{}, 47 wantsErr: true, 48 }, 49 { 50 name: "pull request number argument", 51 input: "23", 52 output: EditOptions{ 53 SelectorArg: "23", 54 Interactive: true, 55 }, 56 wantsErr: false, 57 }, 58 { 59 name: "title flag", 60 input: "23 --title test", 61 output: EditOptions{ 62 SelectorArg: "23", 63 Editable: shared.Editable{ 64 Title: shared.EditableString{ 65 Value: "test", 66 Edited: true, 67 }, 68 }, 69 }, 70 wantsErr: false, 71 }, 72 { 73 name: "body flag", 74 input: "23 --body test", 75 output: EditOptions{ 76 SelectorArg: "23", 77 Editable: shared.Editable{ 78 Body: shared.EditableString{ 79 Value: "test", 80 Edited: true, 81 }, 82 }, 83 }, 84 wantsErr: false, 85 }, 86 { 87 name: "body from stdin", 88 input: "23 --body-file -", 89 stdin: "this is on standard input", 90 output: EditOptions{ 91 SelectorArg: "23", 92 Editable: shared.Editable{ 93 Body: shared.EditableString{ 94 Value: "this is on standard input", 95 Edited: true, 96 }, 97 }, 98 }, 99 wantsErr: false, 100 }, 101 { 102 name: "body from file", 103 input: fmt.Sprintf("23 --body-file '%s'", tmpFile), 104 output: EditOptions{ 105 SelectorArg: "23", 106 Editable: shared.Editable{ 107 Body: shared.EditableString{ 108 Value: "a body from file", 109 Edited: true, 110 }, 111 }, 112 }, 113 wantsErr: false, 114 }, 115 { 116 name: "base flag", 117 input: "23 --base base-branch-name", 118 output: EditOptions{ 119 SelectorArg: "23", 120 Editable: shared.Editable{ 121 Base: shared.EditableString{ 122 Value: "base-branch-name", 123 Edited: true, 124 }, 125 }, 126 }, 127 wantsErr: false, 128 }, 129 { 130 name: "add-reviewer flag", 131 input: "23 --add-reviewer monalisa,owner/core", 132 output: EditOptions{ 133 SelectorArg: "23", 134 Editable: shared.Editable{ 135 Reviewers: shared.EditableSlice{ 136 Add: []string{"monalisa", "owner/core"}, 137 Edited: true, 138 }, 139 }, 140 }, 141 wantsErr: false, 142 }, 143 { 144 name: "remove-reviewer flag", 145 input: "23 --remove-reviewer monalisa,owner/core", 146 output: EditOptions{ 147 SelectorArg: "23", 148 Editable: shared.Editable{ 149 Reviewers: shared.EditableSlice{ 150 Remove: []string{"monalisa", "owner/core"}, 151 Edited: true, 152 }, 153 }, 154 }, 155 wantsErr: false, 156 }, 157 { 158 name: "add-assignee flag", 159 input: "23 --add-assignee monalisa,hubot", 160 output: EditOptions{ 161 SelectorArg: "23", 162 Editable: shared.Editable{ 163 Assignees: shared.EditableSlice{ 164 Add: []string{"monalisa", "hubot"}, 165 Edited: true, 166 }, 167 }, 168 }, 169 wantsErr: false, 170 }, 171 { 172 name: "remove-assignee flag", 173 input: "23 --remove-assignee monalisa,hubot", 174 output: EditOptions{ 175 SelectorArg: "23", 176 Editable: shared.Editable{ 177 Assignees: shared.EditableSlice{ 178 Remove: []string{"monalisa", "hubot"}, 179 Edited: true, 180 }, 181 }, 182 }, 183 wantsErr: false, 184 }, 185 { 186 name: "add-label flag", 187 input: "23 --add-label feature,TODO,bug", 188 output: EditOptions{ 189 SelectorArg: "23", 190 Editable: shared.Editable{ 191 Labels: shared.EditableSlice{ 192 Add: []string{"feature", "TODO", "bug"}, 193 Edited: true, 194 }, 195 }, 196 }, 197 wantsErr: false, 198 }, 199 { 200 name: "remove-label flag", 201 input: "23 --remove-label feature,TODO,bug", 202 output: EditOptions{ 203 SelectorArg: "23", 204 Editable: shared.Editable{ 205 Labels: shared.EditableSlice{ 206 Remove: []string{"feature", "TODO", "bug"}, 207 Edited: true, 208 }, 209 }, 210 }, 211 wantsErr: false, 212 }, 213 { 214 name: "add-project flag", 215 input: "23 --add-project Cleanup,Roadmap", 216 output: EditOptions{ 217 SelectorArg: "23", 218 Editable: shared.Editable{ 219 Projects: shared.EditableSlice{ 220 Add: []string{"Cleanup", "Roadmap"}, 221 Edited: true, 222 }, 223 }, 224 }, 225 wantsErr: false, 226 }, 227 { 228 name: "remove-project flag", 229 input: "23 --remove-project Cleanup,Roadmap", 230 output: EditOptions{ 231 SelectorArg: "23", 232 Editable: shared.Editable{ 233 Projects: shared.EditableSlice{ 234 Remove: []string{"Cleanup", "Roadmap"}, 235 Edited: true, 236 }, 237 }, 238 }, 239 wantsErr: false, 240 }, 241 { 242 name: "milestone flag", 243 input: "23 --milestone GA", 244 output: EditOptions{ 245 SelectorArg: "23", 246 Editable: shared.Editable{ 247 Milestone: shared.EditableString{ 248 Value: "GA", 249 Edited: true, 250 }, 251 }, 252 }, 253 wantsErr: false, 254 }, 255 } 256 for _, tt := range tests { 257 t.Run(tt.name, func(t *testing.T) { 258 io, stdin, _, _ := iostreams.Test() 259 io.SetStdoutTTY(true) 260 io.SetStdinTTY(true) 261 io.SetStderrTTY(true) 262 263 if tt.stdin != "" { 264 _, _ = stdin.WriteString(tt.stdin) 265 } 266 267 f := &cmdutil.Factory{ 268 IOStreams: io, 269 } 270 271 argv, err := shlex.Split(tt.input) 272 assert.NoError(t, err) 273 274 var gotOpts *EditOptions 275 cmd := NewCmdEdit(f, func(opts *EditOptions) error { 276 gotOpts = opts 277 return nil 278 }) 279 cmd.Flags().BoolP("help", "x", false, "") 280 281 cmd.SetArgs(argv) 282 cmd.SetIn(&bytes.Buffer{}) 283 cmd.SetOut(&bytes.Buffer{}) 284 cmd.SetErr(&bytes.Buffer{}) 285 286 _, err = cmd.ExecuteC() 287 if tt.wantsErr { 288 assert.Error(t, err) 289 return 290 } 291 292 assert.NoError(t, err) 293 assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg) 294 assert.Equal(t, tt.output.Interactive, gotOpts.Interactive) 295 assert.Equal(t, tt.output.Editable, gotOpts.Editable) 296 }) 297 } 298 } 299 300 func Test_editRun(t *testing.T) { 301 tests := []struct { 302 name string 303 input *EditOptions 304 httpStubs func(*testing.T, *httpmock.Registry) 305 stdout string 306 stderr string 307 }{ 308 { 309 name: "non-interactive", 310 input: &EditOptions{ 311 SelectorArg: "123", 312 Finder: shared.NewMockFinder("123", &api.PullRequest{ 313 URL: "https://github.com/OWNER/REPO/pull/123", 314 }, ghrepo.New("OWNER", "REPO")), 315 Interactive: false, 316 Editable: shared.Editable{ 317 Title: shared.EditableString{ 318 Value: "new title", 319 Edited: true, 320 }, 321 Body: shared.EditableString{ 322 Value: "new body", 323 Edited: true, 324 }, 325 Base: shared.EditableString{ 326 Value: "base-branch-name", 327 Edited: true, 328 }, 329 Reviewers: shared.EditableSlice{ 330 Add: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot"}, 331 Remove: []string{"dependabot"}, 332 Edited: true, 333 }, 334 Assignees: shared.EditableSlice{ 335 Add: []string{"monalisa", "hubot"}, 336 Remove: []string{"octocat"}, 337 Edited: true, 338 }, 339 Labels: shared.EditableSlice{ 340 Add: []string{"feature", "TODO", "bug"}, 341 Remove: []string{"docs"}, 342 Edited: true, 343 }, 344 Projects: shared.EditableSlice{ 345 Add: []string{"Cleanup", "Roadmap"}, 346 Remove: []string{"Features"}, 347 Edited: true, 348 }, 349 Milestone: shared.EditableString{ 350 Value: "GA", 351 Edited: true, 352 }, 353 }, 354 Fetcher: testFetcher{}, 355 }, 356 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 357 mockRepoMetadata(t, reg, false) 358 mockPullRequestUpdate(t, reg) 359 mockPullRequestReviewersUpdate(t, reg) 360 }, 361 stdout: "https://github.com/OWNER/REPO/pull/123\n", 362 }, 363 { 364 name: "non-interactive skip reviewers", 365 input: &EditOptions{ 366 SelectorArg: "123", 367 Finder: shared.NewMockFinder("123", &api.PullRequest{ 368 URL: "https://github.com/OWNER/REPO/pull/123", 369 }, ghrepo.New("OWNER", "REPO")), 370 Interactive: false, 371 Editable: shared.Editable{ 372 Title: shared.EditableString{ 373 Value: "new title", 374 Edited: true, 375 }, 376 Body: shared.EditableString{ 377 Value: "new body", 378 Edited: true, 379 }, 380 Base: shared.EditableString{ 381 Value: "base-branch-name", 382 Edited: true, 383 }, 384 Assignees: shared.EditableSlice{ 385 Add: []string{"monalisa", "hubot"}, 386 Remove: []string{"octocat"}, 387 Edited: true, 388 }, 389 Labels: shared.EditableSlice{ 390 Value: []string{"feature", "TODO", "bug"}, 391 Remove: []string{"docs"}, 392 Edited: true, 393 }, 394 Projects: shared.EditableSlice{ 395 Value: []string{"Cleanup", "Roadmap"}, 396 Remove: []string{"Features"}, 397 Edited: true, 398 }, 399 Milestone: shared.EditableString{ 400 Value: "GA", 401 Edited: true, 402 }, 403 }, 404 Fetcher: testFetcher{}, 405 }, 406 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 407 mockRepoMetadata(t, reg, true) 408 mockPullRequestUpdate(t, reg) 409 }, 410 stdout: "https://github.com/OWNER/REPO/pull/123\n", 411 }, 412 { 413 name: "interactive", 414 input: &EditOptions{ 415 SelectorArg: "123", 416 Finder: shared.NewMockFinder("123", &api.PullRequest{ 417 URL: "https://github.com/OWNER/REPO/pull/123", 418 }, ghrepo.New("OWNER", "REPO")), 419 Interactive: true, 420 Surveyor: testSurveyor{}, 421 Fetcher: testFetcher{}, 422 EditorRetriever: testEditorRetriever{}, 423 }, 424 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 425 mockRepoMetadata(t, reg, false) 426 mockPullRequestUpdate(t, reg) 427 mockPullRequestReviewersUpdate(t, reg) 428 }, 429 stdout: "https://github.com/OWNER/REPO/pull/123\n", 430 }, 431 { 432 name: "interactive skip reviewers", 433 input: &EditOptions{ 434 SelectorArg: "123", 435 Finder: shared.NewMockFinder("123", &api.PullRequest{ 436 URL: "https://github.com/OWNER/REPO/pull/123", 437 }, ghrepo.New("OWNER", "REPO")), 438 Interactive: true, 439 Surveyor: testSurveyor{skipReviewers: true}, 440 Fetcher: testFetcher{}, 441 EditorRetriever: testEditorRetriever{}, 442 }, 443 httpStubs: func(t *testing.T, reg *httpmock.Registry) { 444 mockRepoMetadata(t, reg, true) 445 mockPullRequestUpdate(t, reg) 446 }, 447 stdout: "https://github.com/OWNER/REPO/pull/123\n", 448 }, 449 } 450 for _, tt := range tests { 451 io, _, stdout, stderr := iostreams.Test() 452 io.SetStdoutTTY(true) 453 io.SetStdinTTY(true) 454 io.SetStderrTTY(true) 455 456 reg := &httpmock.Registry{} 457 defer reg.Verify(t) 458 tt.httpStubs(t, reg) 459 460 httpClient := func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } 461 462 tt.input.IO = io 463 tt.input.HttpClient = httpClient 464 465 t.Run(tt.name, func(t *testing.T) { 466 err := editRun(tt.input) 467 assert.NoError(t, err) 468 assert.Equal(t, tt.stdout, stdout.String()) 469 assert.Equal(t, tt.stderr, stderr.String()) 470 }) 471 } 472 } 473 474 func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry, skipReviewers bool) { 475 reg.Register( 476 httpmock.GraphQL(`query RepositoryAssignableUsers\b`), 477 httpmock.StringResponse(` 478 { "data": { "repository": { "assignableUsers": { 479 "nodes": [ 480 { "login": "hubot", "id": "HUBOTID" }, 481 { "login": "MonaLisa", "id": "MONAID" } 482 ], 483 "pageInfo": { "hasNextPage": false } 484 } } } } 485 `)) 486 reg.Register( 487 httpmock.GraphQL(`query RepositoryLabelList\b`), 488 httpmock.StringResponse(` 489 { "data": { "repository": { "labels": { 490 "nodes": [ 491 { "name": "feature", "id": "FEATUREID" }, 492 { "name": "TODO", "id": "TODOID" }, 493 { "name": "bug", "id": "BUGID" } 494 ], 495 "pageInfo": { "hasNextPage": false } 496 } } } } 497 `)) 498 reg.Register( 499 httpmock.GraphQL(`query RepositoryMilestoneList\b`), 500 httpmock.StringResponse(` 501 { "data": { "repository": { "milestones": { 502 "nodes": [ 503 { "title": "GA", "id": "GAID" }, 504 { "title": "Big One.oh", "id": "BIGONEID" } 505 ], 506 "pageInfo": { "hasNextPage": false } 507 } } } } 508 `)) 509 reg.Register( 510 httpmock.GraphQL(`query RepositoryProjectList\b`), 511 httpmock.StringResponse(` 512 { "data": { "repository": { "projects": { 513 "nodes": [ 514 { "name": "Cleanup", "id": "CLEANUPID" }, 515 { "name": "Roadmap", "id": "ROADMAPID" } 516 ], 517 "pageInfo": { "hasNextPage": false } 518 } } } } 519 `)) 520 reg.Register( 521 httpmock.GraphQL(`query OrganizationProjectList\b`), 522 httpmock.StringResponse(` 523 { "data": { "organization": { "projects": { 524 "nodes": [ 525 { "name": "Triage", "id": "TRIAGEID" } 526 ], 527 "pageInfo": { "hasNextPage": false } 528 } } } } 529 `)) 530 if !skipReviewers { 531 reg.Register( 532 httpmock.GraphQL(`query OrganizationTeamList\b`), 533 httpmock.StringResponse(` 534 { "data": { "organization": { "teams": { 535 "nodes": [ 536 { "slug": "external", "id": "EXTERNALID" }, 537 { "slug": "core", "id": "COREID" } 538 ], 539 "pageInfo": { "hasNextPage": false } 540 } } } } 541 `)) 542 } 543 } 544 545 func mockPullRequestUpdate(t *testing.T, reg *httpmock.Registry) { 546 reg.Register( 547 httpmock.GraphQL(`mutation PullRequestUpdate\b`), 548 httpmock.StringResponse(`{}`)) 549 } 550 551 func mockPullRequestReviewersUpdate(t *testing.T, reg *httpmock.Registry) { 552 reg.Register( 553 httpmock.GraphQL(`mutation PullRequestUpdateRequestReviews\b`), 554 httpmock.StringResponse(`{}`)) 555 } 556 557 type testFetcher struct{} 558 type testSurveyor struct { 559 skipReviewers bool 560 } 561 type testEditorRetriever struct{} 562 563 func (f testFetcher) EditableOptionsFetch(client *api.Client, repo ghrepo.Interface, opts *shared.Editable) error { 564 return shared.FetchOptions(client, repo, opts) 565 } 566 567 func (s testSurveyor) FieldsToEdit(e *shared.Editable) error { 568 e.Title.Edited = true 569 e.Body.Edited = true 570 if !s.skipReviewers { 571 e.Reviewers.Edited = true 572 } 573 e.Assignees.Edited = true 574 e.Labels.Edited = true 575 e.Projects.Edited = true 576 e.Milestone.Edited = true 577 return nil 578 } 579 580 func (s testSurveyor) EditFields(e *shared.Editable, _ string) error { 581 e.Title.Value = "new title" 582 e.Body.Value = "new body" 583 if !s.skipReviewers { 584 e.Reviewers.Value = []string{"monalisa", "hubot", "OWNER/core", "OWNER/external"} 585 } 586 e.Assignees.Value = []string{"monalisa", "hubot"} 587 e.Labels.Value = []string{"feature", "TODO", "bug"} 588 e.Projects.Value = []string{"Cleanup", "Roadmap"} 589 e.Milestone.Value = "GA" 590 return nil 591 } 592 593 func (t testEditorRetriever) Retrieve() (string, error) { 594 return "vim", nil 595 }