From e26a1b98a1ad832bdd97cbba90a0e4e98ad09892 Mon Sep 17 00:00:00 2001 From: edualb Date: Fri, 18 Sep 2020 18:27:27 -0300 Subject: [PATCH] add ssh-key command --- api/client.go | 11 +- internal/authflow/flow.go | 2 +- pkg/cmd/auth/login/login_test.go | 16 +- pkg/cmd/auth/status/status_test.go | 20 +- pkg/cmd/root/root.go | 2 + pkg/cmd/ssh-key/list/list.go | 152 +++++++++++++++ pkg/cmd/ssh-key/list/list_test.go | 289 +++++++++++++++++++++++++++++ pkg/cmd/ssh-key/ssh-key.go | 20 ++ 8 files changed, 490 insertions(+), 22 deletions(-) create mode 100644 pkg/cmd/ssh-key/list/list.go create mode 100644 pkg/cmd/ssh-key/list/list_test.go create mode 100644 pkg/cmd/ssh-key/ssh-key.go diff --git a/api/client.go b/api/client.go index 09195181b..3470159f2 100644 --- a/api/client.go +++ b/api/client.go @@ -203,9 +203,10 @@ func (c Client) HasMinimumScopes(hostname string) error { } search := map[string]bool{ - "repo": false, - "read:org": false, - "admin:org": false, + "repo": false, + "read:org": false, + "admin:org": false, + "read:public_key": false, } for _, s := range strings.Split(scopesHeader, ",") { search[strings.TrimSpace(s)] = true @@ -220,6 +221,10 @@ func (c Client) HasMinimumScopes(hostname string) error { missingScopes = append(missingScopes, "read:org") } + if !search["read:public_key"] && !search["admin:public_key"] { + missingScopes = append(missingScopes, "read:public_key") + } + if len(missingScopes) > 0 { return &MissingScopesError{MissingScopes: missingScopes} } diff --git a/internal/authflow/flow.go b/internal/authflow/flow.go index fac9d31a6..b3a9bce99 100644 --- a/internal/authflow/flow.go +++ b/internal/authflow/flow.go @@ -65,7 +65,7 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition httpClient.Transport = api.VerboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport) } - minimumScopes := []string{"repo", "read:org", "gist", "workflow"} + minimumScopes := []string{"repo", "read:org", "gist", "workflow", "read:public_key"} scopes := append(minimumScopes, additionalScopes...) callbackURI := "http://127.0.0.1/callback" diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index 2a61efdfa..1b7207333 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -210,7 +210,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc123", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) }, wantHosts: "albert.wesker:\n oauth_token: abc123\n", }, @@ -221,7 +221,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc456", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org,read:public_key")) }, wantErr: `could not validate token: missing required scope 'repo'`, }, @@ -243,7 +243,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc456", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org,read:public_key")) }, wantHosts: "github.com:\n oauth_token: abc456\n", }, @@ -274,7 +274,7 @@ func Test_loginRun_nontty(t *testing.T) { if tt.httpStubs != nil { tt.httpStubs(reg) } else { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) } mainBuf := bytes.Buffer{} @@ -315,7 +315,7 @@ func Test_loginRun_Survey(t *testing.T) { _ = cfg.Set("github.com", "oauth_token", "ghi789") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -341,7 +341,7 @@ func Test_loginRun_Survey(t *testing.T) { as.StubOne(false) // cache credentials }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -363,7 +363,7 @@ func Test_loginRun_Survey(t *testing.T) { as.StubOne(false) // cache credentials }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -436,7 +436,7 @@ func Test_loginRun_Survey(t *testing.T) { if tt.httpStubs != nil { tt.httpStubs(reg) } else { - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index b16e4df10..e2ffef4fe 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -91,7 +91,7 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "abc123") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -106,8 +106,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "abc123") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -124,7 +124,7 @@ func Test_statusRun(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -140,8 +140,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "abc123") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -159,8 +159,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "xyz456") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) @@ -180,8 +180,8 @@ func Test_statusRun(t *testing.T) { _ = c.Set("github.com", "oauth_token", "xyz456") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) - reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index d5e509665..ba05f902c 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -20,6 +20,7 @@ import ( repoCmd "github.com/cli/cli/pkg/cmd/repo" creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits" secretCmd "github.com/cli/cli/pkg/cmd/secret" + sshKeyCmd "github.com/cli/cli/pkg/cmd/ssh-key" versionCmd "github.com/cli/cli/pkg/cmd/version" "github.com/cli/cli/pkg/cmdutil" "github.com/spf13/cobra" @@ -76,6 +77,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(gistCmd.NewCmdGist(f)) cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams)) cmd.AddCommand(secretCmd.NewCmdSecret(f)) + cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f)) // the `api` command should not inherit any extra HTTP headers bareHTTPCmdFactory := *f diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go new file mode 100644 index 000000000..bd7907f9b --- /dev/null +++ b/pkg/cmd/ssh-key/list/list.go @@ -0,0 +1,152 @@ +package list + +import ( + "bytes" + "errors" + "fmt" + "net/http" + + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/utils" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/api" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/spf13/cobra" +) + +// ListOptions struct for list command +type ListOptions struct { + HTTPClient func() (*http.Client, error) + IO *iostreams.IOStreams + Config func() (config.Config, error) + + ListMsg []string +} + +// NewCmdList creates a command for list all SSH Keys +func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { + opts := &ListOptions{ + HTTPClient: f.HttpClient, + IO: f.IOStreams, + Config: f.Config, + + ListMsg: []string{}, + } + + cmd := &cobra.Command{ + Use: "list", + Args: cobra.ExactArgs(0), + Short: "Lists currently added ssh keys", + Long: heredoc.Doc(`Lists currently added ssh keys. + + This interactive command lists all SSH keys associated with your account + `), + Example: heredoc.Doc(` + $ gh ssh-key list + # => lists all ssh keys associated with your account + `), + RunE: func(cmd *cobra.Command, args []string) error { + if runF != nil { + return runF(opts) + } + return listRun(opts) + }, + } + + return cmd +} + +func listRun(opts *ListOptions) error { + apiClient, err := opts.getAPIClient() + if err != nil { + opts.printTerminal() + return err + } + + err = opts.hasMinimumScopes(apiClient) + if err != nil { + opts.printTerminal() + return err + } + + type keys struct { + Title string + Key string + } + + type result []keys + + rs := result{} + body := bytes.NewBufferString("") + + err = apiClient.REST(ghinstance.Default(), "GET", "user/keys", body, &rs) + if err != nil { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: Got %s", utils.RedX(), err)) + opts.printTerminal() + return err + } + + for _, r := range rs { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s %s: %s \n %s: %s", utils.Cyan("✹"), utils.Bold("Name"), r.Title, utils.Bold("SSH-KEY"), r.Key)) + } + + opts.printTerminal() + + return nil +} + +func (opts *ListOptions) getAPIClient() (*api.Client, error) { + httpClient, err := opts.HTTPClient() + if err != nil { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: %s", utils.RedX(), err)) + return nil, err + } + return api.NewClientFromHTTP(httpClient), nil +} + +func (opts *ListOptions) hasMinimumScopes(apiClient *api.Client) error { + cfg, err := opts.Config() + if err != nil { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: %s", utils.RedX(), err)) + return err + } + + hostname := ghinstance.Default() + + _, tokenSource, _ := cfg.GetWithSource(hostname, "oauth_token") + + // TODO: Implement tests for this case when CheckWriteable function checks filesystem permissions + tokenIsWriteable := cfg.CheckWriteable(hostname, "oauth_token") == nil + + err = apiClient.HasMinimumScopes(hostname) + + if err != nil { + var missingScopes *api.MissingScopesError + if errors.As(err, &missingScopes) { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: %s", utils.RedX(), err)) + if tokenIsWriteable { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- To request missing scopes, run: %s %s", utils.Bold("gh auth refresh -h"), hostname)) + } + } else { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: authentication failed", utils.RedX())) + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- The %s token in %s is no longer valid.", utils.Bold(hostname), utils.Bold(tokenSource))) + if tokenIsWriteable { + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- To re-authenticate, run: %s %s", utils.Bold("gh auth login -h"), utils.Bold(hostname))) + opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- To forget about this host, run: %s %s", utils.Bold("gh auth logout -h"), utils.Bold(hostname))) + } + } + return err + } + + return nil +} + +func (opts *ListOptions) printTerminal() { + stderr := opts.IO.ErrOut + for _, line := range opts.ListMsg { + fmt.Fprintf(stderr, " %s\n", line) + } +} diff --git a/pkg/cmd/ssh-key/list/list_test.go b/pkg/cmd/ssh-key/list/list_test.go new file mode 100644 index 000000000..f92557107 --- /dev/null +++ b/pkg/cmd/ssh-key/list/list_test.go @@ -0,0 +1,289 @@ +package list + +import ( + "bytes" + "errors" + "net/http" + "reflect" + "testing" + + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" + "github.com/google/shlex" +) + +func TestCmdList(t *testing.T) { + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(true) + io.SetStdinTTY(true) + io.SetStderrTTY(true) + + httpFunc := func() (*http.Client, error) { return nil, nil } + configFunc := func() (config.Config, error) { return nil, nil } + + type input struct { + cli string + httpClient func() (*http.Client, error) + io *iostreams.IOStreams + config func() (config.Config, error) + } + + tests := []struct { + name string + input input + wants ListOptions + }{ + { + name: "no arguments", + input: input{ + cli: "", + httpClient: httpFunc, + io: io, + config: configFunc, + }, + wants: ListOptions{ + HTTPClient: httpFunc, + Config: configFunc, + IO: io, + ListMsg: []string{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &cmdutil.Factory{ + HttpClient: tt.input.httpClient, + Config: tt.input.config, + IOStreams: tt.input.io, + } + + argv, err := shlex.Split(tt.input.cli) + if err != nil { + t.Errorf(`Split() = got %v`, err) + } + + var gotOpts *ListOptions + cmd := NewCmdList(f, func(opts *ListOptions) error { + gotOpts = opts + return nil + }) + + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + if err != nil { + t.Errorf(`ExecuteC() = got %v`, err) + } + + if reflect.ValueOf(tt.wants.HTTPClient).Pointer() != reflect.ValueOf(gotOpts.HTTPClient).Pointer() { + t.Errorf(`HTTPClient has wrong values`) + } + + if reflect.ValueOf(tt.wants.Config).Pointer() != reflect.ValueOf(gotOpts.Config).Pointer() { + t.Errorf(`Config has wrong values`) + } + + if reflect.ValueOf(tt.wants.IO).Pointer() != reflect.ValueOf(gotOpts.IO).Pointer() { + t.Errorf(`IO has wrong values`) + } + + if !reflect.DeepEqual(tt.wants.ListMsg, gotOpts.ListMsg) { + t.Errorf(`ListMsg has wrong values: want %v, got %v`, tt.wants.ListMsg, gotOpts.ListMsg) + } + }) + } +} + +func TestListRun(t *testing.T) { + type input struct { + httpStubs func(*httpmock.Registry) + configError bool + httpClientError bool + hasOauthToken bool + wantErr bool + } + + tests := []struct { + name string + input input + want []string + }{ + { + name: "name and corresponding ssh key", + input: input{ + func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(`[{"id":1234,"key":"ssh-rsa AAAABbBB123","title":"Mac"}]`), + ) + reg.Register( + httpmock.REST("GET", ""), + httpmock.ScopesResponder("repo,read:org,read:public_key"), + ) + }, + false, + false, + true, + false, + }, + want: []string{"✹ Name: Mac \n SSH-KEY: ssh-rsa AAAABbBB123"}, + }, + { + name: "config error", + input: input{ + func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(""), + ) + reg.Register( + httpmock.REST("GET", ""), + httpmock.ScopesResponder("repo,read:org,read:public_key"), + ) + }, + true, + false, + true, + true, + }, + want: []string{"X: Config error"}, + }, + { + name: "http client error", + input: input{ + func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(""), + ) + reg.Register( + httpmock.REST("GET", ""), + httpmock.ScopesResponder("repo,read:org,read:public_key"), + ) + }, + false, + true, + true, + true, + }, + want: []string{"X: HttpClient error"}, + }, + { + name: "not found on api.github.com/user/keys", + input: input{ + func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StatusStringResponse(http.StatusNotFound, `{"message": "Not Found", "documentation_url": "url"}`), + ) + reg.Register( + httpmock.REST("GET", ""), + httpmock.ScopesResponder("repo,read:org,read:public_key"), + ) + }, + false, + false, + true, + true, + }, + want: []string{"X: Got HTTP 404 (https://api.github.com/user/keys)"}, + }, + { + name: "missing scope", + input: input{ + func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(`[{"id":1234,"key":"ssh-rsa AAAABbBB123","title":"Mac"}]`), + ) + reg.Register( + httpmock.REST("GET", ""), + httpmock.ScopesResponder(""), + ) + }, + false, + false, + true, + true, + }, + want: []string{ + "X: missing required scope 'repo';missing required scope 'read:org';missing required scope 'read:public_key'", + "- To request missing scopes, run: gh auth refresh -h github.com", + }, + }, + { + name: "authentication failed", + input: input{ + func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(`[{"id":1234,"key":"ssh-rsa AAAABbBB123","title":"Mac"}]`), + ) + reg.Register( + httpmock.REST("GET", ""), + httpmock.StatusStringResponse(http.StatusNotFound, `{"message": "Not Found", "documentation_url": "url"}`), + ) + }, + false, + false, + true, + true, + }, + want: []string{ + "X: authentication failed", + "- The github.com token in ~/.config/gh/hosts.yml is no longer valid.", + "- To re-authenticate, run: gh auth login -h github.com", + "- To forget about this host, run: gh auth logout -h github.com", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + tt.input.httpStubs(reg) + + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(true) + io.SetStdinTTY(true) + io.SetStderrTTY(true) + + opts := ListOptions{ + HTTPClient: func() (*http.Client, error) { + if tt.input.httpClientError { + return nil, errors.New("HttpClient error") + } + return &http.Client{Transport: reg}, nil + }, + IO: io, + Config: func() (config.Config, error) { + if tt.input.configError { + return nil, errors.New("Config error") + } + cfg := config.NewBlankConfig() + if tt.input.hasOauthToken { + err := cfg.Set("github.com", "oauth_token", "abc123") + if err != nil { + return nil, err + } + } + return cfg, nil + }, + } + + err := listRun(&opts) + if err != nil && !tt.input.wantErr { + t.Errorf("linRun() return error: %v", err) + } + if !reflect.DeepEqual(opts.ListMsg, tt.want) { + t.Errorf("linRun() = want %v, got %v", tt.want, opts.ListMsg) + } + }) + } +} diff --git a/pkg/cmd/ssh-key/ssh-key.go b/pkg/cmd/ssh-key/ssh-key.go new file mode 100644 index 000000000..1d0b471e6 --- /dev/null +++ b/pkg/cmd/ssh-key/ssh-key.go @@ -0,0 +1,20 @@ +package key + +import ( + cmdList "github.com/cli/cli/pkg/cmd/ssh-key/list" + "github.com/cli/cli/pkg/cmdutil" + "github.com/spf13/cobra" +) + +// NewCmdSSHKey creates a command for manage SSH Keys +func NewCmdSSHKey(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "ssh-key ", + Short: "Manage SSH keys", + Long: "Work with GitHub SSH keys", + } + + cmd.AddCommand(cmdList.NewCmdList(f, nil)) + + return cmd +}