github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/repo/fork/fork_test.go (about)

     1  package fork
     2  
     3  import (
     4  	"bytes"
     5  	"io"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/ungtb10d/cli/v2/context"
    13  	"github.com/ungtb10d/cli/v2/git"
    14  	"github.com/ungtb10d/cli/v2/internal/config"
    15  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    16  	"github.com/ungtb10d/cli/v2/internal/run"
    17  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    18  	"github.com/ungtb10d/cli/v2/pkg/httpmock"
    19  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    20  	"github.com/ungtb10d/cli/v2/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  			name: "with fork name",
   137  			cli:  "--fork-name new-fork",
   138  			wants: ForkOptions{
   139  				Remote:     false,
   140  				RemoteName: "origin",
   141  				ForkName:   "new-fork",
   142  				Rename:     false,
   143  			},
   144  		},
   145  	}
   146  
   147  	for _, tt := range tests {
   148  		t.Run(tt.name, func(t *testing.T) {
   149  			ios, _, _, _ := iostreams.Test()
   150  
   151  			f := &cmdutil.Factory{
   152  				IOStreams: ios,
   153  			}
   154  
   155  			ios.SetStdoutTTY(tt.tty)
   156  			ios.SetStdinTTY(tt.tty)
   157  
   158  			argv, err := shlex.Split(tt.cli)
   159  			assert.NoError(t, err)
   160  
   161  			var gotOpts *ForkOptions
   162  			cmd := NewCmdFork(f, func(opts *ForkOptions) error {
   163  				gotOpts = opts
   164  				return nil
   165  			})
   166  			cmd.SetArgs(argv)
   167  			cmd.SetIn(&bytes.Buffer{})
   168  			cmd.SetOut(&bytes.Buffer{})
   169  			cmd.SetErr(&bytes.Buffer{})
   170  
   171  			_, err = cmd.ExecuteC()
   172  			if tt.wantErr {
   173  				assert.Error(t, err)
   174  				assert.Equal(t, tt.errMsg, err.Error())
   175  				return
   176  			}
   177  			assert.NoError(t, err)
   178  
   179  			assert.Equal(t, tt.wants.RemoteName, gotOpts.RemoteName)
   180  			assert.Equal(t, tt.wants.Remote, gotOpts.Remote)
   181  			assert.Equal(t, tt.wants.PromptRemote, gotOpts.PromptRemote)
   182  			assert.Equal(t, tt.wants.PromptClone, gotOpts.PromptClone)
   183  			assert.Equal(t, tt.wants.Organization, gotOpts.Organization)
   184  			assert.Equal(t, tt.wants.GitArgs, gotOpts.GitArgs)
   185  		})
   186  	}
   187  }
   188  
   189  func TestRepoFork(t *testing.T) {
   190  	forkResult := `{
   191  		"node_id": "123",
   192  		"name": "REPO",
   193  		"clone_url": "https://github.com/someone/repo.git",
   194  		"created_at": "2011-01-26T19:01:12Z",
   195  		"owner": {
   196  			"login": "someone"
   197  		}
   198  	}`
   199  
   200  	forkPost := func(reg *httpmock.Registry) {
   201  		reg.Register(
   202  			httpmock.REST("POST", "repos/OWNER/REPO/forks"),
   203  			httpmock.StringResponse(forkResult))
   204  	}
   205  
   206  	tests := []struct {
   207  		name       string
   208  		opts       *ForkOptions
   209  		tty        bool
   210  		httpStubs  func(*httpmock.Registry)
   211  		execStubs  func(*run.CommandStubber)
   212  		askStubs   func(*prompt.AskStubber)
   213  		cfgStubs   func(*config.ConfigMock)
   214  		remotes    []*context.Remote
   215  		wantOut    string
   216  		wantErrOut string
   217  		wantErr    bool
   218  		errMsg     string
   219  	}{
   220  		{
   221  			name: "implicit match, configured protocol overrides provided",
   222  			tty:  true,
   223  			opts: &ForkOptions{
   224  				Remote:     true,
   225  				RemoteName: "fork",
   226  			},
   227  			remotes: []*context.Remote{
   228  				{
   229  					Remote: &git.Remote{Name: "origin", PushURL: &url.URL{
   230  						Scheme: "ssh",
   231  					}},
   232  					Repo: ghrepo.New("OWNER", "REPO"),
   233  				},
   234  			},
   235  			httpStubs: forkPost,
   236  			execStubs: func(cs *run.CommandStubber) {
   237  				cs.Register(`git remote add -f fork https://github\.com/someone/REPO\.git`, 0, "")
   238  			},
   239  			wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote fork\n",
   240  		},
   241  		{
   242  			name: "implicit match, no configured protocol",
   243  			tty:  true,
   244  			opts: &ForkOptions{
   245  				Remote:     true,
   246  				RemoteName: "fork",
   247  			},
   248  			remotes: []*context.Remote{
   249  				{
   250  					Remote: &git.Remote{Name: "origin", PushURL: &url.URL{
   251  						Scheme: "ssh",
   252  					}},
   253  					Repo: ghrepo.New("OWNER", "REPO"),
   254  				},
   255  			},
   256  			cfgStubs: func(c *config.ConfigMock) {
   257  				c.Set("", "git_protocol", "")
   258  			},
   259  			httpStubs: forkPost,
   260  			execStubs: func(cs *run.CommandStubber) {
   261  				cs.Register(`git remote add -f fork git@github\.com:someone/REPO\.git`, 0, "")
   262  			},
   263  			wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote fork\n",
   264  		},
   265  		{
   266  			name: "implicit with negative interactive choices",
   267  			tty:  true,
   268  			opts: &ForkOptions{
   269  				PromptRemote: true,
   270  				Rename:       true,
   271  				RemoteName:   defaultRemoteName,
   272  			},
   273  			httpStubs: forkPost,
   274  			askStubs: func(as *prompt.AskStubber) {
   275  				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
   276  				as.StubOne(false)
   277  			},
   278  			wantErrOut: "✓ Created fork someone/REPO\n",
   279  		},
   280  		{
   281  			name: "implicit with interactive choices",
   282  			tty:  true,
   283  			opts: &ForkOptions{
   284  				PromptRemote: true,
   285  				Rename:       true,
   286  				RemoteName:   defaultRemoteName,
   287  			},
   288  			httpStubs: forkPost,
   289  			execStubs: func(cs *run.CommandStubber) {
   290  				cs.Register("git remote rename origin upstream", 0, "")
   291  				cs.Register(`git remote add -f origin https://github.com/someone/REPO.git`, 0, "")
   292  			},
   293  			askStubs: func(as *prompt.AskStubber) {
   294  				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
   295  				as.StubOne(true)
   296  			},
   297  			wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote origin\n",
   298  		},
   299  		{
   300  			name: "implicit tty reuse existing remote",
   301  			tty:  true,
   302  			opts: &ForkOptions{
   303  				Remote:     true,
   304  				RemoteName: defaultRemoteName,
   305  			},
   306  			remotes: []*context.Remote{
   307  				{
   308  					Remote: &git.Remote{Name: "origin", FetchURL: &url.URL{}},
   309  					Repo:   ghrepo.New("someone", "REPO"),
   310  				},
   311  				{
   312  					Remote: &git.Remote{Name: "upstream", FetchURL: &url.URL{}},
   313  					Repo:   ghrepo.New("OWNER", "REPO"),
   314  				},
   315  			},
   316  			httpStubs:  forkPost,
   317  			wantErrOut: "✓ Created fork someone/REPO\n✓ Using existing remote origin\n",
   318  		},
   319  		{
   320  			name: "implicit tty remote exists",
   321  			// gh repo fork --remote --remote-name origin | cat
   322  			tty: true,
   323  			opts: &ForkOptions{
   324  				Remote:     true,
   325  				RemoteName: defaultRemoteName,
   326  			},
   327  			httpStubs: forkPost,
   328  			wantErr:   true,
   329  			errMsg:    "a git remote named 'origin' already exists",
   330  		},
   331  		{
   332  			name: "implicit tty current owner forked",
   333  			tty:  true,
   334  			opts: &ForkOptions{
   335  				Repository: "someone/REPO",
   336  			},
   337  			httpStubs: func(reg *httpmock.Registry) {
   338  				reg.Register(
   339  					httpmock.REST("POST", "repos/someone/REPO/forks"),
   340  					httpmock.StringResponse(forkResult))
   341  			},
   342  			wantErr: true,
   343  			errMsg:  "failed to fork: someone/REPO cannot be forked",
   344  		},
   345  		{
   346  			name: "implicit tty already forked",
   347  			tty:  true,
   348  			opts: &ForkOptions{
   349  				Since: func(t time.Time) time.Duration {
   350  					return 120 * time.Second
   351  				},
   352  			},
   353  			httpStubs:  forkPost,
   354  			wantErrOut: "! someone/REPO already exists\n",
   355  		},
   356  		{
   357  			name: "implicit tty --remote",
   358  			tty:  true,
   359  			opts: &ForkOptions{
   360  				Remote:     true,
   361  				RemoteName: defaultRemoteName,
   362  				Rename:     true,
   363  			},
   364  			httpStubs: forkPost,
   365  			execStubs: func(cs *run.CommandStubber) {
   366  				cs.Register("git remote rename origin upstream", 0, "")
   367  				cs.Register(`git remote add -f origin https://github.com/someone/REPO.git`, 0, "")
   368  			},
   369  			wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote origin\n",
   370  		},
   371  		{
   372  			name: "implicit nontty reuse existing remote",
   373  			opts: &ForkOptions{
   374  				Remote:     true,
   375  				RemoteName: defaultRemoteName,
   376  				Rename:     true,
   377  			},
   378  			remotes: []*context.Remote{
   379  				{
   380  					Remote: &git.Remote{Name: "origin", FetchURL: &url.URL{}},
   381  					Repo:   ghrepo.New("someone", "REPO"),
   382  				},
   383  				{
   384  					Remote: &git.Remote{Name: "upstream", FetchURL: &url.URL{}},
   385  					Repo:   ghrepo.New("OWNER", "REPO"),
   386  				},
   387  			},
   388  			httpStubs: forkPost,
   389  		},
   390  		{
   391  			name: "implicit nontty remote exists",
   392  			// gh repo fork --remote --remote-name origin | cat
   393  			opts: &ForkOptions{
   394  				Remote:     true,
   395  				RemoteName: defaultRemoteName,
   396  			},
   397  			httpStubs: forkPost,
   398  			wantErr:   true,
   399  			errMsg:    "a git remote named 'origin' already exists",
   400  		},
   401  		{
   402  			name: "implicit nontty already forked",
   403  			opts: &ForkOptions{
   404  				Since: func(t time.Time) time.Duration {
   405  					return 120 * time.Second
   406  				},
   407  			},
   408  			httpStubs:  forkPost,
   409  			wantErrOut: "someone/REPO already exists",
   410  		},
   411  		{
   412  			name: "implicit nontty --remote",
   413  			opts: &ForkOptions{
   414  				Remote:     true,
   415  				RemoteName: defaultRemoteName,
   416  				Rename:     true,
   417  			},
   418  			httpStubs: forkPost,
   419  			execStubs: func(cs *run.CommandStubber) {
   420  				cs.Register("git remote rename origin upstream", 0, "")
   421  				cs.Register(`git remote add -f origin https://github.com/someone/REPO.git`, 0, "")
   422  			},
   423  		},
   424  		{
   425  			name:      "implicit nontty no args",
   426  			opts:      &ForkOptions{},
   427  			httpStubs: forkPost,
   428  		},
   429  		{
   430  			name: "passes git flags",
   431  			tty:  true,
   432  			opts: &ForkOptions{
   433  				Repository: "OWNER/REPO",
   434  				GitArgs:    []string{"--depth", "1"},
   435  				Clone:      true,
   436  			},
   437  			httpStubs: forkPost,
   438  			execStubs: func(cs *run.CommandStubber) {
   439  				cs.Register(`git clone --depth 1 https://github.com/someone/REPO\.git`, 0, "")
   440  				cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
   441  			},
   442  			wantErrOut: "✓ Created fork someone/REPO\n✓ Cloned fork\n",
   443  		},
   444  		{
   445  			name: "repo arg fork to org",
   446  			tty:  true,
   447  			opts: &ForkOptions{
   448  				Repository:   "OWNER/REPO",
   449  				Organization: "gamehendge",
   450  				Clone:        true,
   451  			},
   452  			httpStubs: func(reg *httpmock.Registry) {
   453  				reg.Register(
   454  					httpmock.REST("POST", "repos/OWNER/REPO/forks"),
   455  					func(req *http.Request) (*http.Response, error) {
   456  						bb, err := io.ReadAll(req.Body)
   457  						if err != nil {
   458  							return nil, err
   459  						}
   460  						assert.Equal(t, `{"organization":"gamehendge"}`, strings.TrimSpace(string(bb)))
   461  						return &http.Response{
   462  							Request:    req,
   463  							StatusCode: 200,
   464  							Body:       io.NopCloser(bytes.NewBufferString(`{"name":"REPO", "owner":{"login":"gamehendge"}}`)),
   465  						}, nil
   466  					})
   467  			},
   468  			execStubs: func(cs *run.CommandStubber) {
   469  				cs.Register(`git clone https://github.com/gamehendge/REPO\.git`, 0, "")
   470  				cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
   471  			},
   472  			wantErrOut: "✓ Created fork gamehendge/REPO\n✓ Cloned fork\n",
   473  		},
   474  		{
   475  			name: "repo arg url arg",
   476  			tty:  true,
   477  			opts: &ForkOptions{
   478  				Repository: "https://github.com/OWNER/REPO.git",
   479  				Clone:      true,
   480  			},
   481  			httpStubs: forkPost,
   482  			execStubs: func(cs *run.CommandStubber) {
   483  				cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
   484  				cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
   485  			},
   486  			wantErrOut: "✓ Created fork someone/REPO\n✓ Cloned fork\n",
   487  		},
   488  		{
   489  			name: "repo arg interactive no clone",
   490  			tty:  true,
   491  			opts: &ForkOptions{
   492  				Repository:  "OWNER/REPO",
   493  				PromptClone: true,
   494  			},
   495  			httpStubs: forkPost,
   496  			askStubs: func(as *prompt.AskStubber) {
   497  				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
   498  				as.StubOne(false)
   499  			},
   500  			wantErrOut: "✓ Created fork someone/REPO\n",
   501  		},
   502  		{
   503  			name: "repo arg interactive",
   504  			tty:  true,
   505  			opts: &ForkOptions{
   506  				Repository:  "OWNER/REPO",
   507  				PromptClone: true,
   508  			},
   509  			httpStubs: forkPost,
   510  			askStubs: func(as *prompt.AskStubber) {
   511  				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
   512  				as.StubOne(true)
   513  			},
   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: "✓ Created fork someone/REPO\n✓ Cloned fork\n",
   519  		},
   520  		{
   521  			name: "repo arg interactive already forked",
   522  			tty:  true,
   523  			opts: &ForkOptions{
   524  				Repository:  "OWNER/REPO",
   525  				PromptClone: true,
   526  				Since: func(t time.Time) time.Duration {
   527  					return 120 * time.Second
   528  				},
   529  			},
   530  			httpStubs: forkPost,
   531  			askStubs: func(as *prompt.AskStubber) {
   532  				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
   533  				as.StubOne(true)
   534  			},
   535  			execStubs: func(cs *run.CommandStubber) {
   536  				cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
   537  				cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
   538  			},
   539  			wantErrOut: "! someone/REPO already exists\n✓ Cloned fork\n",
   540  		},
   541  		{
   542  			name: "repo arg nontty no flags",
   543  			opts: &ForkOptions{
   544  				Repository: "OWNER/REPO",
   545  			},
   546  			httpStubs: forkPost,
   547  		},
   548  		{
   549  			name: "repo arg nontty repo already exists",
   550  			opts: &ForkOptions{
   551  				Repository: "OWNER/REPO",
   552  				Since: func(t time.Time) time.Duration {
   553  					return 120 * time.Second
   554  				},
   555  			},
   556  			httpStubs:  forkPost,
   557  			wantErrOut: "someone/REPO already exists",
   558  		},
   559  		{
   560  			name: "repo arg nontty clone arg already exists",
   561  			opts: &ForkOptions{
   562  				Repository: "OWNER/REPO",
   563  				Clone:      true,
   564  				Since: func(t time.Time) time.Duration {
   565  					return 120 * time.Second
   566  				},
   567  			},
   568  			httpStubs: forkPost,
   569  			execStubs: func(cs *run.CommandStubber) {
   570  				cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
   571  				cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
   572  			},
   573  			wantErrOut: "someone/REPO already exists",
   574  		},
   575  		{
   576  			name: "repo arg nontty clone arg",
   577  			opts: &ForkOptions{
   578  				Repository: "OWNER/REPO",
   579  				Clone:      true,
   580  			},
   581  			httpStubs: forkPost,
   582  			execStubs: func(cs *run.CommandStubber) {
   583  				cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
   584  				cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "")
   585  			},
   586  		},
   587  		{
   588  			name: "non tty repo arg with fork-name",
   589  			opts: &ForkOptions{
   590  				Repository: "someone/REPO",
   591  				Clone:      false,
   592  				ForkName:   "NEW_REPO",
   593  			},
   594  			tty: false,
   595  			httpStubs: func(reg *httpmock.Registry) {
   596  				forkResult := `{
   597  					"node_id": "123",
   598  					"name": "REPO",
   599  					"clone_url": "https://github.com/OWNER/REPO.git",
   600  					"created_at": "2011-01-26T19:01:12Z",
   601  					"owner": {
   602  						"login": "OWNER"
   603  					}
   604  				}`
   605  				renameResult := `{
   606  					"node_id": "1234",
   607  					"name": "NEW_REPO",
   608  					"clone_url": "https://github.com/OWNER/NEW_REPO.git",
   609  					"created_at": "2012-01-26T19:01:12Z",
   610  					"owner": {
   611  						"login": "OWNER"
   612  					}
   613  				}`
   614  				reg.Register(
   615  					httpmock.REST("POST", "repos/someone/REPO/forks"),
   616  					httpmock.StringResponse(forkResult))
   617  				reg.Register(
   618  					httpmock.REST("PATCH", "repos/OWNER/REPO"),
   619  					httpmock.StringResponse(renameResult))
   620  			},
   621  			wantErrOut: "",
   622  		},
   623  		{
   624  			name: "tty repo arg with fork-name",
   625  			opts: &ForkOptions{
   626  				Repository: "someone/REPO",
   627  				Clone:      false,
   628  				ForkName:   "NEW_REPO",
   629  			},
   630  			tty: true,
   631  			httpStubs: func(reg *httpmock.Registry) {
   632  				forkResult := `{
   633  					"node_id": "123",
   634  					"name": "REPO",
   635  					"clone_url": "https://github.com/OWNER/REPO.git",
   636  					"created_at": "2011-01-26T19:01:12Z",
   637  					"owner": {
   638  						"login": "OWNER"
   639  					}
   640  				}`
   641  				renameResult := `{
   642  					"node_id": "1234",
   643  					"name": "NEW_REPO",
   644  					"clone_url": "https://github.com/OWNER/NEW_REPO.git",
   645  					"created_at": "2012-01-26T19:01:12Z",
   646  					"owner": {
   647  						"login": "OWNER"
   648  					}
   649  				}`
   650  				reg.Register(
   651  					httpmock.REST("POST", "repos/someone/REPO/forks"),
   652  					httpmock.StringResponse(forkResult))
   653  				reg.Register(
   654  					httpmock.REST("PATCH", "repos/OWNER/REPO"),
   655  					httpmock.StringResponse(renameResult))
   656  			},
   657  			wantErrOut: "✓ Created fork OWNER/REPO\n✓ Renamed fork to OWNER/NEW_REPO\n",
   658  		},
   659  	}
   660  
   661  	for _, tt := range tests {
   662  		ios, _, stdout, stderr := iostreams.Test()
   663  		ios.SetStdinTTY(tt.tty)
   664  		ios.SetStdoutTTY(tt.tty)
   665  		ios.SetStderrTTY(tt.tty)
   666  		tt.opts.IO = ios
   667  
   668  		tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
   669  			return ghrepo.New("OWNER", "REPO"), nil
   670  		}
   671  
   672  		reg := &httpmock.Registry{}
   673  		if tt.httpStubs != nil {
   674  			tt.httpStubs(reg)
   675  		}
   676  		tt.opts.HttpClient = func() (*http.Client, error) {
   677  			return &http.Client{Transport: reg}, nil
   678  		}
   679  
   680  		cfg := config.NewBlankConfig()
   681  		if tt.cfgStubs != nil {
   682  			tt.cfgStubs(cfg)
   683  		}
   684  		tt.opts.Config = func() (config.Config, error) {
   685  			return cfg, nil
   686  		}
   687  
   688  		tt.opts.Remotes = func() (context.Remotes, error) {
   689  			if tt.remotes == nil {
   690  				return []*context.Remote{
   691  					{
   692  						Remote: &git.Remote{
   693  							Name:     "origin",
   694  							FetchURL: &url.URL{},
   695  						},
   696  						Repo: ghrepo.New("OWNER", "REPO"),
   697  					},
   698  				}, nil
   699  			}
   700  			return tt.remotes, nil
   701  		}
   702  
   703  		tt.opts.GitClient = &git.Client{GitPath: "some/path/git"}
   704  
   705  		//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
   706  		as, teardown := prompt.InitAskStubber()
   707  		defer teardown()
   708  		if tt.askStubs != nil {
   709  			tt.askStubs(as)
   710  		}
   711  		cs, restoreRun := run.Stub()
   712  		defer restoreRun(t)
   713  		if tt.execStubs != nil {
   714  			tt.execStubs(cs)
   715  		}
   716  
   717  		t.Run(tt.name, func(t *testing.T) {
   718  			if tt.opts.Since == nil {
   719  				tt.opts.Since = func(t time.Time) time.Duration {
   720  					return 2 * time.Second
   721  				}
   722  			}
   723  			defer reg.Verify(t)
   724  			err := forkRun(tt.opts)
   725  			if tt.wantErr {
   726  				assert.Error(t, err)
   727  				assert.Equal(t, tt.errMsg, err.Error())
   728  				return
   729  			}
   730  
   731  			assert.NoError(t, err)
   732  			assert.Equal(t, tt.wantOut, stdout.String())
   733  			assert.Equal(t, tt.wantErrOut, stderr.String())
   734  		})
   735  	}
   736  }