github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/extension/command_test.go (about)

     1  package extension
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/MakeNowJust/heredoc"
    14  	"github.com/ungtb10d/cli/v2/internal/browser"
    15  	"github.com/ungtb10d/cli/v2/internal/config"
    16  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    17  	"github.com/ungtb10d/cli/v2/internal/prompter"
    18  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    19  	"github.com/ungtb10d/cli/v2/pkg/extensions"
    20  	"github.com/ungtb10d/cli/v2/pkg/httpmock"
    21  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    22  	"github.com/ungtb10d/cli/v2/pkg/search"
    23  	"github.com/spf13/cobra"
    24  	"github.com/stretchr/testify/assert"
    25  )
    26  
    27  func TestNewCmdExtension(t *testing.T) {
    28  	tempDir := t.TempDir()
    29  	oldWd, _ := os.Getwd()
    30  	assert.NoError(t, os.Chdir(tempDir))
    31  	t.Cleanup(func() { _ = os.Chdir(oldWd) })
    32  
    33  	tests := []struct {
    34  		name          string
    35  		args          []string
    36  		managerStubs  func(em *extensions.ExtensionManagerMock) func(*testing.T)
    37  		prompterStubs func(pm *prompter.PrompterMock)
    38  		httpStubs     func(reg *httpmock.Registry)
    39  		browseStubs   func(*browser.Stub) func(*testing.T)
    40  		isTTY         bool
    41  		wantErr       bool
    42  		errMsg        string
    43  		wantStdout    string
    44  		wantStderr    string
    45  	}{
    46  		{
    47  			name: "search for extensions",
    48  			args: []string{"search"},
    49  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
    50  				em.ListFunc = func() []extensions.Extension {
    51  					return []extensions.Extension{
    52  						&extensions.ExtensionMock{
    53  							URLFunc: func() string {
    54  								return "https://github.com/vilmibm/gh-screensaver"
    55  							},
    56  						},
    57  						&extensions.ExtensionMock{
    58  							URLFunc: func() string {
    59  								return "https://github.com/github/gh-gei"
    60  							},
    61  						},
    62  					}
    63  				}
    64  				return func(t *testing.T) {
    65  					listCalls := em.ListCalls()
    66  					assert.Equal(t, 1, len(listCalls))
    67  				}
    68  			},
    69  			httpStubs: func(reg *httpmock.Registry) {
    70  				values := url.Values{
    71  					"page":     []string{"1"},
    72  					"per_page": []string{"30"},
    73  					"q":        []string{"topic:gh-extension"},
    74  				}
    75  				reg.Register(
    76  					httpmock.QueryMatcher("GET", "search/repositories", values),
    77  					httpmock.JSONResponse(searchResults()),
    78  				)
    79  			},
    80  			isTTY:      true,
    81  			wantStdout: "Showing 4 of 4 extensions\n\n   REPO                    DESCRIPTION\n✓  vilmibm/gh-screensaver  terminal animations\n   cli/gh-cool             it's just cool ok\n   samcoe/gh-triage        helps with triage\n✓  github/gh-gei           something something enterprise\n",
    82  		},
    83  		{
    84  			name: "search for extensions non-tty",
    85  			args: []string{"search"},
    86  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
    87  				em.ListFunc = func() []extensions.Extension {
    88  					return []extensions.Extension{
    89  						&extensions.ExtensionMock{
    90  							URLFunc: func() string {
    91  								return "https://github.com/vilmibm/gh-screensaver"
    92  							},
    93  						},
    94  						&extensions.ExtensionMock{
    95  							URLFunc: func() string {
    96  								return "https://github.com/github/gh-gei"
    97  							},
    98  						},
    99  					}
   100  				}
   101  				return func(t *testing.T) {
   102  					listCalls := em.ListCalls()
   103  					assert.Equal(t, 1, len(listCalls))
   104  				}
   105  			},
   106  			httpStubs: func(reg *httpmock.Registry) {
   107  				values := url.Values{
   108  					"page":     []string{"1"},
   109  					"per_page": []string{"30"},
   110  					"q":        []string{"topic:gh-extension"},
   111  				}
   112  				reg.Register(
   113  					httpmock.QueryMatcher("GET", "search/repositories", values),
   114  					httpmock.JSONResponse(searchResults()),
   115  				)
   116  			},
   117  			wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n\tcli/gh-cool\tit's just cool ok\n\tsamcoe/gh-triage\thelps with triage\ninstalled\tgithub/gh-gei\tsomething something enterprise\n",
   118  		},
   119  		{
   120  			name: "search for extensions with keywords",
   121  			args: []string{"search", "screen"},
   122  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   123  				em.ListFunc = func() []extensions.Extension {
   124  					return []extensions.Extension{
   125  						&extensions.ExtensionMock{
   126  							URLFunc: func() string {
   127  								return "https://github.com/vilmibm/gh-screensaver"
   128  							},
   129  						},
   130  						&extensions.ExtensionMock{
   131  							URLFunc: func() string {
   132  								return "https://github.com/github/gh-gei"
   133  							},
   134  						},
   135  					}
   136  				}
   137  				return func(t *testing.T) {
   138  					listCalls := em.ListCalls()
   139  					assert.Equal(t, 1, len(listCalls))
   140  				}
   141  			},
   142  			httpStubs: func(reg *httpmock.Registry) {
   143  				values := url.Values{
   144  					"page":     []string{"1"},
   145  					"per_page": []string{"30"},
   146  					"q":        []string{"screen topic:gh-extension"},
   147  				}
   148  				results := searchResults()
   149  				results.Total = 1
   150  				results.Items = []search.Repository{results.Items[0]}
   151  				reg.Register(
   152  					httpmock.QueryMatcher("GET", "search/repositories", values),
   153  					httpmock.JSONResponse(results),
   154  				)
   155  			},
   156  			wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n",
   157  		},
   158  		{
   159  			name: "search for extensions with parameter flags",
   160  			args: []string{"search", "--limit", "1", "--order", "asc", "--sort", "stars"},
   161  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   162  				em.ListFunc = func() []extensions.Extension {
   163  					return []extensions.Extension{}
   164  				}
   165  				return func(t *testing.T) {
   166  					listCalls := em.ListCalls()
   167  					assert.Equal(t, 1, len(listCalls))
   168  				}
   169  			},
   170  			httpStubs: func(reg *httpmock.Registry) {
   171  				values := url.Values{
   172  					"page":     []string{"1"},
   173  					"order":    []string{"asc"},
   174  					"sort":     []string{"stars"},
   175  					"per_page": []string{"1"},
   176  					"q":        []string{"topic:gh-extension"},
   177  				}
   178  				results := searchResults()
   179  				results.Total = 1
   180  				results.Items = []search.Repository{results.Items[0]}
   181  				reg.Register(
   182  					httpmock.QueryMatcher("GET", "search/repositories", values),
   183  					httpmock.JSONResponse(results),
   184  				)
   185  			},
   186  			wantStdout: "\tvilmibm/gh-screensaver\tterminal animations\n",
   187  		},
   188  		{
   189  			name: "search for extensions with qualifier flags",
   190  			args: []string{"search", "--license", "GPLv3", "--owner", "jillvalentine"},
   191  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   192  				em.ListFunc = func() []extensions.Extension {
   193  					return []extensions.Extension{}
   194  				}
   195  				return func(t *testing.T) {
   196  					listCalls := em.ListCalls()
   197  					assert.Equal(t, 1, len(listCalls))
   198  				}
   199  			},
   200  			httpStubs: func(reg *httpmock.Registry) {
   201  				values := url.Values{
   202  					"page":     []string{"1"},
   203  					"per_page": []string{"30"},
   204  					"q":        []string{"license:GPLv3 topic:gh-extension user:jillvalentine"},
   205  				}
   206  				results := searchResults()
   207  				results.Total = 1
   208  				results.Items = []search.Repository{results.Items[0]}
   209  				reg.Register(
   210  					httpmock.QueryMatcher("GET", "search/repositories", values),
   211  					httpmock.JSONResponse(results),
   212  				)
   213  			},
   214  			wantStdout: "\tvilmibm/gh-screensaver\tterminal animations\n",
   215  		},
   216  		{
   217  			name: "search for extensions with web mode",
   218  			args: []string{"search", "--web"},
   219  			browseStubs: func(b *browser.Stub) func(*testing.T) {
   220  				return func(t *testing.T) {
   221  					b.Verify(t, "https://github.com/search?q=topic%3Agh-extension&type=repositories")
   222  				}
   223  			},
   224  		},
   225  		{
   226  			name: "install an extension",
   227  			args: []string{"install", "owner/gh-some-ext"},
   228  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   229  				em.ListFunc = func() []extensions.Extension {
   230  					return []extensions.Extension{}
   231  				}
   232  				em.InstallFunc = func(_ ghrepo.Interface, _ string) error {
   233  					return nil
   234  				}
   235  				return func(t *testing.T) {
   236  					installCalls := em.InstallCalls()
   237  					assert.Equal(t, 1, len(installCalls))
   238  					assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName())
   239  					listCalls := em.ListCalls()
   240  					assert.Equal(t, 1, len(listCalls))
   241  				}
   242  			},
   243  		},
   244  		{
   245  			name: "install an extension with same name as existing extension",
   246  			args: []string{"install", "owner/gh-existing-ext"},
   247  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   248  				em.ListFunc = func() []extensions.Extension {
   249  					e := &Extension{path: "owner2/gh-existing-ext"}
   250  					return []extensions.Extension{e}
   251  				}
   252  				return func(t *testing.T) {
   253  					calls := em.ListCalls()
   254  					assert.Equal(t, 1, len(calls))
   255  				}
   256  			},
   257  			wantErr: true,
   258  			errMsg:  "there is already an installed extension that provides the \"existing-ext\" command",
   259  		},
   260  		{
   261  			name: "install local extension",
   262  			args: []string{"install", "."},
   263  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   264  				em.InstallLocalFunc = func(dir string) error {
   265  					return nil
   266  				}
   267  				return func(t *testing.T) {
   268  					calls := em.InstallLocalCalls()
   269  					assert.Equal(t, 1, len(calls))
   270  					assert.Equal(t, tempDir, normalizeDir(calls[0].Dir))
   271  				}
   272  			},
   273  		},
   274  		{
   275  			name: "error extension not found",
   276  			args: []string{"install", "owner/gh-some-ext"},
   277  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   278  				em.ListFunc = func() []extensions.Extension {
   279  					return []extensions.Extension{}
   280  				}
   281  				em.InstallFunc = func(_ ghrepo.Interface, _ string) error {
   282  					return repositoryNotFoundErr
   283  				}
   284  				return func(t *testing.T) {
   285  					installCalls := em.InstallCalls()
   286  					assert.Equal(t, 1, len(installCalls))
   287  					assert.Equal(t, "gh-some-ext", installCalls[0].InterfaceMoqParam.RepoName())
   288  				}
   289  			},
   290  			wantErr: true,
   291  			errMsg:  "X Could not find extension 'owner/gh-some-ext' on host github.com",
   292  		},
   293  		{
   294  			name:    "install local extension with pin",
   295  			args:    []string{"install", ".", "--pin", "v1.0.0"},
   296  			wantErr: true,
   297  			errMsg:  "local extensions cannot be pinned",
   298  			isTTY:   true,
   299  		},
   300  		{
   301  			name:    "upgrade argument error",
   302  			args:    []string{"upgrade"},
   303  			wantErr: true,
   304  			errMsg:  "specify an extension to upgrade or `--all`",
   305  		},
   306  		{
   307  			name:    "upgrade --all with extension name error",
   308  			args:    []string{"upgrade", "test", "--all"},
   309  			wantErr: true,
   310  			errMsg:  "cannot use `--all` with extension name",
   311  		},
   312  		{
   313  			name: "upgrade an extension",
   314  			args: []string{"upgrade", "hello"},
   315  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   316  				em.UpgradeFunc = func(name string, force bool) error {
   317  					return nil
   318  				}
   319  				return func(t *testing.T) {
   320  					calls := em.UpgradeCalls()
   321  					assert.Equal(t, 1, len(calls))
   322  					assert.Equal(t, "hello", calls[0].Name)
   323  				}
   324  			},
   325  			isTTY:      true,
   326  			wantStdout: "✓ Successfully upgraded extension hello\n",
   327  		},
   328  		{
   329  			name: "upgrade an extension dry run",
   330  			args: []string{"upgrade", "hello", "--dry-run"},
   331  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   332  				em.EnableDryRunModeFunc = func() {}
   333  				em.UpgradeFunc = func(name string, force bool) error {
   334  					return nil
   335  				}
   336  				return func(t *testing.T) {
   337  					dryRunCalls := em.EnableDryRunModeCalls()
   338  					assert.Equal(t, 1, len(dryRunCalls))
   339  					upgradeCalls := em.UpgradeCalls()
   340  					assert.Equal(t, 1, len(upgradeCalls))
   341  					assert.Equal(t, "hello", upgradeCalls[0].Name)
   342  					assert.False(t, upgradeCalls[0].Force)
   343  				}
   344  			},
   345  			isTTY:      true,
   346  			wantStdout: "✓ Would have upgraded extension hello\n",
   347  		},
   348  		{
   349  			name: "upgrade an extension notty",
   350  			args: []string{"upgrade", "hello"},
   351  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   352  				em.UpgradeFunc = func(name string, force bool) error {
   353  					return nil
   354  				}
   355  				return func(t *testing.T) {
   356  					calls := em.UpgradeCalls()
   357  					assert.Equal(t, 1, len(calls))
   358  					assert.Equal(t, "hello", calls[0].Name)
   359  				}
   360  			},
   361  			isTTY: false,
   362  		},
   363  		{
   364  			name: "upgrade an up-to-date extension",
   365  			args: []string{"upgrade", "hello"},
   366  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   367  				em.UpgradeFunc = func(name string, force bool) error {
   368  					return upToDateError
   369  				}
   370  				return func(t *testing.T) {
   371  					calls := em.UpgradeCalls()
   372  					assert.Equal(t, 1, len(calls))
   373  					assert.Equal(t, "hello", calls[0].Name)
   374  				}
   375  			},
   376  			isTTY:      true,
   377  			wantStdout: "✓ Extension already up to date\n",
   378  			wantStderr: "",
   379  		},
   380  		{
   381  			name: "upgrade extension error",
   382  			args: []string{"upgrade", "hello"},
   383  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   384  				em.UpgradeFunc = func(name string, force bool) error {
   385  					return errors.New("oh no")
   386  				}
   387  				return func(t *testing.T) {
   388  					calls := em.UpgradeCalls()
   389  					assert.Equal(t, 1, len(calls))
   390  					assert.Equal(t, "hello", calls[0].Name)
   391  				}
   392  			},
   393  			isTTY:      false,
   394  			wantErr:    true,
   395  			errMsg:     "SilentError",
   396  			wantStdout: "",
   397  			wantStderr: "X Failed upgrading extension hello: oh no\n",
   398  		},
   399  		{
   400  			name: "upgrade an extension gh-prefix",
   401  			args: []string{"upgrade", "gh-hello"},
   402  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   403  				em.UpgradeFunc = func(name string, force bool) error {
   404  					return nil
   405  				}
   406  				return func(t *testing.T) {
   407  					calls := em.UpgradeCalls()
   408  					assert.Equal(t, 1, len(calls))
   409  					assert.Equal(t, "hello", calls[0].Name)
   410  				}
   411  			},
   412  			isTTY:      true,
   413  			wantStdout: "✓ Successfully upgraded extension hello\n",
   414  		},
   415  		{
   416  			name: "upgrade an extension full name",
   417  			args: []string{"upgrade", "monalisa/gh-hello"},
   418  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   419  				em.UpgradeFunc = func(name string, force bool) error {
   420  					return nil
   421  				}
   422  				return func(t *testing.T) {
   423  					calls := em.UpgradeCalls()
   424  					assert.Equal(t, 1, len(calls))
   425  					assert.Equal(t, "hello", calls[0].Name)
   426  				}
   427  			},
   428  			isTTY:      true,
   429  			wantStdout: "✓ Successfully upgraded extension hello\n",
   430  		},
   431  		{
   432  			name: "upgrade all",
   433  			args: []string{"upgrade", "--all"},
   434  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   435  				em.UpgradeFunc = func(name string, force bool) error {
   436  					return nil
   437  				}
   438  				return func(t *testing.T) {
   439  					calls := em.UpgradeCalls()
   440  					assert.Equal(t, 1, len(calls))
   441  					assert.Equal(t, "", calls[0].Name)
   442  				}
   443  			},
   444  			isTTY:      true,
   445  			wantStdout: "✓ Successfully upgraded extensions\n",
   446  		},
   447  		{
   448  			name: "upgrade all dry run",
   449  			args: []string{"upgrade", "--all", "--dry-run"},
   450  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   451  				em.EnableDryRunModeFunc = func() {}
   452  				em.UpgradeFunc = func(name string, force bool) error {
   453  					return nil
   454  				}
   455  				return func(t *testing.T) {
   456  					dryRunCalls := em.EnableDryRunModeCalls()
   457  					assert.Equal(t, 1, len(dryRunCalls))
   458  					upgradeCalls := em.UpgradeCalls()
   459  					assert.Equal(t, 1, len(upgradeCalls))
   460  					assert.Equal(t, "", upgradeCalls[0].Name)
   461  					assert.False(t, upgradeCalls[0].Force)
   462  				}
   463  			},
   464  			isTTY:      true,
   465  			wantStdout: "✓ Would have upgraded extensions\n",
   466  		},
   467  		{
   468  			name: "upgrade all none installed",
   469  			args: []string{"upgrade", "--all"},
   470  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   471  				em.UpgradeFunc = func(name string, force bool) error {
   472  					return noExtensionsInstalledError
   473  				}
   474  				return func(t *testing.T) {
   475  					calls := em.UpgradeCalls()
   476  					assert.Equal(t, 1, len(calls))
   477  					assert.Equal(t, "", calls[0].Name)
   478  				}
   479  			},
   480  			isTTY:   true,
   481  			wantErr: true,
   482  			errMsg:  "no installed extensions found",
   483  		},
   484  		{
   485  			name: "upgrade all notty",
   486  			args: []string{"upgrade", "--all"},
   487  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   488  				em.UpgradeFunc = func(name string, force bool) error {
   489  					return nil
   490  				}
   491  				return func(t *testing.T) {
   492  					calls := em.UpgradeCalls()
   493  					assert.Equal(t, 1, len(calls))
   494  					assert.Equal(t, "", calls[0].Name)
   495  				}
   496  			},
   497  			isTTY: false,
   498  		},
   499  		{
   500  			name: "remove extension tty",
   501  			args: []string{"remove", "hello"},
   502  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   503  				em.RemoveFunc = func(name string) error {
   504  					return nil
   505  				}
   506  				return func(t *testing.T) {
   507  					calls := em.RemoveCalls()
   508  					assert.Equal(t, 1, len(calls))
   509  					assert.Equal(t, "hello", calls[0].Name)
   510  				}
   511  			},
   512  			isTTY:      true,
   513  			wantStdout: "✓ Removed extension hello\n",
   514  		},
   515  		{
   516  			name: "remove extension nontty",
   517  			args: []string{"remove", "hello"},
   518  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   519  				em.RemoveFunc = func(name string) error {
   520  					return nil
   521  				}
   522  				return func(t *testing.T) {
   523  					calls := em.RemoveCalls()
   524  					assert.Equal(t, 1, len(calls))
   525  					assert.Equal(t, "hello", calls[0].Name)
   526  				}
   527  			},
   528  			isTTY:      false,
   529  			wantStdout: "",
   530  		},
   531  		{
   532  			name: "remove extension gh-prefix",
   533  			args: []string{"remove", "gh-hello"},
   534  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   535  				em.RemoveFunc = func(name string) error {
   536  					return nil
   537  				}
   538  				return func(t *testing.T) {
   539  					calls := em.RemoveCalls()
   540  					assert.Equal(t, 1, len(calls))
   541  					assert.Equal(t, "hello", calls[0].Name)
   542  				}
   543  			},
   544  			isTTY:      false,
   545  			wantStdout: "",
   546  		},
   547  		{
   548  			name: "remove extension full name",
   549  			args: []string{"remove", "monalisa/gh-hello"},
   550  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   551  				em.RemoveFunc = func(name string) error {
   552  					return nil
   553  				}
   554  				return func(t *testing.T) {
   555  					calls := em.RemoveCalls()
   556  					assert.Equal(t, 1, len(calls))
   557  					assert.Equal(t, "hello", calls[0].Name)
   558  				}
   559  			},
   560  			isTTY:      false,
   561  			wantStdout: "",
   562  		},
   563  		{
   564  			name: "list extensions",
   565  			args: []string{"list"},
   566  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   567  				em.ListFunc = func() []extensions.Extension {
   568  					ex1 := &Extension{path: "cli/gh-test", url: "https://github.com/cli/gh-test", currentVersion: "1"}
   569  					ex2 := &Extension{path: "cli/gh-test2", url: "https://github.com/cli/gh-test2", currentVersion: "1"}
   570  					return []extensions.Extension{ex1, ex2}
   571  				}
   572  				return func(t *testing.T) {
   573  					calls := em.ListCalls()
   574  					assert.Equal(t, 1, len(calls))
   575  				}
   576  			},
   577  			wantStdout: "gh test\tcli/gh-test\t1\ngh test2\tcli/gh-test2\t1\n",
   578  		},
   579  		{
   580  			name: "create extension interactive",
   581  			args: []string{"create"},
   582  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   583  				em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
   584  					return nil
   585  				}
   586  				return func(t *testing.T) {
   587  					calls := em.CreateCalls()
   588  					assert.Equal(t, 1, len(calls))
   589  					assert.Equal(t, "gh-test", calls[0].Name)
   590  				}
   591  			},
   592  			isTTY: true,
   593  			prompterStubs: func(pm *prompter.PrompterMock) {
   594  				pm.InputFunc = func(prompt, defVal string) (string, error) {
   595  					if prompt == "Extension name:" {
   596  						return "test", nil
   597  					}
   598  					return "", nil
   599  				}
   600  				pm.SelectFunc = func(prompt, defVal string, opts []string) (int, error) {
   601  					return prompter.IndexFor(opts, "Script (Bash, Ruby, Python, etc)")
   602  				}
   603  			},
   604  			wantStdout: heredoc.Doc(`
   605  				✓ Created directory gh-test
   606  				✓ Initialized git repository
   607  				✓ Set up extension scaffolding
   608  
   609  				gh-test is ready for development!
   610  
   611  				Next Steps
   612  				- run 'cd gh-test; gh extension install .; gh test' to see your new extension in action
   613  				- commit and use 'gh repo create' to share your extension with others
   614  
   615  				For more information on writing extensions:
   616  				https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
   617  			`),
   618  		},
   619  		{
   620  			name: "create extension with arg, --precompiled=go",
   621  			args: []string{"create", "test", "--precompiled", "go"},
   622  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   623  				em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
   624  					return nil
   625  				}
   626  				return func(t *testing.T) {
   627  					calls := em.CreateCalls()
   628  					assert.Equal(t, 1, len(calls))
   629  					assert.Equal(t, "gh-test", calls[0].Name)
   630  				}
   631  			},
   632  			isTTY: true,
   633  			wantStdout: heredoc.Doc(`
   634  				✓ Created directory gh-test
   635  				✓ Initialized git repository
   636  				✓ Set up extension scaffolding
   637  				✓ Downloaded Go dependencies
   638  				✓ Built gh-test binary
   639  
   640  				gh-test is ready for development!
   641  
   642  				Next Steps
   643  				- run 'cd gh-test; gh extension install .; gh test' to see your new extension in action
   644  				- use 'go build && gh test' to see changes in your code as you develop
   645  				- commit and use 'gh repo create' to share your extension with others
   646  
   647  				For more information on writing extensions:
   648  				https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
   649  			`),
   650  		},
   651  		{
   652  			name: "create extension with arg, --precompiled=other",
   653  			args: []string{"create", "test", "--precompiled", "other"},
   654  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   655  				em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
   656  					return nil
   657  				}
   658  				return func(t *testing.T) {
   659  					calls := em.CreateCalls()
   660  					assert.Equal(t, 1, len(calls))
   661  					assert.Equal(t, "gh-test", calls[0].Name)
   662  				}
   663  			},
   664  			isTTY: true,
   665  			wantStdout: heredoc.Doc(`
   666  				✓ Created directory gh-test
   667  				✓ Initialized git repository
   668  				✓ Set up extension scaffolding
   669  
   670  				gh-test is ready for development!
   671  
   672  				Next Steps
   673  				- run 'cd gh-test; gh extension install .' to install your extension locally
   674  				- fill in script/build.sh with your compilation script for automated builds
   675  				- compile a gh-test binary locally and run 'gh test' to see changes
   676  				- commit and use 'gh repo create' to share your extension with others
   677  
   678  				For more information on writing extensions:
   679  				https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
   680  			`),
   681  		},
   682  		{
   683  			name: "create extension tty with argument",
   684  			args: []string{"create", "test"},
   685  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   686  				em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
   687  					return nil
   688  				}
   689  				return func(t *testing.T) {
   690  					calls := em.CreateCalls()
   691  					assert.Equal(t, 1, len(calls))
   692  					assert.Equal(t, "gh-test", calls[0].Name)
   693  				}
   694  			},
   695  			isTTY: true,
   696  			wantStdout: heredoc.Doc(`
   697  				✓ Created directory gh-test
   698  				✓ Initialized git repository
   699  				✓ Set up extension scaffolding
   700  
   701  				gh-test is ready for development!
   702  
   703  				Next Steps
   704  				- run 'cd gh-test; gh extension install .; gh test' to see your new extension in action
   705  				- commit and use 'gh repo create' to share your extension with others
   706  
   707  				For more information on writing extensions:
   708  				https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions
   709  			`),
   710  		},
   711  		{
   712  			name: "create extension notty",
   713  			args: []string{"create", "gh-test"},
   714  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   715  				em.CreateFunc = func(name string, tmplType extensions.ExtTemplateType) error {
   716  					return nil
   717  				}
   718  				return func(t *testing.T) {
   719  					calls := em.CreateCalls()
   720  					assert.Equal(t, 1, len(calls))
   721  					assert.Equal(t, "gh-test", calls[0].Name)
   722  				}
   723  			},
   724  			isTTY:      false,
   725  			wantStdout: "",
   726  		},
   727  		{
   728  			name: "exec extension missing",
   729  			args: []string{"exec", "invalid"},
   730  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   731  				em.DispatchFunc = func(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) {
   732  					return false, nil
   733  				}
   734  				return func(t *testing.T) {
   735  					calls := em.DispatchCalls()
   736  					assert.Equal(t, 1, len(calls))
   737  					assert.EqualValues(t, []string{"invalid"}, calls[0].Args)
   738  				}
   739  			},
   740  			wantErr: true,
   741  			errMsg:  `extension "invalid" not found`,
   742  		},
   743  		{
   744  			name: "exec extension with arguments",
   745  			args: []string{"exec", "test", "arg1", "arg2", "--flag1"},
   746  			managerStubs: func(em *extensions.ExtensionManagerMock) func(*testing.T) {
   747  				em.DispatchFunc = func(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) {
   748  					fmt.Fprintf(stdout, "test output")
   749  					return true, nil
   750  				}
   751  				return func(t *testing.T) {
   752  					calls := em.DispatchCalls()
   753  					assert.Equal(t, 1, len(calls))
   754  					assert.EqualValues(t, []string{"test", "arg1", "arg2", "--flag1"}, calls[0].Args)
   755  				}
   756  			},
   757  			wantStdout: "test output",
   758  		},
   759  		{
   760  			name:    "browse",
   761  			args:    []string{"browse"},
   762  			wantErr: true,
   763  			errMsg:  "this command runs an interactive UI and needs to be run in a terminal",
   764  		},
   765  	}
   766  
   767  	for _, tt := range tests {
   768  		t.Run(tt.name, func(t *testing.T) {
   769  			ios, _, stdout, stderr := iostreams.Test()
   770  			ios.SetStdoutTTY(tt.isTTY)
   771  			ios.SetStderrTTY(tt.isTTY)
   772  
   773  			var assertFunc func(*testing.T)
   774  			em := &extensions.ExtensionManagerMock{}
   775  			if tt.managerStubs != nil {
   776  				assertFunc = tt.managerStubs(em)
   777  			}
   778  
   779  			pm := &prompter.PrompterMock{}
   780  			if tt.prompterStubs != nil {
   781  				tt.prompterStubs(pm)
   782  			}
   783  
   784  			reg := httpmock.Registry{}
   785  			defer reg.Verify(t)
   786  			client := http.Client{Transport: &reg}
   787  
   788  			if tt.httpStubs != nil {
   789  				tt.httpStubs(&reg)
   790  			}
   791  
   792  			var assertBrowserFunc func(*testing.T)
   793  			browseStub := &browser.Stub{}
   794  			if tt.browseStubs != nil {
   795  				assertBrowserFunc = tt.browseStubs(browseStub)
   796  			}
   797  
   798  			f := cmdutil.Factory{
   799  				Config: func() (config.Config, error) {
   800  					return config.NewBlankConfig(), nil
   801  				},
   802  				IOStreams:        ios,
   803  				ExtensionManager: em,
   804  				Prompter:         pm,
   805  				Browser:          browseStub,
   806  				HttpClient: func() (*http.Client, error) {
   807  					return &client, nil
   808  				},
   809  			}
   810  
   811  			cmd := NewCmdExtension(&f)
   812  			cmd.SetArgs(tt.args)
   813  			cmd.SetOut(io.Discard)
   814  			cmd.SetErr(io.Discard)
   815  
   816  			_, err := cmd.ExecuteC()
   817  			if tt.wantErr {
   818  				assert.EqualError(t, err, tt.errMsg)
   819  			} else {
   820  				assert.NoError(t, err)
   821  			}
   822  
   823  			if assertFunc != nil {
   824  				assertFunc(t)
   825  			}
   826  
   827  			if assertBrowserFunc != nil {
   828  				assertBrowserFunc(t)
   829  			}
   830  
   831  			assert.Equal(t, tt.wantStdout, stdout.String())
   832  			assert.Equal(t, tt.wantStderr, stderr.String())
   833  		})
   834  	}
   835  }
   836  
   837  func normalizeDir(d string) string {
   838  	return strings.TrimPrefix(d, "/private")
   839  }
   840  
   841  func Test_checkValidExtension(t *testing.T) {
   842  	rootCmd := &cobra.Command{}
   843  	rootCmd.AddCommand(&cobra.Command{Use: "help"})
   844  	rootCmd.AddCommand(&cobra.Command{Use: "auth"})
   845  
   846  	m := &extensions.ExtensionManagerMock{
   847  		ListFunc: func() []extensions.Extension {
   848  			return []extensions.Extension{
   849  				&extensions.ExtensionMock{
   850  					NameFunc: func() string { return "screensaver" },
   851  				},
   852  				&extensions.ExtensionMock{
   853  					NameFunc: func() string { return "triage" },
   854  				},
   855  			}
   856  		},
   857  	}
   858  
   859  	type args struct {
   860  		rootCmd *cobra.Command
   861  		manager extensions.ExtensionManager
   862  		extName string
   863  	}
   864  	tests := []struct {
   865  		name      string
   866  		args      args
   867  		wantError string
   868  	}{
   869  		{
   870  			name: "valid extension",
   871  			args: args{
   872  				rootCmd: rootCmd,
   873  				manager: m,
   874  				extName: "gh-hello",
   875  			},
   876  		},
   877  		{
   878  			name: "invalid extension name",
   879  			args: args{
   880  				rootCmd: rootCmd,
   881  				manager: m,
   882  				extName: "gherkins",
   883  			},
   884  			wantError: "extension repository name must start with `gh-`",
   885  		},
   886  		{
   887  			name: "clashes with built-in command",
   888  			args: args{
   889  				rootCmd: rootCmd,
   890  				manager: m,
   891  				extName: "gh-auth",
   892  			},
   893  			wantError: "\"auth\" matches the name of a built-in command",
   894  		},
   895  		{
   896  			name: "clashes with an installed extension",
   897  			args: args{
   898  				rootCmd: rootCmd,
   899  				manager: m,
   900  				extName: "gh-triage",
   901  			},
   902  			wantError: "there is already an installed extension that provides the \"triage\" command",
   903  		},
   904  	}
   905  	for _, tt := range tests {
   906  		t.Run(tt.name, func(t *testing.T) {
   907  			err := checkValidExtension(tt.args.rootCmd, tt.args.manager, tt.args.extName)
   908  			if tt.wantError == "" {
   909  				assert.NoError(t, err)
   910  			} else {
   911  				assert.EqualError(t, err, tt.wantError)
   912  			}
   913  		})
   914  	}
   915  }
   916  
   917  func searchResults() search.RepositoriesResult {
   918  	return search.RepositoriesResult{
   919  		IncompleteResults: false,
   920  		Items: []search.Repository{
   921  			{
   922  				FullName:    "vilmibm/gh-screensaver",
   923  				Name:        "gh-screensaver",
   924  				Description: "terminal animations",
   925  				Owner: search.User{
   926  					Login: "vilmibm",
   927  				},
   928  			},
   929  			{
   930  				FullName:    "cli/gh-cool",
   931  				Name:        "gh-cool",
   932  				Description: "it's just cool ok",
   933  				Owner: search.User{
   934  					Login: "cli",
   935  				},
   936  			},
   937  			{
   938  				FullName:    "samcoe/gh-triage",
   939  				Name:        "gh-triage",
   940  				Description: "helps with triage",
   941  				Owner: search.User{
   942  					Login: "samcoe",
   943  				},
   944  			},
   945  			{
   946  				FullName:    "github/gh-gei",
   947  				Name:        "gh-gei",
   948  				Description: "something something enterprise",
   949  				Owner: search.User{
   950  					Login: "github",
   951  				},
   952  			},
   953  		},
   954  		Total: 4,
   955  	}
   956  }