Skip to content

Commit

Permalink
Show friendly message when API key is expired (#792)
Browse files Browse the repository at this point in the history
* Show login prompt when API key is expired

* Add package docs

* Reword message
  • Loading branch information
bernerd-stripe committed Dec 3, 2021
1 parent f60b7d0 commit 720733d
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 2 deletions.
3 changes: 3 additions & 0 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/stripe/stripe-cli/pkg/cmd/resource"
"github.com/stripe/stripe-cli/pkg/config"
"github.com/stripe/stripe-cli/pkg/login"
"github.com/stripe/stripe-cli/pkg/requests"
"github.com/stripe/stripe-cli/pkg/stripe"
"github.com/stripe/stripe-cli/pkg/useragent"
"github.com/stripe/stripe-cli/pkg/validators"
Expand Down Expand Up @@ -83,6 +84,8 @@ func Execute(ctx context.Context) {
isLoginRequiredError := errString == validators.ErrAPIKeyNotConfigured.Error() || errString == validators.ErrDeviceNameNotConfigured.Error()

switch {
case requests.IsAPIKeyExpiredError(err):
fmt.Fprintln(os.Stderr, "The API key provided has expired. Obtain a new key from the Dashboard or run `stripe login` and try again.")
case isLoginRequiredError:
// capitalize first letter of error because linter
errRunes := []rune(errString)
Expand Down
25 changes: 23 additions & 2 deletions pkg/requests/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
Expand Down Expand Up @@ -60,13 +61,26 @@ type RequestError struct {
msg string
StatusCode int
ErrorType string
ErrorCode string
Body interface{} // the raw response body
}

func (e RequestError) Error() string {
return fmt.Sprintf("%s, status=%d, body=%s", e.msg, e.StatusCode, e.Body)
}

// IsAPIKeyExpiredError returns true if the provided error was caused by a
// request returning an `api_key_expired` error code.
//
// See https://stripe.com/docs/error-codes/api-key-expired.
func IsAPIKeyExpiredError(err error) bool {
var reqErr RequestError
if errors.As(err, &reqErr) {
return reqErr.StatusCode == 401 && reqErr.ErrorCode == "api_key_expired"
}
return false
}

// Base encapsulates the required information needed to make requests to the API
type Base struct {
Cmd *cobra.Command
Expand Down Expand Up @@ -192,7 +206,7 @@ func (rb *Base) MakeRequest(ctx context.Context, apiKey, path string, params *Re

body, err := ioutil.ReadAll(resp.Body)

if errOnStatus && resp.StatusCode >= 300 {
if resp.StatusCode == 401 || (errOnStatus && resp.StatusCode >= 300) {
requestError := compileRequestError(body, resp.StatusCode)
return nil, requestError
}
Expand All @@ -211,6 +225,7 @@ func (rb *Base) MakeRequest(ctx context.Context, apiKey, path string, params *Re

func compileRequestError(body []byte, statusCode int) RequestError {
type requestErrorContent struct {
Code string `json:"code"`
Type string `json:"type"`
}

Expand All @@ -220,7 +235,13 @@ func compileRequestError(body []byte, statusCode int) RequestError {

var errorBody requestErrorBody
json.Unmarshal(body, &errorBody)
return RequestError{"Request failed", statusCode, errorBody.Content.Type, string(body)}
return RequestError{
msg: "Request failed",
StatusCode: statusCode,
ErrorType: errorBody.Content.Type,
ErrorCode: errorBody.Content.Code,
Body: string(body),
}
}

// Confirm calls the confirmCommand() function, triggering the confirmation process
Expand Down
52 changes: 52 additions & 0 deletions pkg/requests/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package requests
import (
"bufio"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -128,6 +129,32 @@ func TestMakeRequest_ErrOnStatus(t *testing.T) {
require.Equal(t, "Request failed, status=500, body=:(", err.Error())
}

func TestMakeRequest_ErrOnAPIKeyExpired(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`
{
"error": {
"code": "api_key_expired",
"doc_url": "https://stripe.com/docs/error-codes/api-key-expired",
"message": "Expired API Key provided: rk_test_***123",
"type": "invalid_request_error"
}
}
`))
}))
defer ts.Close()

rb := Base{APIBaseURL: ts.URL}
rb.Method = http.MethodGet

params := &RequestParameters{}

_, err := rb.MakeRequest(context.Background(), "sk_test_1234", "/foo/bar", params, false)
require.Error(t, err)
require.Contains(t, err.Error(), "Request failed, status=401, body=")
}

func TestGetUserConfirmationRequired(t *testing.T) {
reader := bufio.NewReader(strings.NewReader("yes\n"))

Expand Down Expand Up @@ -208,3 +235,28 @@ func TestCreateOrNormalizePath(t *testing.T) {
result, _ = createOrNormalizePath("charges")
require.Equal(t, "/v1/charges", result)
}

func TestIsAPIKeyExpiredError(t *testing.T) {
for _, tt := range []struct {
statusCode int
errorCode string
want bool
}{
{200, "", false},
{401, "authentication_required", false},
{500, "api_key_expired", false},
{401, "api_key_expired", true},
} {
t.Run(fmt.Sprintf("status=%v,code=%q", tt.statusCode, tt.errorCode), func(t *testing.T) {
err := RequestError{
StatusCode: tt.statusCode,
ErrorCode: tt.errorCode,
}
require.Equal(t, tt.want, IsAPIKeyExpiredError(err))
})
}

t.Run("non-RequestError", func(t *testing.T) {
require.False(t, IsAPIKeyExpiredError(fmt.Errorf("other")))
})
}

0 comments on commit 720733d

Please sign in to comment.