diff --git a/pkg/cmd/gpg-key/add/add_test.go b/pkg/cmd/gpg-key/add/add_test.go index c6d7c18fbe5..45119bdfda3 100644 --- a/pkg/cmd/gpg-key/add/add_test.go +++ b/pkg/cmd/gpg-key/add/add_test.go @@ -2,16 +2,93 @@ package add import ( "net/http" + "strings" "testing" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func Test_gpgKeyUploadScopesMissing(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.REST("POST", "user/gpg_keys"), + httpmock.StatusStringResponse(http.StatusNotFound, `{"message":"Not Found"}`), + ) + + err := gpgKeyUpload(&http.Client{Transport: reg}, "github.com", strings.NewReader("-----BEGIN PGP PUBLIC KEY BLOCK-----"), "") + + require.Same(t, errScopesMissing, err) +} + +func Test_gpgKeyUploadDuplicateKeyBeforeWrongFormat(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.REST("POST", "user/gpg_keys"), + httpmock.WithHeader(httpmock.StatusStringResponse(http.StatusUnprocessableEntity, `{ + "message": "Validation Failed", + "errors": [{ + "resource": "GpgKey", + "code": "custom", + "field": "key_id", + "message": "key_id already exists" + }] + }`), "Content-Type", "application/json"), + ) + + err := gpgKeyUpload(&http.Client{Transport: reg}, "github.com", strings.NewReader("binary-key"), "") + + require.Same(t, errDuplicateKey, err) +} + +func Test_gpgKeyUploadWrongFormat(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.REST("POST", "user/gpg_keys"), + httpmock.StatusStringResponse(http.StatusUnprocessableEntity, `{ + "message": "Validation Failed", + "errors": [{ + "resource": "GpgKey", + "code": "custom", + "message": "We got an error doing that." + }] + }`), + ) + + err := gpgKeyUpload(&http.Client{Transport: reg}, "github.com", strings.NewReader("binary-key"), "") + + require.Same(t, errWrongFormat, err) +} + +func Test_gpgKeyUploadHTTPError(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.REST("POST", "user/gpg_keys"), + httpmock.WithHeader( + httpmock.StatusStringResponse(http.StatusInternalServerError, `{"message":"Internal Server Error"}`), + "Content-Type", "application/json", + ), + ) + + err := gpgKeyUpload(&http.Client{Transport: reg}, "github.com", strings.NewReader("-----BEGIN PGP PUBLIC KEY BLOCK-----"), "") + + var httpErr api.HTTPError + require.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusInternalServerError, httpErr.StatusCode) + assert.Contains(t, err.Error(), "HTTP 500") + assert.EqualError(t, err, "HTTP 500: Internal Server Error (https://api.github.com/user/gpg_keys)") +} + func Test_runAdd(t *testing.T) { tests := []struct { name string @@ -28,7 +105,7 @@ func Test_runAdd(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("POST", "user/gpg_keys"), - httpmock.RESTPayload(200, ``, func(payload map[string]interface{}) { + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Contains(t, payload, "armored_public_key") assert.NotContains(t, payload, "title") })) @@ -44,7 +121,7 @@ func Test_runAdd(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("POST", "user/gpg_keys"), - httpmock.RESTPayload(200, ``, func(payload map[string]interface{}) { + httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Contains(t, payload, "armored_public_key") assert.Contains(t, payload, "name") })) diff --git a/pkg/cmd/gpg-key/add/http.go b/pkg/cmd/gpg-key/add/http.go index b1f0fca74cd..41b220a4d57 100644 --- a/pkg/cmd/gpg-key/add/http.go +++ b/pkg/cmd/gpg-key/add/http.go @@ -8,7 +8,6 @@ import ( "net/http" "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/safeurl" ) @@ -17,11 +16,6 @@ var errDuplicateKey = errors.New("key already exists") var errWrongFormat = errors.New("key in wrong format") func gpgKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, title string) error { - u, err := safeurl.JoinPathWithHostPrefix(ghinstance.RESTPrefix(hostname), "user", "gpg_keys") - if err != nil { - return err - } - keyBytes, err := io.ReadAll(keyFile) if err != nil { return err @@ -39,36 +33,33 @@ func gpgKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, t return err } - req, err := http.NewRequest("POST", u.String(), bytes.NewBuffer(payloadBytes)) + path, err := safeurl.JoinPath("user", "gpg_keys") if err != nil { return err } - resp, err := httpClient.Do(req) + // TODO(api-client-rollout) + // This line of code is part of a mechanical roll out of the api client. + // As a follow up, consider whether the api client can be injected to this call site, rather than constructed + apiClient := api.NewClientFromHTTP(httpClient) + err = apiClient.REST(hostname, "POST", path.String(), bytes.NewBuffer(payloadBytes), nil) if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == 404 { - return errScopesMissing - } else if resp.StatusCode > 299 { - err := api.HandleHTTPError(resp) - var httpError api.HTTPError - if errors.As(err, &httpError) { + if httpError, ok := errors.AsType[api.HTTPError](err); ok { + if httpError.StatusCode == 404 { + return errScopesMissing + } for _, e := range httpError.Errors { - if resp.StatusCode == 422 && e.Field == "key_id" && e.Message == "key_id already exists" { + if httpError.StatusCode == 422 && e.Field == "key_id" && e.Message == "key_id already exists" { return errDuplicateKey } } - } - if resp.StatusCode == 422 && !isGpgKeyArmored(keyBytes) { - return errWrongFormat + if httpError.StatusCode == 422 && !isGpgKeyArmored(keyBytes) { + return errWrongFormat + } } return err } - _, _ = io.Copy(io.Discard, resp.Body) return nil } diff --git a/pkg/cmd/gpg-key/delete/delete_test.go b/pkg/cmd/gpg-key/delete/delete_test.go index dc730b100ed..ef3b36f64ca 100644 --- a/pkg/cmd/gpg-key/delete/delete_test.go +++ b/pkg/cmd/gpg-key/delete/delete_test.go @@ -3,19 +3,63 @@ package delete import ( "bytes" "net/http" + "net/url" "testing" + "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "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/cli/go-gh/v2/pkg/api" + ghAPI "github.com/cli/go-gh/v2/pkg/api" "github.com/google/shlex" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func Test_deleteGPGKeyHTTPError(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.REST("DELETE", "user/gpg_keys/123"), + httpmock.WithHeader( + httpmock.StatusStringResponse(http.StatusInternalServerError, `{"message":"Internal Server Error"}`), + "Content-Type", "application/json", + ), + ) + + err := deleteGPGKey(&http.Client{Transport: reg}, "github.com", "123") + + var httpErr api.HTTPError + require.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusInternalServerError, httpErr.StatusCode) + assert.Contains(t, err.Error(), "HTTP 500") + assert.EqualError(t, err, "HTTP 500: Internal Server Error (https://api.github.com/user/gpg_keys/123)") +} + +func Test_getGPGKeysHTTPError(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.QueryMatcher("GET", "user/gpg_keys", url.Values{"per_page": []string{"100"}}), + httpmock.WithHeader( + httpmock.StatusStringResponse(http.StatusInternalServerError, `{"message":"Internal Server Error"}`), + "Content-Type", "application/json", + ), + ) + + keys, err := getGPGKeys(&http.Client{Transport: reg}, "github.com") + + assert.Nil(t, keys) + var httpErr api.HTTPError + require.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusInternalServerError, httpErr.StatusCode) + assert.Contains(t, err.Error(), "HTTP 500") + assert.EqualError(t, err, "HTTP 500: Internal Server Error (https://api.github.com/user/gpg_keys?per_page=100)") +} + func TestNewCmdDelete(t *testing.T) { tests := []struct { name string @@ -177,7 +221,7 @@ func Test_deleteRun(t *testing.T) { opts: DeleteOptions{KeyID: "ABC123", Confirmed: true}, httpStubs: func(reg *httpmock.Registry) { reg.Register(httpmock.REST("GET", "user/gpg_keys"), httpmock.StatusStringResponse(200, keysResp)) - reg.Register(httpmock.REST("DELETE", "user/gpg_keys/123"), httpmock.JSONErrorResponse(404, api.HTTPError{ + reg.Register(httpmock.REST("DELETE", "user/gpg_keys/123"), httpmock.JSONErrorResponse(404, ghAPI.HTTPError{ StatusCode: 404, Message: "GPG key 123 not found", })) diff --git a/pkg/cmd/gpg-key/delete/http.go b/pkg/cmd/gpg-key/delete/http.go index 9b6c2a46eae..af4f15a4557 100644 --- a/pkg/cmd/gpg-key/delete/http.go +++ b/pkg/cmd/gpg-key/delete/http.go @@ -1,12 +1,9 @@ package delete import ( - "encoding/json" - "io" "net/http" "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/safeurl" ) @@ -16,59 +13,30 @@ type gpgKey struct { } func deleteGPGKey(httpClient *http.Client, host, id string) error { - url, err := safeurl.JoinPathWithHostPrefix(ghinstance.RESTPrefix(host), "user", "gpg_keys", id) + path, err := safeurl.JoinPath("user", "gpg_keys", id) if err != nil { return err } - req, err := http.NewRequest("DELETE", url.String(), 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 + // TODO(api-client-rollout) + // This line of code is part of a mechanical roll out of the api client. + // As a follow up, consider whether the api client can be injected to this call site, rather than constructed + return api.NewClientFromHTTP(httpClient).REST(host, "DELETE", path.String(), nil, nil) } func getGPGKeys(httpClient *http.Client, host string) ([]gpgKey, error) { - u, err := safeurl.JoinPathWithHostPrefix(ghinstance.RESTPrefix(host), "user", "gpg_keys") + u, err := safeurl.JoinPath("user", "gpg_keys") if err != nil { return nil, err } u.SetQuery("per_page", "100") - req, err := http.NewRequest("GET", u.String(), 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 keys []gpgKey - err = json.Unmarshal(b, &keys) + // TODO(api-client-rollout) + // This line of code is part of a mechanical roll out of the api client. + // As a follow up, consider whether the api client can be injected to this call site, rather than constructed + err = api.NewClientFromHTTP(httpClient).REST(host, "GET", u.String(), nil, &keys) if err != nil { return nil, err } - return keys, nil } diff --git a/pkg/cmd/gpg-key/list/http.go b/pkg/cmd/gpg-key/list/http.go index 1b00684590e..8a282711025 100644 --- a/pkg/cmd/gpg-key/list/http.go +++ b/pkg/cmd/gpg-key/list/http.go @@ -1,15 +1,12 @@ package list import ( - "encoding/json" "errors" - "io" "net/http" "strings" "time" "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/safeurl" ) @@ -38,42 +35,27 @@ type gpgKey struct { } func userKeys(httpClient *http.Client, host, userHandle string) ([]gpgKey, error) { - u, err := safeurl.JoinPathWithHostPrefix(ghinstance.RESTPrefix(host), "user", "gpg_keys") + u, err := safeurl.JoinPath("user", "gpg_keys") if err != nil { return nil, err } if userHandle != "" { - u, err = safeurl.JoinPathWithHostPrefix(ghinstance.RESTPrefix(host), "users", userHandle, "gpg_keys") + u, err = safeurl.JoinPath("users", userHandle, "gpg_keys") if err != nil { return nil, err } } u.SetQuery("per_page", "100") - req, err := http.NewRequest("GET", u.String(), 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 == 404 { - return nil, errScopes - } else if resp.StatusCode > 299 { - return nil, api.HandleHTTPError(resp) - } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } var keys []gpgKey - err = json.Unmarshal(b, &keys) + // TODO(api-client-rollout) + // This line of code is part of a mechanical roll out of the api client. + // As a follow up, consider whether the api client can be injected to this call site, rather than constructed + err = api.NewClientFromHTTP(httpClient).REST(host, "GET", u.String(), nil, &keys) if err != nil { + if httpErr, ok := errors.AsType[api.HTTPError](err); ok && httpErr.StatusCode == 404 { + return nil, errScopes + } return nil, err } diff --git a/pkg/cmd/gpg-key/list/list_test.go b/pkg/cmd/gpg-key/list/list_test.go index daf8c991d2c..cf9a9b45d25 100644 --- a/pkg/cmd/gpg-key/list/list_test.go +++ b/pkg/cmd/gpg-key/list/list_test.go @@ -3,17 +3,70 @@ package list import ( "fmt" "net/http" + "net/url" "testing" "time" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func Test_userKeysScopesMissing(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.QueryMatcher("GET", "user/gpg_keys", url.Values{"per_page": []string{"100"}}), + httpmock.StatusStringResponse(http.StatusNotFound, `{"message":"Not Found"}`), + ) + + keys, err := userKeys(&http.Client{Transport: reg}, "github.com", "") + + assert.Nil(t, keys) + require.Same(t, errScopes, err) +} + +func Test_userKeysHTTPError(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.QueryMatcher("GET", "user/gpg_keys", url.Values{"per_page": []string{"100"}}), + httpmock.WithHeader( + httpmock.StatusStringResponse(http.StatusInternalServerError, `{"message":"Internal Server Error"}`), + "Content-Type", "application/json", + ), + ) + + keys, err := userKeys(&http.Client{Transport: reg}, "github.com", "") + + assert.Nil(t, keys) + var httpErr api.HTTPError + require.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusInternalServerError, httpErr.StatusCode) + assert.Contains(t, err.Error(), "HTTP 500") + assert.EqualError(t, err, "HTTP 500: Internal Server Error (https://api.github.com/user/gpg_keys?per_page=100)") +} + +func Test_userKeysForUser(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.QueryMatcher("GET", "users/monalisa/gpg_keys", url.Values{"per_page": []string{"100"}}), + httpmock.StringResponse(`[{"key_id":"ABC123"}]`), + ) + + keys, err := userKeys(&http.Client{Transport: reg}, "github.com", "monalisa") + + require.NoError(t, err) + require.Len(t, keys, 1) + assert.Equal(t, "ABC123", keys[0].KeyID) +} + func Test_listRun(t *testing.T) { tests := []struct { name string