Skip to content

Commit

Permalink
feat: better flag create error handling (#85)
Browse files Browse the repository at this point in the history
Handle 401 error consistent with other errors
  • Loading branch information
dbolson authored Mar 28, 2024
1 parent 51601a2 commit 708925b
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 11 deletions.
73 changes: 67 additions & 6 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package errors

import (
"encoding/json"
"fmt"

"github.com/pkg/errors"

ldapi "github.com/launchdarkly/api-client-go/v14"

"ldcli/cmd/cliflags"
)

Expand Down Expand Up @@ -45,12 +44,74 @@ func NewErrorWrapped(message string, underlying error) error {
})
}

func NewAPIError(err error) error {
var ldErr *ldapi.GenericOpenAPIError
ok := errors.As(err, &ldErr)
// LDAPIError is an error from the LaunchDarkly API client.
type LDAPIError interface {
Body() []byte
Error() string
Model() interface{}
}

type APIError struct {
body []byte
err error
model interface{}
}

func NewAPIError(body []byte, err error, model interface{}) APIError {
return APIError{
body: body,
err: err,
model: model,
}
}

func (e APIError) Error() string {
return e.err.Error()
}

func (e APIError) Body() []byte {
return e.body
}

func (e APIError) Model() interface{} {
return e.model
}

// NewLDAPIError converts the error returned from API calls to LaunchDarkly to have a
// consistent Error() JSON structure.
func NewLDAPIError(err error) error {
var apiErr LDAPIError
ok := errors.As(err, &apiErr)
if ok {
return NewErrorWrapped(string(ldErr.Body()), ldErr)
// the 401 response does not have a body, so we need to create one for a
// consistent response
if err.Error() == "401 Unauthorized" {
errMsg, err := normalizeUnauthorizedJSON()
if err != nil {
return err
}

return NewError(string(errMsg))
}

return NewErrorWrapped(string(apiErr.Body()), apiErr)
}

return err
}

func normalizeUnauthorizedJSON() ([]byte, error) {
e := struct {
Code string `json:"code"`
Message string `json:"message"`
}{
Code: "unauthorized",
Message: "You do not have access to perform this action",
}
errMsg, err := json.Marshal(e)
if err != nil {
return nil, err
}

return errMsg, nil
}
57 changes: 57 additions & 0 deletions internal/errors/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package errors_test

import (
"encoding/json"
"errors"
"testing"

ldapi "github.com/launchdarkly/api-client-go/v14"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

errs "ldcli/internal/errors"
)

func TestAPIError(t *testing.T) {
t.Run("with a 400 error has a JSON response", func(t *testing.T) {
underlying := errs.NewAPIError(
[]byte(`{"code":"conflict","message":"an error"}`),
errors.New("409 Conflict"),
[]string{},
)

err := errs.NewLDAPIError(underlying)

require.Error(t, err)
assert.JSONEq(t, `{"code": "conflict", "message": "an error"}`, err.Error())
})

t.Run("with a 401 error has a JSON response", func(t *testing.T) {
rep := ldapi.UnauthorizedErrorRep{}
repBytes, err := json.Marshal(rep)
require.NoError(t, err)
underlying := errs.NewAPIError(
repBytes,
errors.New("401 Unauthorized"),
[]string{},
)

err = errs.NewLDAPIError(underlying)

require.Error(t, err)
assert.JSONEq(t, `{"code": "unauthorized", "message": "You do not have access to perform this action"}`, err.Error())
})

t.Run("with a 403 error has a JSON response", func(t *testing.T) {
underlying := errs.NewAPIError(
[]byte(`{"code": "forbidden", "message": "an error"}`),
errors.New("409 Conflict"),
[]string{},
)

err := errs.NewLDAPIError(underlying)

require.Error(t, err)
assert.JSONEq(t, `{"code": "forbidden", "message": "an error"}`, err.Error())
})
}
4 changes: 2 additions & 2 deletions internal/flags/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c FlagsClient) Create(
post := ldapi.NewFeatureFlagBody(name, key)
flag, _, err := client.FeatureFlagsApi.PostFeatureFlag(ctx, projectKey).FeatureFlagBody(*post).Execute()
if err != nil {
return nil, errors.NewAPIError(err)
return nil, errors.NewLDAPIError(err)

}

Expand All @@ -68,7 +68,7 @@ func (c FlagsClient) Update(
PatchWithComment(*ldapi.NewPatchWithComment(patch)).
Execute()
if err != nil {
return nil, errors.NewAPIError(err)
return nil, errors.NewLDAPIError(err)
}

responseJSON, err := json.Marshal(flag)
Expand Down
2 changes: 1 addition & 1 deletion internal/members/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (c MembersClient) Create(ctx context.Context, accessToken, baseURI, email,
memberForm := ldapi.NewMemberForm{Email: email, Role: &role}
members, _, err := client.AccountMembersApi.PostMembers(ctx).NewMemberForm([]ldapi.NewMemberForm{memberForm}).Execute()
if err != nil {
return nil, errors.NewAPIError(err)
return nil, errors.NewLDAPIError(err)
}
memberJson, err := json.Marshal(members.Items[0])
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/projects/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (c ProjectsClient) Create(
projectPost := ldapi.NewProjectPost(name, key)
project, _, err := client.ProjectsApi.PostProject(ctx).ProjectPost(*projectPost).Execute()
if err != nil {
return nil, errors.NewAPIError(err)
return nil, errors.NewLDAPIError(err)
}
projectJSON, err := json.Marshal(project)
if err != nil {
Expand All @@ -55,7 +55,7 @@ func (c ProjectsClient) List(
Limit(2).
Execute()
if err != nil {
return nil, errors.NewAPIError(err)
return nil, errors.NewLDAPIError(err)
}

projectsJSON, err := json.Marshal(projects)
Expand Down

0 comments on commit 708925b

Please sign in to comment.