diff --git a/pkg/cmd/auth/shared/login_flow.go b/pkg/cmd/auth/shared/login_flow.go index 837ff0d64..4f2cb9e0c 100644 --- a/pkg/cmd/auth/shared/login_flow.go +++ b/pkg/cmd/auth/shared/login_flow.go @@ -200,7 +200,7 @@ func Login(opts *LoginOptions) error { } if keyToUpload != "" { - err := sshKeyUpload(httpClient, hostname, keyToUpload, keyTitle) + err := sshKeyUpload(httpClient, hostname, keyToUpload, keyTitle, opts.IO) if err != nil { return err } @@ -223,14 +223,14 @@ func scopesSentence(scopes []string, isEnterprise bool) string { return strings.Join(quoted, ", ") } -func sshKeyUpload(httpClient *http.Client, hostname, keyFile string, title string) error { +func sshKeyUpload(httpClient *http.Client, hostname, keyFile string, title string, ios *iostreams.IOStreams) error { f, err := os.Open(keyFile) if err != nil { return err } defer f.Close() - return add.SSHKeyUpload(httpClient, hostname, f, title) + return add.SSHKeyUpload(httpClient, hostname, f, title, false, ios) } func getCurrentLogin(httpClient httpClient, hostname, authToken string) (string, error) { diff --git a/pkg/cmd/auth/shared/login_flow_test.go b/pkg/cmd/auth/shared/login_flow_test.go index f01ba46ff..92ae75915 100644 --- a/pkg/cmd/auth/shared/login_flow_test.go +++ b/pkg/cmd/auth/shared/login_flow_test.go @@ -38,6 +38,9 @@ func TestLogin_ssh(t *testing.T) { tr.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{ "login": "monalisa" }}}`)) + tr.Register( + httpmock.REST("GET", "api/v3/user/keys"), + httpmock.StringResponse(`[]`)) tr.Register( httpmock.REST("POST", "api/v3/user/keys"), httpmock.StringResponse(`{}`)) @@ -78,7 +81,7 @@ func TestLogin_ssh(t *testing.T) { } assert.Equal(t, expected, args) // simulate that the public key file has been generated - _ = os.WriteFile(keyFile+".pub", []byte("PUBKEY"), 0600) + _ = os.WriteFile(keyFile+".pub", []byte("PUBKEY asdf"), 0600) }) cfg := tinyConfig{} diff --git a/pkg/cmd/ssh-key/add/add.go b/pkg/cmd/ssh-key/add/add.go index e3f5088d2..df977b291 100644 --- a/pkg/cmd/ssh-key/add/add.go +++ b/pkg/cmd/ssh-key/add/add.go @@ -1,7 +1,6 @@ package add import ( - "fmt" "io" "net/http" "os" @@ -79,14 +78,11 @@ func runAdd(opts *AddOptions) error { hostname, _ := cfg.Authentication().DefaultHost() - err = SSHKeyUpload(httpClient, hostname, keyReader, opts.Title) + err = SSHKeyUpload(httpClient, hostname, keyReader, opts.Title, true, opts.IO) if err != nil { return err } - if opts.IO.IsStdoutTTY() { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.ErrOut, "%s Public key added to your account\n", cs.SuccessIcon()) - } + // SSHKeyUpload prints the success message. return nil } diff --git a/pkg/cmd/ssh-key/add/add_test.go b/pkg/cmd/ssh-key/add/add_test.go index 565528ddd..8497dc991 100644 --- a/pkg/cmd/ssh-key/add/add_test.go +++ b/pkg/cmd/ssh-key/add/add_test.go @@ -11,33 +11,94 @@ import ( ) func Test_runAdd(t *testing.T) { - ios, stdin, stdout, stderr := iostreams.Test() - ios.SetStdinTTY(false) - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) + tests := []struct { + name string + stdin string + opts AddOptions + httpStubs func(*httpmock.Registry) + wantStdout string + wantStderr string + wantErrMsg string + }{ + { + name: "valid key format, not already in use", + stdin: "ssh-ed25519 asdf", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse("[]")) + reg.Register( + httpmock.REST("POST", "user/keys"), + httpmock.RESTPayload(200, ``, func(payload map[string]interface{}) { + assert.Contains(t, payload, "key") + assert.Empty(t, payload["title"]) + })) + }, + wantStdout: "", + wantStderr: "✓ Public key added to your account\n", + wantErrMsg: "", + opts: AddOptions{KeyFile: "-"}, + }, + { + name: "valid key format, already in use", + stdin: "ssh-ed25519 asdf title", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(`[ + { + "id": 1, + "key": "ssh-ed25519 asdf", + "title": "anything" + } + ]`)) + }, + wantStdout: "", + wantStderr: "✓ Public key already exists on your account\n", + wantErrMsg: "", + opts: AddOptions{KeyFile: "-"}, + }, + { + name: "invalid key format", + stdin: "ssh-ed25519", + wantStdout: "", + wantStderr: "X Error: provided key is not in a valid format\n", + wantErrMsg: "SilentError", + opts: AddOptions{KeyFile: "-"}, + }, + } - stdin.WriteString("PUBKEY") + for _, tt := range tests { + ios, stdin, stdout, stderr := iostreams.Test() + ios.SetStdinTTY(false) + ios.SetStdoutTTY(true) + ios.SetStderrTTY(true) - tr := httpmock.Registry{} - defer tr.Verify(t) + stdin.WriteString(tt.stdin) - tr.Register( - httpmock.REST("POST", "user/keys"), - httpmock.StringResponse(`{}`)) + reg := &httpmock.Registry{} - err := runAdd(&AddOptions{ - IO: ios, - Config: func() (config.Config, error) { + tt.opts.IO = ios + tt.opts.HTTPClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + tt.opts.Config = func() (config.Config, error) { return config.NewBlankConfig(), nil - }, - HTTPClient: func() (*http.Client, error) { - return &http.Client{Transport: &tr}, nil - }, - KeyFile: "-", - Title: "my sacred key", - }) - assert.NoError(t, err) + } - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "✓ Public key added to your account\n", stderr.String()) + t.Run(tt.name, func(t *testing.T) { + defer reg.Verify(t) + err := runAdd(&tt.opts) + if tt.wantErrMsg != "" { + assert.Equal(t, tt.wantErrMsg, err.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) + }) + } } diff --git a/pkg/cmd/ssh-key/add/http.go b/pkg/cmd/ssh-key/add/http.go index 1b8b7b245..9a603393d 100644 --- a/pkg/cmd/ssh-key/add/http.go +++ b/pkg/cmd/ssh-key/add/http.go @@ -3,21 +3,50 @@ package add import ( "bytes" "encoding/json" + "fmt" "io" "net/http" + "strings" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/cmd/ssh-key/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" ) -func SSHKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, title string) error { +func SSHKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, title string, printSuccessMsgs bool, ios *iostreams.IOStreams) error { url := ghinstance.RESTPrefix(hostname) + "user/keys" + cs := ios.ColorScheme() keyBytes, err := io.ReadAll(keyFile) if err != nil { return err } + userKey := string(keyBytes) + splitKey := strings.Fields(userKey) + if len(splitKey) < 2 { + fmt.Fprintf(ios.ErrOut, "%s Error: provided key is not in a valid format\n", cs.FailureIcon()) + return cmdutil.SilentError + } + + userKey = splitKey[0] + " " + splitKey[1] + + keys, err := shared.UserKeys(httpClient, hostname, "") + if err != nil { + return err + } + + for _, k := range keys { + if k.Key == userKey { + if printSuccessMsgs && ios.IsStdoutTTY() { + fmt.Fprintf(ios.ErrOut, "%s Public key already exists on your account\n", cs.SuccessIcon()) + } + return nil + } + } + payload := map[string]string{ "title": title, "key": string(keyBytes), @@ -48,5 +77,8 @@ func SSHKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, t return err } + if printSuccessMsgs { + fmt.Fprintf(ios.ErrOut, "%s Public key added to your account\n", cs.SuccessIcon()) + } return nil } diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go index d057b198c..0c0cbcd0b 100644 --- a/pkg/cmd/ssh-key/list/list.go +++ b/pkg/cmd/ssh-key/list/list.go @@ -7,6 +7,7 @@ import ( "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/cmd/ssh-key/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/utils" @@ -55,7 +56,7 @@ func listRun(opts *ListOptions) error { host, _ := cfg.Authentication().DefaultHost() - sshKeys, err := userKeys(apiClient, host, "") + sshKeys, err := shared.UserKeys(apiClient, host, "") if err != nil { return err } diff --git a/pkg/cmd/ssh-key/list/http.go b/pkg/cmd/ssh-key/shared/user_keys.go similarity index 91% rename from pkg/cmd/ssh-key/list/http.go rename to pkg/cmd/ssh-key/shared/user_keys.go index 7275615e0..57f309d19 100644 --- a/pkg/cmd/ssh-key/list/http.go +++ b/pkg/cmd/ssh-key/shared/user_keys.go @@ -1,4 +1,4 @@ -package list +package shared import ( "encoding/json" @@ -18,7 +18,7 @@ type sshKey struct { CreatedAt time.Time `json:"created_at"` } -func userKeys(httpClient *http.Client, host, userHandle string) ([]sshKey, error) { +func UserKeys(httpClient *http.Client, host, userHandle string) ([]sshKey, error) { resource := "user/keys" if userHandle != "" { resource = fmt.Sprintf("users/%s/keys", userHandle)