Add ssh-key delete command (#6273)

This commit is contained in:
Natthakit Susanthitanon 2022-10-12 15:22:22 +07:00 committed by GitHub
parent f80f566ab2
commit 5962bc4383
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 366 additions and 1 deletions

View file

@ -0,0 +1,87 @@
package delete
import (
"fmt"
"net/http"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type DeleteOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
KeyID string
Confirmed bool
Prompter prompter.Prompter
}
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
opts := &DeleteOptions{
HttpClient: f.HttpClient,
Config: f.Config,
IO: f.IOStreams,
Prompter: f.Prompter,
}
cmd := &cobra.Command{
Use: "delete <id>",
Short: "Delete an SSH key from your GitHub account",
Args: cmdutil.ExactArgs(1, "cannot delete: key id required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.KeyID = args[0]
if !opts.IO.CanPrompt() && !opts.Confirmed {
return cmdutil.FlagErrorf("--confirm required when not running interactively")
}
if runF != nil {
return runF(opts)
}
return deleteRun(opts)
},
}
cmd.Flags().BoolVarP(&opts.Confirmed, "confirm", "y", false, "Skip the confirmation prompt")
return cmd
}
func deleteRun(opts *DeleteOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
cfg, err := opts.Config()
if err != nil {
return err
}
host, _ := cfg.DefaultHost()
key, err := getSSHKey(httpClient, host, opts.KeyID)
if err != nil {
return err
}
if !opts.Confirmed {
if err := opts.Prompter.ConfirmDeletion(key.Title); err != nil {
return err
}
}
err = deleteSSHKey(httpClient, host, opts.KeyID)
if err != nil {
return err
}
if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "%s SSH key %q (%s) deleted from your account\n", cs.SuccessIcon(), key.Title, opts.KeyID)
}
return nil
}

View file

@ -0,0 +1,210 @@
package delete
import (
"bytes"
"net/http"
"testing"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdDelete(t *testing.T) {
tests := []struct {
name string
tty bool
input string
output DeleteOptions
wantErr bool
wantErrMsg string
}{
{
name: "tty",
tty: true,
input: "123",
output: DeleteOptions{KeyID: "123", Confirmed: false},
},
{
name: "confirm flag tty",
tty: true,
input: "123 --confirm",
output: DeleteOptions{KeyID: "123", Confirmed: true},
},
{
name: "shorthand confirm flag tty",
tty: true,
input: "123 -y",
output: DeleteOptions{KeyID: "123", Confirmed: true},
},
{
name: "no tty",
input: "123",
wantErr: true,
wantErrMsg: "--confirm required when not running interactively",
},
{
name: "confirm flag no tty",
input: "123 --confirm",
output: DeleteOptions{KeyID: "123", Confirmed: true},
},
{
name: "shorthand confirm flag no tty",
input: "123 -y",
output: DeleteOptions{KeyID: "123", Confirmed: true},
},
{
name: "no args",
input: "",
wantErr: true,
wantErrMsg: "cannot delete: key id required",
},
{
name: "too many args",
input: "123 456",
wantErr: true,
wantErrMsg: "too many arguments",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, _, _ := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
f := &cmdutil.Factory{
IOStreams: ios,
}
argv, err := shlex.Split(tt.input)
assert.NoError(t, err)
var cmdOpts *DeleteOptions
cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
cmdOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantErr {
assert.Error(t, err)
assert.EqualError(t, err, tt.wantErrMsg)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.output.KeyID, cmdOpts.KeyID)
assert.Equal(t, tt.output.Confirmed, cmdOpts.Confirmed)
})
}
}
func Test_deleteRun(t *testing.T) {
keyResp := "{\"title\":\"My Key\"}"
tests := []struct {
name string
tty bool
opts DeleteOptions
httpStubs func(*httpmock.Registry)
prompterStubs func(*prompter.PrompterMock)
wantStdout string
wantErr bool
wantErrMsg string
}{
{
name: "delete tty",
tty: true,
opts: DeleteOptions{KeyID: "123"},
prompterStubs: func(pm *prompter.PrompterMock) {
pm.ConfirmDeletionFunc = func(_ string) error {
return nil
}
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(200, keyResp))
reg.Register(httpmock.REST("DELETE", "user/keys/123"), httpmock.StatusStringResponse(204, ""))
},
wantStdout: "✓ SSH key \"My Key\" (123) deleted from your account\n",
},
{
name: "delete with confirm flag tty",
tty: true,
opts: DeleteOptions{KeyID: "123", Confirmed: true},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(200, keyResp))
reg.Register(httpmock.REST("DELETE", "user/keys/123"), httpmock.StatusStringResponse(204, ""))
},
wantStdout: "✓ SSH key \"My Key\" (123) deleted from your account\n",
},
{
name: "not found tty",
tty: true,
opts: DeleteOptions{KeyID: "123"},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(404, ""))
},
wantErr: true,
wantErrMsg: "HTTP 404 (https://api.github.com/user/keys/123)",
},
{
name: "delete no tty",
opts: DeleteOptions{KeyID: "123", Confirmed: true},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(200, keyResp))
reg.Register(httpmock.REST("DELETE", "user/keys/123"), httpmock.StatusStringResponse(204, ""))
},
wantStdout: "",
},
{
name: "not found no tty",
opts: DeleteOptions{KeyID: "123", Confirmed: true},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "user/keys/123"), httpmock.StatusStringResponse(404, ""))
},
wantErr: true,
wantErrMsg: "HTTP 404 (https://api.github.com/user/keys/123)",
},
}
for _, tt := range tests {
pm := &prompter.PrompterMock{}
if tt.prompterStubs != nil {
tt.prompterStubs(pm)
}
tt.opts.Prompter = pm
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.Config = func() (config.Config, error) {
return config.NewBlankConfig(), nil
}
ios, _, stdout, _ := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
tt.opts.IO = ios
t.Run(tt.name, func(t *testing.T) {
err := deleteRun(&tt.opts)
reg.Verify(t)
if tt.wantErr {
assert.Error(t, err)
assert.EqualError(t, err, tt.wantErrMsg)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
})
}
}

View file

@ -0,0 +1,66 @@
package delete
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
)
type sshKey struct {
Title string
}
func deleteSSHKey(httpClient *http.Client, host string, keyID string) error {
url := fmt.Sprintf("%suser/keys/%s", ghinstance.RESTPrefix(host), keyID)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return api.HandleHTTPError(resp)
}
return nil
}
func getSSHKey(httpClient *http.Client, host string, keyID string) (*sshKey, error) {
url := fmt.Sprintf("%suser/keys/%s", ghinstance.RESTPrefix(host), keyID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode > 299 {
return nil, api.HandleHTTPError(resp)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var key sshKey
err = json.Unmarshal(b, &key)
if err != nil {
return nil, err
}
return &key, nil
}

View file

@ -2,6 +2,7 @@ package key
import (
cmdAdd "github.com/cli/cli/v2/pkg/cmd/ssh-key/add"
cmdDelete "github.com/cli/cli/v2/pkg/cmd/ssh-key/delete"
cmdList "github.com/cli/cli/v2/pkg/cmd/ssh-key/list"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
@ -14,8 +15,9 @@ func NewCmdSSHKey(f *cmdutil.Factory) *cobra.Command {
Long: "Manage SSH keys registered with your GitHub account.",
}
cmd.AddCommand(cmdList.NewCmdList(f, nil))
cmd.AddCommand(cmdAdd.NewCmdAdd(f, nil))
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
cmd.AddCommand(cmdList.NewCmdList(f, nil))
return cmd
}