github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/repo/fork/fork_test.go (about) 1 package fork 2 3 import ( 4 "bytes" 5 "io/ioutil" 6 "net/http" 7 "net/url" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/cli/cli/context" 13 "github.com/cli/cli/git" 14 "github.com/cli/cli/internal/config" 15 "github.com/cli/cli/internal/ghrepo" 16 "github.com/cli/cli/internal/run" 17 "github.com/cli/cli/pkg/cmdutil" 18 "github.com/cli/cli/pkg/httpmock" 19 "github.com/cli/cli/pkg/iostreams" 20 "github.com/cli/cli/pkg/prompt" 21 "github.com/google/shlex" 22 "github.com/stretchr/testify/assert" 23 ) 24 25 func TestNewCmdFork(t *testing.T) { 26 tests := []struct { 27 name string 28 cli string 29 tty bool 30 wants ForkOptions 31 wantErr bool 32 errMsg string 33 }{ 34 { 35 name: "repo with git args", 36 cli: "foo/bar -- --foo=bar", 37 wants: ForkOptions{ 38 Repository: "foo/bar", 39 GitArgs: []string{"--foo=bar"}, 40 RemoteName: "origin", 41 Rename: true, 42 }, 43 }, 44 { 45 name: "git args without repo", 46 cli: "-- --foo bar", 47 wantErr: true, 48 errMsg: "repository argument required when passing 'git clone' flags", 49 }, 50 { 51 name: "repo", 52 cli: "foo/bar", 53 wants: ForkOptions{ 54 Repository: "foo/bar", 55 RemoteName: "origin", 56 Rename: true, 57 GitArgs: []string{}, 58 }, 59 }, 60 { 61 name: "blank remote name", 62 cli: "--remote --remote-name=''", 63 wantErr: true, 64 errMsg: "--remote-name cannot be blank", 65 }, 66 { 67 name: "remote name", 68 cli: "--remote --remote-name=foo", 69 wants: ForkOptions{ 70 RemoteName: "foo", 71 Rename: false, 72 Remote: true, 73 }, 74 }, 75 { 76 name: "blank nontty", 77 cli: "", 78 wants: ForkOptions{ 79 RemoteName: "origin", 80 Rename: true, 81 Organization: "", 82 }, 83 }, 84 { 85 name: "blank tty", 86 cli: "", 87 tty: true, 88 wants: ForkOptions{ 89 RemoteName: "origin", 90 PromptClone: true, 91 PromptRemote: true, 92 Rename: true, 93 Organization: "", 94 }, 95 }, 96 { 97 name: "clone", 98 cli: "--clone", 99 wants: ForkOptions{ 100 RemoteName: "origin", 101 Rename: true, 102 }, 103 }, 104 { 105 name: "remote", 106 cli: "--remote", 107 wants: ForkOptions{ 108 RemoteName: "origin", 109 Remote: true, 110 Rename: true, 111 }, 112 }, 113 { 114 name: "to org", 115 cli: "--org batmanshome", 116 wants: ForkOptions{ 117 RemoteName: "origin", 118 Remote: false, 119 Rename: false, 120 Organization: "batmanshome", 121 }, 122 }, 123 { 124 name: "empty org", 125 cli: " --org=''", 126 wantErr: true, 127 errMsg: "--org cannot be blank", 128 }, 129 { 130 name: "git flags in wrong place", 131 cli: "--depth 1 OWNER/REPO", 132 wantErr: true, 133 errMsg: "unknown flag: --depth\nSeparate git clone flags with '--'.", 134 }, 135 } 136 137 for _, tt := range tests { 138 t.Run(tt.name, func(t *testing.T) { 139 io, _, _, _ := iostreams.Test() 140 141 f := &cmdutil.Factory{ 142 IOStreams: io, 143 } 144 145 io.SetStdoutTTY(tt.tty) 146 io.SetStdinTTY(tt.tty) 147 148 argv, err := shlex.Split(tt.cli) 149 assert.NoError(t, err) 150 151 var gotOpts *ForkOptions 152 cmd := NewCmdFork(f, func(opts *ForkOptions) error { 153 gotOpts = opts 154 return nil 155 }) 156 cmd.SetArgs(argv) 157 cmd.SetIn(&bytes.Buffer{}) 158 cmd.SetOut(&bytes.Buffer{}) 159 cmd.SetErr(&bytes.Buffer{}) 160 161 _, err = cmd.ExecuteC() 162 if tt.wantErr { 163 assert.Error(t, err) 164 assert.Equal(t, tt.errMsg, err.Error()) 165 return 166 } 167 assert.NoError(t, err) 168 169 assert.Equal(t, tt.wants.RemoteName, gotOpts.RemoteName) 170 assert.Equal(t, tt.wants.Remote, gotOpts.Remote) 171 assert.Equal(t, tt.wants.PromptRemote, gotOpts.PromptRemote) 172 assert.Equal(t, tt.wants.PromptClone, gotOpts.PromptClone) 173 assert.Equal(t, tt.wants.Organization, gotOpts.Organization) 174 assert.Equal(t, tt.wants.GitArgs, gotOpts.GitArgs) 175 }) 176 } 177 } 178 179 func TestRepoFork(t *testing.T) { 180 forkPost := func(reg *httpmock.Registry) { 181 forkResult := `{ 182 "node_id": "123", 183 "name": "REPO", 184 "clone_url": "https://github.com/someone/repo.git", 185 "created_at": "2011-01-26T19:01:12Z", 186 "owner": { 187 "login": "someone" 188 } 189 }` 190 reg.Register( 191 httpmock.REST("POST", "repos/OWNER/REPO/forks"), 192 httpmock.StringResponse(forkResult)) 193 } 194 tests := []struct { 195 name string 196 opts *ForkOptions 197 tty bool 198 httpStubs func(*httpmock.Registry) 199 execStubs func(*run.CommandStubber) 200 askStubs func(*prompt.AskStubber) 201 remotes []*context.Remote 202 wantOut string 203 wantErrOut string 204 wantErr bool 205 errMsg string 206 }{ 207 // TODO implicit, override existing remote's protocol with configured protocol 208 { 209 name: "implicit match existing remote's protocol", 210 tty: true, 211 opts: &ForkOptions{ 212 Remote: true, 213 RemoteName: "fork", 214 }, 215 remotes: []*context.Remote{ 216 { 217 Remote: &git.Remote{Name: "origin", PushURL: &url.URL{ 218 Scheme: "ssh", 219 }}, 220 Repo: ghrepo.New("OWNER", "REPO"), 221 }, 222 }, 223 httpStubs: forkPost, 224 execStubs: func(cs *run.CommandStubber) { 225 cs.Register(`git remote add -f fork git@github\.com:someone/REPO\.git`, 0, "") 226 }, 227 wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote fork\n", 228 }, 229 { 230 name: "implicit with negative interactive choices", 231 tty: true, 232 opts: &ForkOptions{ 233 PromptRemote: true, 234 Rename: true, 235 RemoteName: defaultRemoteName, 236 }, 237 httpStubs: forkPost, 238 askStubs: func(as *prompt.AskStubber) { 239 as.StubOne(false) 240 }, 241 wantErrOut: "✓ Created fork someone/REPO\n", 242 }, 243 { 244 name: "implicit with interactive choices", 245 tty: true, 246 opts: &ForkOptions{ 247 PromptRemote: true, 248 Rename: true, 249 RemoteName: defaultRemoteName, 250 }, 251 httpStubs: forkPost, 252 execStubs: func(cs *run.CommandStubber) { 253 cs.Register("git remote rename origin upstream", 0, "") 254 cs.Register(`git remote add -f origin https://github.com/someone/REPO.git`, 0, "") 255 }, 256 askStubs: func(as *prompt.AskStubber) { 257 as.StubOne(true) 258 }, 259 wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote origin\n", 260 }, 261 { 262 name: "implicit tty reuse existing remote", 263 tty: true, 264 opts: &ForkOptions{ 265 Remote: true, 266 RemoteName: defaultRemoteName, 267 }, 268 remotes: []*context.Remote{ 269 { 270 Remote: &git.Remote{Name: "origin", FetchURL: &url.URL{}}, 271 Repo: ghrepo.New("someone", "REPO"), 272 }, 273 { 274 Remote: &git.Remote{Name: "upstream", FetchURL: &url.URL{}}, 275 Repo: ghrepo.New("OWNER", "REPO"), 276 }, 277 }, 278 httpStubs: forkPost, 279 wantErrOut: "✓ Created fork someone/REPO\n✓ Using existing remote origin\n", 280 }, 281 { 282 name: "implicit tty remote exists", 283 // gh repo fork --remote --remote-name origin | cat 284 tty: true, 285 opts: &ForkOptions{ 286 Remote: true, 287 RemoteName: defaultRemoteName, 288 }, 289 httpStubs: forkPost, 290 wantErr: true, 291 errMsg: "a git remote named 'origin' already exists", 292 }, 293 { 294 name: "implicit tty already forked", 295 tty: true, 296 opts: &ForkOptions{ 297 Since: func(t time.Time) time.Duration { 298 return 120 * time.Second 299 }, 300 }, 301 httpStubs: forkPost, 302 wantErrOut: "! someone/REPO already exists\n", 303 }, 304 { 305 name: "implicit tty --remote", 306 tty: true, 307 opts: &ForkOptions{ 308 Remote: true, 309 RemoteName: defaultRemoteName, 310 Rename: true, 311 }, 312 httpStubs: forkPost, 313 execStubs: func(cs *run.CommandStubber) { 314 cs.Register("git remote rename origin upstream", 0, "") 315 cs.Register(`git remote add -f origin https://github.com/someone/REPO.git`, 0, "") 316 }, 317 wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote origin\n", 318 }, 319 { 320 name: "implicit nontty reuse existing remote", 321 opts: &ForkOptions{ 322 Remote: true, 323 RemoteName: defaultRemoteName, 324 Rename: true, 325 }, 326 remotes: []*context.Remote{ 327 { 328 Remote: &git.Remote{Name: "origin", FetchURL: &url.URL{}}, 329 Repo: ghrepo.New("someone", "REPO"), 330 }, 331 { 332 Remote: &git.Remote{Name: "upstream", FetchURL: &url.URL{}}, 333 Repo: ghrepo.New("OWNER", "REPO"), 334 }, 335 }, 336 httpStubs: forkPost, 337 }, 338 { 339 name: "implicit nontty remote exists", 340 // gh repo fork --remote --remote-name origin | cat 341 opts: &ForkOptions{ 342 Remote: true, 343 RemoteName: defaultRemoteName, 344 }, 345 httpStubs: forkPost, 346 wantErr: true, 347 errMsg: "a git remote named 'origin' already exists", 348 }, 349 { 350 name: "implicit nontty already forked", 351 opts: &ForkOptions{ 352 Since: func(t time.Time) time.Duration { 353 return 120 * time.Second 354 }, 355 }, 356 httpStubs: forkPost, 357 wantErrOut: "someone/REPO already exists", 358 }, 359 { 360 name: "implicit nontty --remote", 361 opts: &ForkOptions{ 362 Remote: true, 363 RemoteName: defaultRemoteName, 364 Rename: true, 365 }, 366 httpStubs: forkPost, 367 execStubs: func(cs *run.CommandStubber) { 368 cs.Register("git remote rename origin upstream", 0, "") 369 cs.Register(`git remote add -f origin https://github.com/someone/REPO.git`, 0, "") 370 }, 371 }, 372 { 373 name: "implicit nontty no args", 374 opts: &ForkOptions{}, 375 httpStubs: forkPost, 376 }, 377 { 378 name: "passes git flags", 379 tty: true, 380 opts: &ForkOptions{ 381 Repository: "OWNER/REPO", 382 GitArgs: []string{"--depth", "1"}, 383 Clone: true, 384 }, 385 httpStubs: forkPost, 386 execStubs: func(cs *run.CommandStubber) { 387 cs.Register(`git clone --depth 1 https://github.com/someone/REPO\.git`, 0, "") 388 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 389 }, 390 wantErrOut: "✓ Created fork someone/REPO\n✓ Cloned fork\n", 391 }, 392 { 393 name: "repo arg fork to org", 394 tty: true, 395 opts: &ForkOptions{ 396 Repository: "OWNER/REPO", 397 Organization: "gamehendge", 398 Clone: true, 399 }, 400 httpStubs: func(reg *httpmock.Registry) { 401 reg.Register( 402 httpmock.REST("POST", "repos/OWNER/REPO/forks"), 403 func(req *http.Request) (*http.Response, error) { 404 bb, err := ioutil.ReadAll(req.Body) 405 if err != nil { 406 return nil, err 407 } 408 assert.Equal(t, `{"organization":"gamehendge"}`, strings.TrimSpace(string(bb))) 409 return &http.Response{ 410 Request: req, 411 StatusCode: 200, 412 Body: ioutil.NopCloser(bytes.NewBufferString(`{"name":"REPO", "owner":{"login":"gamehendge"}}`)), 413 }, nil 414 }) 415 }, 416 execStubs: func(cs *run.CommandStubber) { 417 cs.Register(`git clone https://github.com/gamehendge/REPO\.git`, 0, "") 418 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 419 }, 420 wantErrOut: "✓ Created fork gamehendge/REPO\n✓ Cloned fork\n", 421 }, 422 { 423 name: "repo arg url arg", 424 tty: true, 425 opts: &ForkOptions{ 426 Repository: "https://github.com/OWNER/REPO.git", 427 Clone: true, 428 }, 429 httpStubs: forkPost, 430 execStubs: func(cs *run.CommandStubber) { 431 cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") 432 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 433 }, 434 wantErrOut: "✓ Created fork someone/REPO\n✓ Cloned fork\n", 435 }, 436 { 437 name: "repo arg interactive no clone", 438 tty: true, 439 opts: &ForkOptions{ 440 Repository: "OWNER/REPO", 441 PromptClone: true, 442 }, 443 httpStubs: forkPost, 444 askStubs: func(as *prompt.AskStubber) { 445 as.StubOne(false) 446 }, 447 wantErrOut: "✓ Created fork someone/REPO\n", 448 }, 449 { 450 name: "repo arg interactive", 451 tty: true, 452 opts: &ForkOptions{ 453 Repository: "OWNER/REPO", 454 PromptClone: true, 455 }, 456 httpStubs: forkPost, 457 askStubs: func(as *prompt.AskStubber) { 458 as.StubOne(true) 459 }, 460 execStubs: func(cs *run.CommandStubber) { 461 cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") 462 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 463 }, 464 wantErrOut: "✓ Created fork someone/REPO\n✓ Cloned fork\n", 465 }, 466 { 467 name: "repo arg interactive already forked", 468 tty: true, 469 opts: &ForkOptions{ 470 Repository: "OWNER/REPO", 471 PromptClone: true, 472 Since: func(t time.Time) time.Duration { 473 return 120 * time.Second 474 }, 475 }, 476 httpStubs: forkPost, 477 askStubs: func(as *prompt.AskStubber) { 478 as.StubOne(true) 479 }, 480 execStubs: func(cs *run.CommandStubber) { 481 cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") 482 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 483 }, 484 wantErrOut: "! someone/REPO already exists\n✓ Cloned fork\n", 485 }, 486 { 487 name: "repo arg nontty no flags", 488 opts: &ForkOptions{ 489 Repository: "OWNER/REPO", 490 }, 491 httpStubs: forkPost, 492 }, 493 { 494 name: "repo arg nontty repo already exists", 495 opts: &ForkOptions{ 496 Repository: "OWNER/REPO", 497 Since: func(t time.Time) time.Duration { 498 return 120 * time.Second 499 }, 500 }, 501 httpStubs: forkPost, 502 wantErrOut: "someone/REPO already exists", 503 }, 504 { 505 name: "repo arg nontty clone arg already exists", 506 opts: &ForkOptions{ 507 Repository: "OWNER/REPO", 508 Clone: true, 509 Since: func(t time.Time) time.Duration { 510 return 120 * time.Second 511 }, 512 }, 513 httpStubs: forkPost, 514 execStubs: func(cs *run.CommandStubber) { 515 cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") 516 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 517 }, 518 wantErrOut: "someone/REPO already exists", 519 }, 520 { 521 name: "repo arg nontty clone arg", 522 opts: &ForkOptions{ 523 Repository: "OWNER/REPO", 524 Clone: true, 525 }, 526 httpStubs: forkPost, 527 execStubs: func(cs *run.CommandStubber) { 528 cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") 529 cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") 530 }, 531 }, 532 } 533 534 for _, tt := range tests { 535 io, _, stdout, stderr := iostreams.Test() 536 io.SetStdinTTY(tt.tty) 537 io.SetStdoutTTY(tt.tty) 538 io.SetStderrTTY(tt.tty) 539 tt.opts.IO = io 540 541 tt.opts.BaseRepo = func() (ghrepo.Interface, error) { 542 return ghrepo.New("OWNER", "REPO"), nil 543 } 544 545 reg := &httpmock.Registry{} 546 if tt.httpStubs != nil { 547 tt.httpStubs(reg) 548 } 549 tt.opts.HttpClient = func() (*http.Client, error) { 550 return &http.Client{Transport: reg}, nil 551 } 552 553 cfg := config.NewBlankConfig() 554 tt.opts.Config = func() (config.Config, error) { 555 return cfg, nil 556 } 557 558 tt.opts.Remotes = func() (context.Remotes, error) { 559 if tt.remotes == nil { 560 return []*context.Remote{ 561 { 562 Remote: &git.Remote{ 563 Name: "origin", 564 FetchURL: &url.URL{}, 565 }, 566 Repo: ghrepo.New("OWNER", "REPO"), 567 }, 568 }, nil 569 } 570 return tt.remotes, nil 571 } 572 573 as, teardown := prompt.InitAskStubber() 574 defer teardown() 575 if tt.askStubs != nil { 576 tt.askStubs(as) 577 } 578 cs, restoreRun := run.Stub() 579 defer restoreRun(t) 580 if tt.execStubs != nil { 581 tt.execStubs(cs) 582 } 583 584 t.Run(tt.name, func(t *testing.T) { 585 if tt.opts.Since == nil { 586 tt.opts.Since = func(t time.Time) time.Duration { 587 return 2 * time.Second 588 } 589 } 590 defer reg.Verify(t) 591 err := forkRun(tt.opts) 592 if tt.wantErr { 593 assert.Error(t, err) 594 assert.Equal(t, tt.errMsg, err.Error()) 595 return 596 } 597 598 assert.NoError(t, err) 599 assert.Equal(t, tt.wantOut, stdout.String()) 600 assert.Equal(t, tt.wantErrOut, stderr.String()) 601 }) 602 } 603 }