github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/develop/develop_test.go (about) 1 package develop 2 3 import ( 4 "errors" 5 "net/http" 6 "testing" 7 8 "github.com/ungtb10d/cli/v2/context" 9 "github.com/ungtb10d/cli/v2/git" 10 "github.com/ungtb10d/cli/v2/internal/config" 11 "github.com/ungtb10d/cli/v2/internal/ghrepo" 12 "github.com/ungtb10d/cli/v2/internal/run" 13 "github.com/ungtb10d/cli/v2/pkg/httpmock" 14 "github.com/ungtb10d/cli/v2/pkg/iostreams" 15 "github.com/ungtb10d/cli/v2/pkg/prompt" 16 "github.com/ungtb10d/cli/v2/test" 17 "github.com/stretchr/testify/assert" 18 ) 19 20 func Test_developRun(t *testing.T) { 21 featureEnabledPayload := `{ 22 "data": { 23 "LinkedBranch": { 24 "fields": [ 25 { 26 "name": "id" 27 }, 28 { 29 "name": "ref" 30 } 31 ] 32 } 33 } 34 }` 35 36 featureDisabledPayload := `{ "data": { "LinkedBranch": null } }` 37 38 tests := []struct { 39 name string 40 setup func(*DevelopOptions, *testing.T) func() 41 cmdStubs func(*run.CommandStubber) 42 runStubs func(*run.CommandStubber) 43 remotes map[string]string 44 askStubs func(*prompt.AskStubber) // TODO eventually migrate to PrompterMock 45 httpStubs func(*httpmock.Registry, *testing.T) 46 expectedOut string 47 expectedErrOut string 48 expectedBrowse string 49 wantErr string 50 tty bool 51 }{ 52 {name: "list branches for an issue", 53 setup: func(opts *DevelopOptions, t *testing.T) func() { 54 opts.IssueSelector = "42" 55 opts.List = true 56 return func() {} 57 }, 58 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 59 reg.Register( 60 httpmock.GraphQL(`query LinkedBranch_fields\b`), 61 httpmock.StringResponse(featureEnabledPayload), 62 ) 63 reg.Register( 64 httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), 65 httpmock.GraphQLQuery(`{ 66 "data": { 67 "repository": { 68 "issue": { 69 "linkedBranches": { 70 "edges": [ 71 { 72 "node": { 73 "ref": { 74 "name": "foo" 75 } 76 } 77 }, 78 { 79 "node": { 80 "ref": { 81 "name": "bar" 82 } 83 } 84 } 85 ] 86 } 87 } 88 } 89 } 90 } 91 `, func(query string, inputs map[string]interface{}) { 92 assert.Equal(t, float64(42), inputs["issueNumber"]) 93 assert.Equal(t, "OWNER", inputs["repositoryOwner"]) 94 assert.Equal(t, "REPO", inputs["repositoryName"]) 95 })) 96 }, 97 expectedOut: "foo\nbar\n", 98 }, 99 {name: "list branches for an issue in tty", 100 setup: func(opts *DevelopOptions, t *testing.T) func() { 101 opts.IssueSelector = "42" 102 opts.List = true 103 return func() {} 104 }, 105 tty: true, 106 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 107 reg.Register( 108 httpmock.GraphQL(`query LinkedBranch_fields\b`), 109 httpmock.StringResponse(featureEnabledPayload), 110 ) 111 reg.Register( 112 httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), 113 httpmock.GraphQLQuery(`{ 114 "data": { 115 "repository": { 116 "issue": { 117 "linkedBranches": { 118 "edges": [ 119 { 120 "node": { 121 "ref": { 122 "name": "foo", 123 "repository": { 124 "url": "http://github.localhost/OWNER/REPO" 125 } 126 } 127 } 128 }, 129 { 130 "node": { 131 "ref": { 132 "name": "bar", 133 "repository": { 134 "url": "http://github.localhost/OWNER/OTHER-REPO" 135 } 136 } 137 } 138 } 139 ] 140 } 141 } 142 } 143 } 144 } 145 `, func(query string, inputs map[string]interface{}) { 146 assert.Equal(t, float64(42), inputs["issueNumber"]) 147 assert.Equal(t, "OWNER", inputs["repositoryOwner"]) 148 assert.Equal(t, "REPO", inputs["repositoryName"]) 149 })) 150 }, 151 expectedOut: "\nShowing linked branches for OWNER/REPO#42\n\nfoo http://github.localhost/OWNER/REPO/tree/foo\nbar http://github.localhost/OWNER/OTHER-REPO/tree/bar\n", 152 }, 153 {name: "list branches for an issue providing an issue url", 154 setup: func(opts *DevelopOptions, t *testing.T) func() { 155 opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" 156 opts.List = true 157 return func() {} 158 }, 159 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 160 reg.Register( 161 httpmock.GraphQL(`query LinkedBranch_fields\b`), 162 httpmock.StringResponse(featureEnabledPayload), 163 ) 164 reg.Register( 165 httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), 166 httpmock.GraphQLQuery(`{ 167 "data": { 168 "repository": { 169 "issue": { 170 "linkedBranches": { 171 "edges": [ 172 { 173 "node": { 174 "ref": { 175 "name": "foo" 176 } 177 } 178 }, 179 { 180 "node": { 181 "ref": { 182 "name": "bar" 183 } 184 } 185 } 186 ] 187 } 188 } 189 } 190 } 191 } 192 `, func(query string, inputs map[string]interface{}) { 193 assert.Equal(t, float64(42), inputs["issueNumber"]) 194 assert.Equal(t, "cli", inputs["repositoryOwner"]) 195 assert.Equal(t, "test-repo", inputs["repositoryName"]) 196 })) 197 }, 198 expectedOut: "foo\nbar\n", 199 }, 200 {name: "list branches for an issue providing an issue repo", 201 setup: func(opts *DevelopOptions, t *testing.T) func() { 202 opts.IssueSelector = "42" 203 opts.IssueRepoSelector = "cli/test-repo" 204 opts.List = true 205 return func() {} 206 }, 207 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 208 reg.Register( 209 httpmock.GraphQL(`query LinkedBranch_fields\b`), 210 httpmock.StringResponse(featureEnabledPayload), 211 ) 212 reg.Register( 213 httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), 214 httpmock.GraphQLQuery(`{ 215 "data": { 216 "repository": { 217 "issue": { 218 "linkedBranches": { 219 "edges": [ 220 { 221 "node": { 222 "ref": { 223 "name": "foo" 224 } 225 } 226 }, 227 { 228 "node": { 229 "ref": { 230 "name": "bar" 231 } 232 } 233 } 234 ] 235 } 236 } 237 } 238 } 239 } 240 `, func(query string, inputs map[string]interface{}) { 241 assert.Equal(t, float64(42), inputs["issueNumber"]) 242 assert.Equal(t, "cli", inputs["repositoryOwner"]) 243 assert.Equal(t, "test-repo", inputs["repositoryName"]) 244 })) 245 }, 246 expectedOut: "foo\nbar\n", 247 }, 248 {name: "list branches for an issue providing an issue url and specifying the same repo works", 249 setup: func(opts *DevelopOptions, t *testing.T) func() { 250 opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" 251 opts.IssueRepoSelector = "cli/test-repo" 252 opts.List = true 253 return func() {} 254 }, 255 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 256 reg.Register( 257 httpmock.GraphQL(`query LinkedBranch_fields\b`), 258 httpmock.StringResponse(featureEnabledPayload), 259 ) 260 reg.Register( 261 httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), 262 httpmock.GraphQLQuery(`{ 263 "data": { 264 "repository": { 265 "issue": { 266 "linkedBranches": { 267 "edges": [ 268 { 269 "node": { 270 "ref": { 271 "name": "foo" 272 } 273 } 274 }, 275 { 276 "node": { 277 "ref": { 278 "name": "bar" 279 } 280 } 281 } 282 ] 283 } 284 } 285 } 286 } 287 } 288 `, func(query string, inputs map[string]interface{}) { 289 assert.Equal(t, float64(42), inputs["issueNumber"]) 290 assert.Equal(t, "cli", inputs["repositoryOwner"]) 291 assert.Equal(t, "test-repo", inputs["repositoryName"]) 292 })) 293 }, 294 expectedOut: "foo\nbar\n", 295 }, 296 {name: "list branches for an issue providing an issue url and specifying a different repo returns an error", 297 setup: func(opts *DevelopOptions, t *testing.T) func() { 298 opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" 299 opts.IssueRepoSelector = "cli/other" 300 opts.List = true 301 return func() {} 302 }, 303 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 304 reg.Register( 305 httpmock.GraphQL(`query LinkedBranch_fields\b`), 306 httpmock.StringResponse(featureEnabledPayload), 307 ) 308 }, 309 wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other", 310 }, 311 {name: "returns an error when the feature isn't enabled in the GraphQL API", 312 setup: func(opts *DevelopOptions, t *testing.T) func() { 313 opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" 314 opts.List = true 315 return func() {} 316 }, 317 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 318 reg.Register( 319 httpmock.GraphQL(`query LinkedBranch_fields\b`), 320 httpmock.StringResponse(featureDisabledPayload), 321 ) 322 }, 323 wantErr: "the `gh issue develop` command is not currently available", 324 }, 325 {name: "develop new branch with a name provided", 326 setup: func(opts *DevelopOptions, t *testing.T) func() { 327 opts.Name = "my-branch" 328 opts.BaseBranch = "main" 329 opts.IssueSelector = "123" 330 return func() {} 331 }, 332 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 333 reg.Register( 334 httpmock.GraphQL(`query LinkedBranch_fields\b`), 335 httpmock.StringResponse(featureEnabledPayload), 336 ) 337 reg.Register( 338 httpmock.GraphQL(`query RepositoryInfo\b`), 339 httpmock.StringResponse(` 340 { "data": { "repository": { 341 "id": "REPOID", 342 "hasIssuesEnabled": true 343 } } }`), 344 ) 345 reg.Register( 346 httpmock.GraphQL(`query IssueByNumber\b`), 347 httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) 348 reg.Register( 349 httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), 350 httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) 351 352 reg.Register( 353 httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*issueId: \$issueId,\s+name: \$name,\s+oid: \$oid,`), 354 httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`, 355 func(query string, inputs map[string]interface{}) { 356 assert.Equal(t, "REPOID", inputs["repositoryId"]) 357 assert.Equal(t, "my-branch", inputs["name"]) 358 assert.Equal(t, "yar", inputs["issueId"]) 359 }), 360 ) 361 362 }, 363 expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", 364 }, 365 {name: "develop new branch without a name provided omits the param from the mutation", 366 setup: func(opts *DevelopOptions, t *testing.T) func() { 367 opts.Name = "" 368 opts.BaseBranch = "main" 369 opts.IssueSelector = "123" 370 return func() {} 371 }, 372 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 373 reg.Register( 374 httpmock.GraphQL(`query LinkedBranch_fields\b`), 375 httpmock.StringResponse(featureEnabledPayload), 376 ) 377 reg.Register( 378 httpmock.GraphQL(`query RepositoryInfo\b`), 379 httpmock.StringResponse(` 380 { "data": { "repository": { 381 "id": "REPOID", 382 "hasIssuesEnabled": true 383 } } }`), 384 ) 385 reg.Register( 386 httpmock.GraphQL(`query IssueByNumber\b`), 387 httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) 388 reg.Register( 389 httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), 390 httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) 391 392 reg.Register( 393 httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*\$oid: GitObjectID!, \$repositoryId:.*issueId: \$issueId,\s+oid: \$oid,`), 394 httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-issue-1"} } } } }`, 395 func(query string, inputs map[string]interface{}) { 396 assert.Equal(t, "REPOID", inputs["repositoryId"]) 397 assert.Equal(t, "", inputs["name"]) 398 assert.Equal(t, "yar", inputs["issueId"]) 399 }), 400 ) 401 402 }, 403 expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n", 404 }, 405 {name: "develop providing an issue url and specifying a different repo returns an error", 406 setup: func(opts *DevelopOptions, t *testing.T) func() { 407 opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" 408 opts.IssueRepoSelector = "cli/other" 409 return func() {} 410 }, 411 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 412 reg.Register( 413 httpmock.GraphQL(`query LinkedBranch_fields\b`), 414 httpmock.StringResponse(featureEnabledPayload), 415 ) 416 reg.Register( 417 httpmock.GraphQL(`query RepositoryInfo\b`), 418 httpmock.StringResponse(` 419 { "data": { "repository": { 420 "id": "REPOID", 421 "hasIssuesEnabled": true 422 } } }`), 423 ) 424 }, 425 wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other", 426 }, 427 {name: "develop new branch with checkout when the branch exists locally", 428 setup: func(opts *DevelopOptions, t *testing.T) func() { 429 opts.Name = "my-branch" 430 opts.BaseBranch = "main" 431 opts.IssueSelector = "123" 432 opts.Checkout = true 433 return func() {} 434 }, 435 remotes: map[string]string{ 436 "origin": "OWNER/REPO", 437 }, 438 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 439 reg.Register( 440 httpmock.GraphQL(`query LinkedBranch_fields\b`), 441 httpmock.StringResponse(featureEnabledPayload), 442 ) 443 reg.Register( 444 httpmock.GraphQL(`query RepositoryInfo\b`), 445 httpmock.StringResponse(` 446 { "data": { "repository": { 447 "id": "REPOID", 448 "hasIssuesEnabled": true 449 } } }`), 450 ) 451 reg.Register( 452 httpmock.GraphQL(`query IssueByNumber\b`), 453 httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) 454 reg.Register( 455 httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), 456 httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) 457 458 reg.Register( 459 httpmock.GraphQL(`mutation CreateLinkedBranch\b`), 460 httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`, 461 func(query string, inputs map[string]interface{}) { 462 assert.Equal(t, "REPOID", inputs["repositoryId"]) 463 assert.Equal(t, "my-branch", inputs["name"]) 464 assert.Equal(t, "yar", inputs["issueId"]) 465 }), 466 ) 467 468 }, 469 runStubs: func(cs *run.CommandStubber) { 470 cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") 471 cs.Register(`git checkout my-branch`, 0, "") 472 cs.Register(`git pull --ff-only origin my-branch`, 0, "") 473 }, 474 expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", 475 }, 476 {name: "develop new branch with checkout when the branch does not exist locally", 477 setup: func(opts *DevelopOptions, t *testing.T) func() { 478 opts.Name = "my-branch" 479 opts.BaseBranch = "main" 480 opts.IssueSelector = "123" 481 opts.Checkout = true 482 return func() {} 483 }, 484 remotes: map[string]string{ 485 "origin": "OWNER/REPO", 486 }, 487 httpStubs: func(reg *httpmock.Registry, t *testing.T) { 488 reg.Register( 489 httpmock.GraphQL(`query LinkedBranch_fields\b`), 490 httpmock.StringResponse(featureEnabledPayload), 491 ) 492 reg.Register( 493 httpmock.GraphQL(`query RepositoryInfo\b`), 494 httpmock.StringResponse(` 495 { "data": { "repository": { 496 "id": "REPOID", 497 "hasIssuesEnabled": true 498 } } }`), 499 ) 500 reg.Register( 501 httpmock.GraphQL(`query IssueByNumber\b`), 502 httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) 503 reg.Register( 504 httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), 505 httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) 506 507 reg.Register( 508 httpmock.GraphQL(`mutation CreateLinkedBranch\b`), 509 httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`, 510 func(query string, inputs map[string]interface{}) { 511 assert.Equal(t, "REPOID", inputs["repositoryId"]) 512 assert.Equal(t, "my-branch", inputs["name"]) 513 assert.Equal(t, "yar", inputs["issueId"]) 514 }), 515 ) 516 517 }, 518 runStubs: func(cs *run.CommandStubber) { 519 cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "") 520 cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") 521 cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "") 522 cs.Register(`git pull --ff-only origin my-branch`, 0, "") 523 }, 524 expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", 525 }, 526 } 527 for _, tt := range tests { 528 t.Run(tt.name, func(t *testing.T) { 529 reg := &httpmock.Registry{} 530 defer reg.Verify(t) 531 if tt.httpStubs != nil { 532 tt.httpStubs(reg, t) 533 } 534 535 opts := DevelopOptions{} 536 537 ios, _, stdout, stderr := iostreams.Test() 538 539 ios.SetStdoutTTY(tt.tty) 540 ios.SetStdinTTY(tt.tty) 541 ios.SetStderrTTY(tt.tty) 542 opts.IO = ios 543 544 opts.BaseRepo = func() (ghrepo.Interface, error) { 545 return ghrepo.New("OWNER", "REPO"), nil 546 } 547 opts.HttpClient = func() (*http.Client, error) { 548 return &http.Client{Transport: reg}, nil 549 } 550 opts.Config = func() (config.Config, error) { 551 return config.NewBlankConfig(), nil 552 } 553 554 opts.Remotes = func() (context.Remotes, error) { 555 if len(tt.remotes) == 0 { 556 return nil, errors.New("no remotes") 557 } 558 var remotes context.Remotes 559 for name, repo := range tt.remotes { 560 r, err := ghrepo.FromFullName(repo) 561 if err != nil { 562 return remotes, err 563 } 564 remotes = append(remotes, &context.Remote{ 565 Remote: &git.Remote{Name: name}, 566 Repo: r, 567 }) 568 } 569 return remotes, nil 570 } 571 572 opts.GitClient = &git.Client{GitPath: "some/path/git"} 573 574 cmdStubs, cmdTeardown := run.Stub() 575 defer cmdTeardown(t) 576 if tt.runStubs != nil { 577 tt.runStubs(cmdStubs) 578 } 579 580 cleanSetup := func() {} 581 if tt.setup != nil { 582 cleanSetup = tt.setup(&opts, t) 583 } 584 defer cleanSetup() 585 586 var err error 587 if opts.List { 588 err = developRunList(&opts) 589 } else { 590 591 err = developRunCreate(&opts) 592 } 593 output := &test.CmdOut{ 594 OutBuf: stdout, 595 ErrBuf: stderr, 596 } 597 if tt.wantErr != "" { 598 assert.EqualError(t, err, tt.wantErr) 599 } else { 600 assert.NoError(t, err) 601 assert.Equal(t, tt.expectedOut, output.String()) 602 assert.Equal(t, tt.expectedErrOut, output.Stderr()) 603 } 604 }) 605 } 606 }