github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/extension/command_test.go (about)

     1  package extension
     2  
     3  import (
     4  	"io"
     5  	"io/ioutil"
     6  	"os"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/MakeNowJust/heredoc"
    11  	"github.com/cli/cli/internal/config"
    12  	"github.com/cli/cli/pkg/cmdutil"
    13  	"github.com/cli/cli/pkg/extensions"
    14  	"github.com/cli/cli/pkg/iostreams"
    15  	"github.com/spf13/cobra"
    16  	"github.com/stretchr/testify/assert"
    17  )
    18  
    19  func TestNewCmdExtension(t *testing.T) {
    20  	tempDir := t.TempDir()
    21  	oldWd, _ := os.Getwd()
    22  	assert.NoError(t, os.Chdir(tempDir))
    23  	t.Cleanup(func() { _ = os.Chdir(oldWd) })
    24  
    25  	tests := []struct {
    26  		name         string
    27  		args         []string
    28  		managerStubs func(em *extensions.ExtensionManagerMock) func(*testing.T)
    29  		isTTY        bool
    30  		wantErr      bool
    31  		errMsg       string
    32  		wantStdout   string
    33  		wantStderr   string
    34  	}{
    35  		{
    36  			name: "install an extension",
    37  			args: []string{"install", "owner/gh-some-ext"},
    38  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
    39  				em.ListFunc = func(bool) []extensions.Extension {
    40  					return []extensions.Extension{}
    41  				}
    42  				em.InstallFunc = func(s string, out, errOut io.Writer) error {
    43  					return nil
    44  				}
    45  				return func(t *testing.T) {
    46  					installCalls := em.InstallCalls()
    47  					assert.Equal(t, 1, len(installCalls))
    48  					assert.Equal(t, "https://github.com/owner/gh-some-ext.git", installCalls[0].URL)
    49  					listCalls := em.ListCalls()
    50  					assert.Equal(t, 1, len(listCalls))
    51  				}
    52  			},
    53  		},
    54  		{
    55  			name: "install an extension with same name as existing extension",
    56  			args: []string{"install", "owner/gh-existing-ext"},
    57  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
    58  				em.ListFunc = func(bool) []extensions.Extension {
    59  					e := &Extension{path: "owner2/gh-existing-ext"}
    60  					return []extensions.Extension{e}
    61  				}
    62  				return func(t *testing.T) {
    63  					calls := em.ListCalls()
    64  					assert.Equal(t, 1, len(calls))
    65  				}
    66  			},
    67  			wantErr: true,
    68  			errMsg:  "there is already an installed extension that provides the \"existing-ext\" command",
    69  		},
    70  		{
    71  			name: "install local extension",
    72  			args: []string{"install", "."},
    73  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
    74  				em.InstallLocalFunc = func(dir string) error {
    75  					return nil
    76  				}
    77  				return func(t *testing.T) {
    78  					calls := em.InstallLocalCalls()
    79  					assert.Equal(t, 1, len(calls))
    80  					assert.Equal(t, tempDir, normalizeDir(calls[0].Dir))
    81  				}
    82  			},
    83  		},
    84  		{
    85  			name:    "upgrade error",
    86  			args:    []string{"upgrade"},
    87  			wantErr: true,
    88  			errMsg:  "must specify an extension to upgrade",
    89  		},
    90  		{
    91  			name: "upgrade an extension",
    92  			args: []string{"upgrade", "hello"},
    93  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
    94  				em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
    95  					return nil
    96  				}
    97  				return func(t *testing.T) {
    98  					calls := em.UpgradeCalls()
    99  					assert.Equal(t, 1, len(calls))
   100  					assert.Equal(t, "hello", calls[0].Name)
   101  				}
   102  			},
   103  		},
   104  		{
   105  			name: "upgrade an extension gh-prefix",
   106  			args: []string{"upgrade", "gh-hello"},
   107  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   108  				em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
   109  					return nil
   110  				}
   111  				return func(t *testing.T) {
   112  					calls := em.UpgradeCalls()
   113  					assert.Equal(t, 1, len(calls))
   114  					assert.Equal(t, "hello", calls[0].Name)
   115  				}
   116  			},
   117  		},
   118  		{
   119  			name: "upgrade an extension full name",
   120  			args: []string{"upgrade", "monalisa/gh-hello"},
   121  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   122  				em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
   123  					return nil
   124  				}
   125  				return func(t *testing.T) {
   126  					calls := em.UpgradeCalls()
   127  					assert.Equal(t, 1, len(calls))
   128  					assert.Equal(t, "hello", calls[0].Name)
   129  				}
   130  			},
   131  		},
   132  		{
   133  			name: "upgrade all",
   134  			args: []string{"upgrade", "--all"},
   135  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   136  				em.UpgradeFunc = func(name string, force bool, out, errOut io.Writer) error {
   137  					return nil
   138  				}
   139  				return func(t *testing.T) {
   140  					calls := em.UpgradeCalls()
   141  					assert.Equal(t, 1, len(calls))
   142  					assert.Equal(t, "", calls[0].Name)
   143  				}
   144  			},
   145  		},
   146  		{
   147  			name: "remove extension tty",
   148  			args: []string{"remove", "hello"},
   149  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   150  				em.RemoveFunc = func(name string) error {
   151  					return nil
   152  				}
   153  				return func(t *testing.T) {
   154  					calls := em.RemoveCalls()
   155  					assert.Equal(t, 1, len(calls))
   156  					assert.Equal(t, "hello", calls[0].Name)
   157  				}
   158  			},
   159  			isTTY:      true,
   160  			wantStdout: "✓ Removed extension hello\n",
   161  		},
   162  		{
   163  			name: "remove extension nontty",
   164  			args: []string{"remove", "hello"},
   165  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   166  				em.RemoveFunc = func(name string) error {
   167  					return nil
   168  				}
   169  				return func(t *testing.T) {
   170  					calls := em.RemoveCalls()
   171  					assert.Equal(t, 1, len(calls))
   172  					assert.Equal(t, "hello", calls[0].Name)
   173  				}
   174  			},
   175  			isTTY:      false,
   176  			wantStdout: "",
   177  		},
   178  		{
   179  			name: "remove extension gh-prefix",
   180  			args: []string{"remove", "gh-hello"},
   181  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   182  				em.RemoveFunc = func(name string) error {
   183  					return nil
   184  				}
   185  				return func(t *testing.T) {
   186  					calls := em.RemoveCalls()
   187  					assert.Equal(t, 1, len(calls))
   188  					assert.Equal(t, "hello", calls[0].Name)
   189  				}
   190  			},
   191  			isTTY:      false,
   192  			wantStdout: "",
   193  		},
   194  		{
   195  			name: "remove extension full name",
   196  			args: []string{"remove", "monalisa/gh-hello"},
   197  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   198  				em.RemoveFunc = func(name string) error {
   199  					return nil
   200  				}
   201  				return func(t *testing.T) {
   202  					calls := em.RemoveCalls()
   203  					assert.Equal(t, 1, len(calls))
   204  					assert.Equal(t, "hello", calls[0].Name)
   205  				}
   206  			},
   207  			isTTY:      false,
   208  			wantStdout: "",
   209  		},
   210  		{
   211  			name: "list extensions",
   212  			args: []string{"list"},
   213  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   214  				em.ListFunc = func(bool) []extensions.Extension {
   215  					ex1 := &Extension{path: "cli/gh-test", url: "https://github.com/cli/gh-test", updateAvailable: false}
   216  					ex2 := &Extension{path: "cli/gh-test2", url: "https://github.com/cli/gh-test2", updateAvailable: true}
   217  					return []extensions.Extension{ex1, ex2}
   218  				}
   219  				return func(t *testing.T) {
   220  					assert.Equal(t, 1, len(em.ListCalls()))
   221  				}
   222  			},
   223  			wantStdout: "gh test\tcli/gh-test\t\ngh test2\tcli/gh-test2\tUpgrade available\n",
   224  		},
   225  		{
   226  			name: "create extension tty",
   227  			args: []string{"create", "test"},
   228  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   229  				em.CreateFunc = func(name string) error {
   230  					return nil
   231  				}
   232  				return func(t *testing.T) {
   233  					calls := em.CreateCalls()
   234  					assert.Equal(t, 1, len(calls))
   235  					assert.Equal(t, "gh-test", calls[0].Name)
   236  				}
   237  			},
   238  			isTTY: true,
   239  			wantStdout: heredoc.Doc(`
   240  				✓ Created directory gh-test
   241  				✓ Initialized git repository
   242  				✓ Set up extension scaffolding
   243  
   244  				gh-test is ready for development
   245  
   246  				Install locally with: cd gh-test && gh extension install .
   247  
   248  				Publish to GitHub with: gh repo create gh-test
   249  
   250  				For more information on writing extensions:
   251  				https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
   252  			`),
   253  		},
   254  		{
   255  			name: "create extension notty",
   256  			args: []string{"create", "gh-test"},
   257  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   258  				em.CreateFunc = func(name string) error {
   259  					return nil
   260  				}
   261  				return func(t *testing.T) {
   262  					calls := em.CreateCalls()
   263  					assert.Equal(t, 1, len(calls))
   264  					assert.Equal(t, "gh-test", calls[0].Name)
   265  				}
   266  			},
   267  			isTTY:      false,
   268  			wantStdout: "",
   269  		},
   270  	}
   271  
   272  	for _, tt := range tests {
   273  		t.Run(tt.name, func(t *testing.T) {
   274  			ios, _, stdout, stderr := iostreams.Test()
   275  			ios.SetStdoutTTY(tt.isTTY)
   276  			ios.SetStderrTTY(tt.isTTY)
   277  
   278  			var assertFunc func(*testing.T)
   279  			em := &extensions.ExtensionManagerMock{}
   280  			if tt.managerStubs != nil {
   281  				assertFunc = tt.managerStubs(em)
   282  			}
   283  
   284  			f := cmdutil.Factory{
   285  				Config: func() (config.Config, error) {
   286  					return config.NewBlankConfig(), nil
   287  				},
   288  				IOStreams:        ios,
   289  				ExtensionManager: em,
   290  			}
   291  
   292  			cmd := NewCmdExtension(&f)
   293  			cmd.SetArgs(tt.args)
   294  			cmd.SetOut(ioutil.Discard)
   295  			cmd.SetErr(ioutil.Discard)
   296  
   297  			_, err := cmd.ExecuteC()
   298  			if tt.wantErr {
   299  				assert.EqualError(t, err, tt.errMsg)
   300  			} else {
   301  				assert.NoError(t, err)
   302  			}
   303  
   304  			if assertFunc != nil {
   305  				assertFunc(t)
   306  			}
   307  
   308  			assert.Equal(t, tt.wantStdout, stdout.String())
   309  			assert.Equal(t, tt.wantStderr, stderr.String())
   310  		})
   311  	}
   312  }
   313  
   314  func normalizeDir(d string) string {
   315  	return strings.TrimPrefix(d, "/private")
   316  }
   317  
   318  func Test_checkValidExtension(t *testing.T) {
   319  	rootCmd := &cobra.Command{}
   320  	rootCmd.AddCommand(&cobra.Command{Use: "help"})
   321  	rootCmd.AddCommand(&cobra.Command{Use: "auth"})
   322  
   323  	m := &extensions.ExtensionManagerMock{
   324  		ListFunc: func(bool) []extensions.Extension {
   325  			return []extensions.Extension{
   326  				&extensions.ExtensionMock{
   327  					NameFunc: func() string { return "screensaver" },
   328  				},
   329  				&extensions.ExtensionMock{
   330  					NameFunc: func() string { return "triage" },
   331  				},
   332  			}
   333  		},
   334  	}
   335  
   336  	type args struct {
   337  		rootCmd *cobra.Command
   338  		manager extensions.ExtensionManager
   339  		extName string
   340  	}
   341  	tests := []struct {
   342  		name      string
   343  		args      args
   344  		wantError string
   345  	}{
   346  		{
   347  			name: "valid extension",
   348  			args: args{
   349  				rootCmd: rootCmd,
   350  				manager: m,
   351  				extName: "gh-hello",
   352  			},
   353  		},
   354  		{
   355  			name: "invalid extension name",
   356  			args: args{
   357  				rootCmd: rootCmd,
   358  				manager: m,
   359  				extName: "gherkins",
   360  			},
   361  			wantError: "extension repository name must start with `gh-`",
   362  		},
   363  		{
   364  			name: "clashes with built-in command",
   365  			args: args{
   366  				rootCmd: rootCmd,
   367  				manager: m,
   368  				extName: "gh-auth",
   369  			},
   370  			wantError: "\"auth\" matches the name of a built-in command",
   371  		},
   372  		{
   373  			name: "clashes with an installed extension",
   374  			args: args{
   375  				rootCmd: rootCmd,
   376  				manager: m,
   377  				extName: "gh-triage",
   378  			},
   379  			wantError: "there is already an installed extension that provides the \"triage\" command",
   380  		},
   381  	}
   382  	for _, tt := range tests {
   383  		t.Run(tt.name, func(t *testing.T) {
   384  			err := checkValidExtension(tt.args.rootCmd, tt.args.manager, tt.args.extName)
   385  			if tt.wantError == "" {
   386  				assert.NoError(t, err)
   387  			} else {
   388  				assert.EqualError(t, err, tt.wantError)
   389  			}
   390  		})
   391  	}
   392  }