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

     1  package create
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"testing"
     8  
     9  	"github.com/ungtb10d/cli/v2/git"
    10  	"github.com/ungtb10d/cli/v2/internal/config"
    11  	"github.com/ungtb10d/cli/v2/internal/prompter"
    12  	"github.com/ungtb10d/cli/v2/internal/run"
    13  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    14  	"github.com/ungtb10d/cli/v2/pkg/httpmock"
    15  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    16  	"github.com/google/shlex"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestNewCmdCreate(t *testing.T) {
    22  	tests := []struct {
    23  		name      string
    24  		tty       bool
    25  		cli       string
    26  		wantsErr  bool
    27  		errMsg    string
    28  		wantsOpts CreateOptions
    29  	}{
    30  		{
    31  			name:      "no args tty",
    32  			tty:       true,
    33  			cli:       "",
    34  			wantsOpts: CreateOptions{Interactive: true},
    35  		},
    36  		{
    37  			name:     "no args no-tty",
    38  			tty:      false,
    39  			cli:      "",
    40  			wantsErr: true,
    41  			errMsg:   "at least one argument required in non-interactive mode",
    42  		},
    43  		{
    44  			name: "new repo from remote",
    45  			cli:  "NEWREPO --public --clone",
    46  			wantsOpts: CreateOptions{
    47  				Name:   "NEWREPO",
    48  				Public: true,
    49  				Clone:  true},
    50  		},
    51  		{
    52  			name:     "no visibility",
    53  			tty:      true,
    54  			cli:      "NEWREPO",
    55  			wantsErr: true,
    56  			errMsg:   "`--public`, `--private`, or `--internal` required when not running interactively",
    57  		},
    58  		{
    59  			name:     "multiple visibility",
    60  			tty:      true,
    61  			cli:      "NEWREPO --public --private",
    62  			wantsErr: true,
    63  			errMsg:   "expected exactly one of `--public`, `--private`, or `--internal`",
    64  		},
    65  		{
    66  			name: "new remote from local",
    67  			cli:  "--source=/path/to/repo --private",
    68  			wantsOpts: CreateOptions{
    69  				Private: true,
    70  				Source:  "/path/to/repo"},
    71  		},
    72  		{
    73  			name: "new remote from local with remote",
    74  			cli:  "--source=/path/to/repo --public --remote upstream",
    75  			wantsOpts: CreateOptions{
    76  				Public: true,
    77  				Source: "/path/to/repo",
    78  				Remote: "upstream",
    79  			},
    80  		},
    81  		{
    82  			name: "new remote from local with push",
    83  			cli:  "--source=/path/to/repo --push --public",
    84  			wantsOpts: CreateOptions{
    85  				Public: true,
    86  				Source: "/path/to/repo",
    87  				Push:   true,
    88  			},
    89  		},
    90  		{
    91  			name: "new remote from local without visibility",
    92  			cli:  "--source=/path/to/repo --push",
    93  			wantsOpts: CreateOptions{
    94  				Source: "/path/to/repo",
    95  				Push:   true,
    96  			},
    97  			wantsErr: true,
    98  			errMsg:   "`--public`, `--private`, or `--internal` required when not running interactively",
    99  		},
   100  		{
   101  			name:     "source with template",
   102  			cli:      "--source=/path/to/repo --private --template mytemplate",
   103  			wantsErr: true,
   104  			errMsg:   "the `--source` option is not supported with `--clone`, `--template`, `--license`, or `--gitignore`",
   105  		},
   106  		{
   107  			name:     "include all branches without template",
   108  			cli:      "--source=/path/to/repo --private --include-all-branches",
   109  			wantsErr: true,
   110  			errMsg:   "the `--include-all-branches` option is only supported when using `--template`",
   111  		},
   112  		{
   113  			name: "new remote from template with include all branches",
   114  			cli:  "template-repo --template https://github.com/OWNER/REPO --public --include-all-branches",
   115  			wantsOpts: CreateOptions{
   116  				Name:               "template-repo",
   117  				Public:             true,
   118  				Template:           "https://github.com/OWNER/REPO",
   119  				IncludeAllBranches: true,
   120  			},
   121  		},
   122  	}
   123  
   124  	for _, tt := range tests {
   125  		t.Run(tt.name, func(t *testing.T) {
   126  			ios, _, _, _ := iostreams.Test()
   127  			ios.SetStdinTTY(tt.tty)
   128  			ios.SetStdoutTTY(tt.tty)
   129  
   130  			f := &cmdutil.Factory{
   131  				IOStreams: ios,
   132  			}
   133  
   134  			var opts *CreateOptions
   135  			cmd := NewCmdCreate(f, func(o *CreateOptions) error {
   136  				opts = o
   137  				return nil
   138  			})
   139  
   140  			// TODO STUPID HACK
   141  			// cobra aggressively adds help to all commands. since we're not running through the root command
   142  			// (which manages help when running for real) and since create has a '-h' flag (for homepage),
   143  			// cobra blows up when it tried to add a help flag and -h is already in use. This hack adds a
   144  			// dummy help flag with a random shorthand to get around this.
   145  			cmd.Flags().BoolP("help", "x", false, "")
   146  
   147  			args, err := shlex.Split(tt.cli)
   148  			require.NoError(t, err)
   149  			cmd.SetArgs(args)
   150  			cmd.SetIn(&bytes.Buffer{})
   151  			cmd.SetOut(&bytes.Buffer{})
   152  			cmd.SetErr(&bytes.Buffer{})
   153  
   154  			_, err = cmd.ExecuteC()
   155  			if tt.wantsErr {
   156  				assert.Error(t, err)
   157  				assert.Equal(t, tt.errMsg, err.Error())
   158  				return
   159  			} else {
   160  				require.NoError(t, err)
   161  			}
   162  
   163  			assert.Equal(t, tt.wantsOpts.Interactive, opts.Interactive)
   164  			assert.Equal(t, tt.wantsOpts.Source, opts.Source)
   165  			assert.Equal(t, tt.wantsOpts.Name, opts.Name)
   166  			assert.Equal(t, tt.wantsOpts.Public, opts.Public)
   167  			assert.Equal(t, tt.wantsOpts.Internal, opts.Internal)
   168  			assert.Equal(t, tt.wantsOpts.Private, opts.Private)
   169  			assert.Equal(t, tt.wantsOpts.Clone, opts.Clone)
   170  		})
   171  	}
   172  }
   173  
   174  func Test_createRun(t *testing.T) {
   175  	tests := []struct {
   176  		name        string
   177  		tty         bool
   178  		opts        *CreateOptions
   179  		httpStubs   func(*httpmock.Registry)
   180  		promptStubs func(*prompter.PrompterMock)
   181  		execStubs   func(*run.CommandStubber)
   182  		wantStdout  string
   183  		wantErr     bool
   184  		errMsg      string
   185  	}{
   186  		{
   187  			name:       "interactive create from scratch with gitignore and license",
   188  			opts:       &CreateOptions{Interactive: true},
   189  			tty:        true,
   190  			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n",
   191  			promptStubs: func(p *prompter.PrompterMock) {
   192  				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
   193  					switch message {
   194  					case "Would you like to add a README file?":
   195  						return false, nil
   196  					case "Would you like to add a .gitignore?":
   197  						return true, nil
   198  					case "Would you like to add a license?":
   199  						return true, nil
   200  					case `This will create "REPO" as a private repository on GitHub. Continue?`:
   201  						return defaultValue, nil
   202  					case "Clone the new repository locally?":
   203  						return defaultValue, nil
   204  					default:
   205  						return false, fmt.Errorf("unexpected confirm prompt: %s", message)
   206  					}
   207  				}
   208  				p.InputFunc = func(message, defaultValue string) (string, error) {
   209  					switch message {
   210  					case "Repository name":
   211  						return "REPO", nil
   212  					case "Description":
   213  						return "my new repo", nil
   214  					default:
   215  						return "", fmt.Errorf("unexpected input prompt: %s", message)
   216  					}
   217  				}
   218  				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
   219  					switch message {
   220  					case "What would you like to do?":
   221  						return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
   222  					case "Visibility":
   223  						return prompter.IndexFor(options, "Private")
   224  					case "Choose a license":
   225  						return prompter.IndexFor(options, "GNU Lesser General Public License v3.0")
   226  					case "Choose a .gitignore template":
   227  						return prompter.IndexFor(options, "Go")
   228  					default:
   229  						return 0, fmt.Errorf("unexpected select prompt: %s", message)
   230  					}
   231  				}
   232  			},
   233  			httpStubs: func(reg *httpmock.Registry) {
   234  				reg.Register(
   235  					httpmock.REST("GET", "gitignore/templates"),
   236  					httpmock.StringResponse(`["Actionscript","Android","AppceleratorTitanium","Autotools","Bancha","C","C++","Go"]`))
   237  				reg.Register(
   238  					httpmock.REST("GET", "licenses"),
   239  					httpmock.StringResponse(`[{"key": "mit","name": "MIT License"},{"key": "lgpl-3.0","name": "GNU Lesser General Public License v3.0"}]`))
   240  				reg.Register(
   241  					httpmock.REST("POST", "user/repos"),
   242  					httpmock.StringResponse(`{"name":"REPO", "owner":{"login": "OWNER"}, "html_url":"https://github.com/OWNER/REPO"}`))
   243  
   244  			},
   245  			execStubs: func(cs *run.CommandStubber) {
   246  				cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "")
   247  			},
   248  		},
   249  		{
   250  			name: "interactive create from scratch but cancel before submit",
   251  			opts: &CreateOptions{Interactive: true},
   252  			tty:  true,
   253  			promptStubs: func(p *prompter.PrompterMock) {
   254  				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
   255  					switch message {
   256  					case "Would you like to add a README file?":
   257  						return false, nil
   258  					case "Would you like to add a .gitignore?":
   259  						return false, nil
   260  					case "Would you like to add a license?":
   261  						return false, nil
   262  					case `This will create "REPO" as a private repository on GitHub. Continue?`:
   263  						return false, nil
   264  					default:
   265  						return false, fmt.Errorf("unexpected confirm prompt: %s", message)
   266  					}
   267  				}
   268  				p.InputFunc = func(message, defaultValue string) (string, error) {
   269  					switch message {
   270  					case "Repository name":
   271  						return "REPO", nil
   272  					case "Description":
   273  						return "my new repo", nil
   274  					default:
   275  						return "", fmt.Errorf("unexpected input prompt: %s", message)
   276  					}
   277  				}
   278  				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
   279  					switch message {
   280  					case "What would you like to do?":
   281  						return prompter.IndexFor(options, "Create a new repository on GitHub from scratch")
   282  					case "Visibility":
   283  						return prompter.IndexFor(options, "Private")
   284  					default:
   285  						return 0, fmt.Errorf("unexpected select prompt: %s", message)
   286  					}
   287  				}
   288  			},
   289  			wantStdout: "",
   290  			wantErr:    true,
   291  			errMsg:     "CancelError",
   292  		},
   293  		{
   294  			name: "interactive with existing repository public",
   295  			opts: &CreateOptions{Interactive: true},
   296  			tty:  true,
   297  			promptStubs: func(p *prompter.PrompterMock) {
   298  				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
   299  					switch message {
   300  					case "Add a remote?":
   301  						return false, nil
   302  					default:
   303  						return false, fmt.Errorf("unexpected confirm prompt: %s", message)
   304  					}
   305  				}
   306  				p.InputFunc = func(message, defaultValue string) (string, error) {
   307  					switch message {
   308  					case "Path to local repository":
   309  						return defaultValue, nil
   310  					case "Repository name":
   311  						return "REPO", nil
   312  					case "Description":
   313  						return "my new repo", nil
   314  					default:
   315  						return "", fmt.Errorf("unexpected input prompt: %s", message)
   316  					}
   317  				}
   318  				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
   319  					switch message {
   320  					case "What would you like to do?":
   321  						return prompter.IndexFor(options, "Push an existing local repository to GitHub")
   322  					case "Visibility":
   323  						return prompter.IndexFor(options, "Private")
   324  					default:
   325  						return 0, fmt.Errorf("unexpected select prompt: %s", message)
   326  					}
   327  				}
   328  			},
   329  			httpStubs: func(reg *httpmock.Registry) {
   330  				reg.Register(
   331  					httpmock.GraphQL(`mutation RepositoryCreate\b`),
   332  					httpmock.StringResponse(`
   333  					{
   334  						"data": {
   335  							"createRepository": {
   336  								"repository": {
   337  									"id": "REPOID",
   338  									"name": "REPO",
   339  									"owner": {"login":"OWNER"},
   340  									"url": "https://github.com/OWNER/REPO"
   341  								}
   342  							}
   343  						}
   344  					}`))
   345  			},
   346  			execStubs: func(cs *run.CommandStubber) {
   347  				cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
   348  				cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
   349  			},
   350  			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n",
   351  		},
   352  		{
   353  			name: "interactive with existing repository public add remote and push",
   354  			opts: &CreateOptions{Interactive: true},
   355  			tty:  true,
   356  			promptStubs: func(p *prompter.PrompterMock) {
   357  				p.ConfirmFunc = func(message string, defaultValue bool) (bool, error) {
   358  					switch message {
   359  					case "Add a remote?":
   360  						return true, nil
   361  					case `Would you like to push commits from the current branch to "origin"?`:
   362  						return true, nil
   363  					default:
   364  						return false, fmt.Errorf("unexpected confirm prompt: %s", message)
   365  					}
   366  				}
   367  				p.InputFunc = func(message, defaultValue string) (string, error) {
   368  					switch message {
   369  					case "Path to local repository":
   370  						return defaultValue, nil
   371  					case "Repository name":
   372  						return "REPO", nil
   373  					case "Description":
   374  						return "my new repo", nil
   375  					case "What should the new remote be called?":
   376  						return defaultValue, nil
   377  					default:
   378  						return "", fmt.Errorf("unexpected input prompt: %s", message)
   379  					}
   380  				}
   381  				p.SelectFunc = func(message, defaultValue string, options []string) (int, error) {
   382  					switch message {
   383  					case "What would you like to do?":
   384  						return prompter.IndexFor(options, "Push an existing local repository to GitHub")
   385  					case "Visibility":
   386  						return prompter.IndexFor(options, "Private")
   387  					default:
   388  						return 0, fmt.Errorf("unexpected select prompt: %s", message)
   389  					}
   390  				}
   391  			},
   392  			httpStubs: func(reg *httpmock.Registry) {
   393  				reg.Register(
   394  					httpmock.GraphQL(`mutation RepositoryCreate\b`),
   395  					httpmock.StringResponse(`
   396  					{
   397  						"data": {
   398  							"createRepository": {
   399  								"repository": {
   400  									"id": "REPOID",
   401  									"name": "REPO",
   402  									"owner": {"login":"OWNER"},
   403  									"url": "https://github.com/OWNER/REPO"
   404  								}
   405  							}
   406  						}
   407  					}`))
   408  			},
   409  			execStubs: func(cs *run.CommandStubber) {
   410  				cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
   411  				cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
   412  				cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
   413  				cs.Register(`git -C . push --set-upstream origin HEAD`, 0, "")
   414  			},
   415  			wantStdout: "✓ Created repository OWNER/REPO on GitHub\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n",
   416  		},
   417  		{
   418  			name: "noninteractive create from scratch",
   419  			opts: &CreateOptions{
   420  				Interactive: false,
   421  				Name:        "REPO",
   422  				Visibility:  "PRIVATE",
   423  			},
   424  			tty: false,
   425  			httpStubs: func(reg *httpmock.Registry) {
   426  				reg.Register(
   427  					httpmock.GraphQL(`mutation RepositoryCreate\b`),
   428  					httpmock.StringResponse(`
   429  					{
   430  						"data": {
   431  							"createRepository": {
   432  								"repository": {
   433  									"id": "REPOID",
   434  									"name": "REPO",
   435  									"owner": {"login":"OWNER"},
   436  									"url": "https://github.com/OWNER/REPO"
   437  								}
   438  							}
   439  						}
   440  					}`))
   441  			},
   442  			wantStdout: "https://github.com/OWNER/REPO\n",
   443  		},
   444  		{
   445  			name: "noninteractive create from source",
   446  			opts: &CreateOptions{
   447  				Interactive: false,
   448  				Source:      ".",
   449  				Name:        "REPO",
   450  				Visibility:  "PRIVATE",
   451  			},
   452  			tty: false,
   453  			httpStubs: func(reg *httpmock.Registry) {
   454  				reg.Register(
   455  					httpmock.GraphQL(`mutation RepositoryCreate\b`),
   456  					httpmock.StringResponse(`
   457  					{
   458  						"data": {
   459  							"createRepository": {
   460  								"repository": {
   461  									"id": "REPOID",
   462  									"name": "REPO",
   463  									"owner": {"login":"OWNER"},
   464  									"url": "https://github.com/OWNER/REPO"
   465  								}
   466  							}
   467  						}
   468  					}`))
   469  			},
   470  			execStubs: func(cs *run.CommandStubber) {
   471  				cs.Register(`git -C . rev-parse --git-dir`, 0, ".git")
   472  				cs.Register(`git -C . rev-parse HEAD`, 0, "commithash")
   473  				cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "")
   474  			},
   475  			wantStdout: "https://github.com/OWNER/REPO\n",
   476  		},
   477  	}
   478  	for _, tt := range tests {
   479  		prompterMock := &prompter.PrompterMock{}
   480  		tt.opts.Prompter = prompterMock
   481  		if tt.promptStubs != nil {
   482  			tt.promptStubs(prompterMock)
   483  		}
   484  
   485  		reg := &httpmock.Registry{}
   486  		if tt.httpStubs != nil {
   487  			tt.httpStubs(reg)
   488  		}
   489  		tt.opts.HttpClient = func() (*http.Client, error) {
   490  			return &http.Client{Transport: reg}, nil
   491  		}
   492  		tt.opts.Config = func() (config.Config, error) {
   493  			return config.NewBlankConfig(), nil
   494  		}
   495  
   496  		tt.opts.GitClient = &git.Client{GitPath: "some/path/git"}
   497  
   498  		ios, _, stdout, stderr := iostreams.Test()
   499  		ios.SetStdinTTY(tt.tty)
   500  		ios.SetStdoutTTY(tt.tty)
   501  		tt.opts.IO = ios
   502  
   503  		t.Run(tt.name, func(t *testing.T) {
   504  			cs, restoreRun := run.Stub()
   505  			defer restoreRun(t)
   506  			if tt.execStubs != nil {
   507  				tt.execStubs(cs)
   508  			}
   509  
   510  			defer reg.Verify(t)
   511  			err := createRun(tt.opts)
   512  			if tt.wantErr {
   513  				assert.Error(t, err)
   514  				assert.Equal(t, tt.errMsg, err.Error())
   515  				return
   516  			}
   517  			assert.NoError(t, err)
   518  			assert.Equal(t, tt.wantStdout, stdout.String())
   519  			assert.Equal(t, "", stderr.String())
   520  		})
   521  	}
   522  }