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  }