Skip to content

Commit

Permalink
Merge pull request #507 from DRK3/MergeHigherLevelErrorsIntoMobileErr…
Browse files Browse the repository at this point in the history
…orDetails

feat(sdk): Merge higher-level errors into mobile error details field
  • Loading branch information
Derek Trider authored Jul 24, 2023
2 parents be47cf0 + 0b45be5 commit 1f6dc9f
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,8 @@ func TestIssuerInitiatedInteraction_GrantTypes(t *testing.T) {
require.False(t, interaction.AuthorizationCodeGrantTypeSupported())

authorizationCodeGrantParams, err := interaction.AuthorizationCodeGrantParams()
requireErrorContains(t, err,
"INVALID_SDK_USAGE(OCI3-0000):issuer does not support the authorization code grant")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "issuer does not support the authorization code grant")
require.Nil(t, authorizationCodeGrantParams)

interaction = createIssuerInitiatedInteraction(t, kms, nil, createCredentialOfferIssuanceURI(t, "example.com", true),
Expand All @@ -415,13 +415,13 @@ func TestIssuerInitiatedInteraction_DynamicClientRegistration(t *testing.T) {
nil, false)

supported, err := interaction.DynamicClientRegistrationSupported()
requireErrorContains(t, err, "ISSUER_OPENID_CONFIG_FETCH_FAILED(OCI1-0003):failed to fetch issuer's "+
"OpenID configuration: openid configuration endpoint: Get")
requireErrorContains(t, err, "ISSUER_OPENID_CONFIG_FETCH_FAILED")
requireErrorContains(t, err, "failed to fetch issuer's OpenID configuration: openid configuration endpoint: Get")
require.False(t, supported)

endpoint, err := interaction.DynamicClientRegistrationEndpoint()
requireErrorContains(t, err, "ISSUER_OPENID_CONFIG_FETCH_FAILED(OCI1-0003):failed to fetch issuer's "+
"OpenID configuration: openid configuration endpoint: Get ")
requireErrorContains(t, err, "ISSUER_OPENID_CONFIG_FETCH_FAILED")
requireErrorContains(t, err, "failed to fetch issuer's OpenID configuration: openid configuration endpoint: Get ")
require.Empty(t, endpoint)
}

Expand Down Expand Up @@ -499,17 +499,17 @@ func TestIssuerInitiatedInteractionAlias(t *testing.T) {
require.False(t, interaction.AuthorizationCodeGrantTypeSupported())

authCodeGrantParams, err := interaction.AuthorizationCodeGrantParams()
requireErrorContains(t, err,
"INVALID_SDK_USAGE(OCI3-0000):issuer does not support the authorization code grant")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "issuer does not support the authorization code grant")
require.Nil(t, authCodeGrantParams)

dynamicClientRegistrationSupported, err := interaction.DynamicClientRegistrationSupported()
require.NoError(t, err)
require.False(t, dynamicClientRegistrationSupported)

dynamicClientRegistrationEndpoint, err := interaction.DynamicClientRegistrationEndpoint()
requireErrorContains(t, err,
"INVALID_SDK_USAGE(OCI3-0000):issuer does not support dynamic client registration")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "issuer does not support dynamic client registration")
require.Empty(t, dynamicClientRegistrationEndpoint)

traceID := interaction.OTelTraceID()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ func TestWalletInitiatedInteraction_Flow(t *testing.T) {
require.False(t, dynamicClientRegistrationSupported)

dynamicClientRegistrationEndpoint, err := interaction.DynamicClientRegistrationEndpoint()
requireErrorContains(t, err,
"INVALID_SDK_USAGE(OCI3-0000):issuer does not support dynamic client registration")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "issuer does not support dynamic client registration")
require.Empty(t, dynamicClientRegistrationEndpoint)

credentialTypes := api.NewStringArray().Append("type")
Expand Down Expand Up @@ -107,7 +107,8 @@ func TestWalletInitiatedInteraction_Flow(t *testing.T) {

func TestNewWalletInitiatedInteraction(t *testing.T) {
interaction, err := openid4ci.NewWalletInitiatedInteraction(nil, nil)
requireErrorContains(t, err, "INVALID_SDK_USAGE(OCI3-0000):args object must be provided")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "args object must be provided")
require.Nil(t, interaction)
}

Expand Down Expand Up @@ -141,7 +142,8 @@ func TestWalletInitiatedInteraction_RequestCredential_Failure(t *testing.T) {
require.NotNil(t, interaction)

credentials, err := interaction.RequestCredential(nil, "", nil)
requireErrorContains(t, err, "INVALID_SDK_USAGE(OCI3-0000):verification method must be provided")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "verification method must be provided")
require.Nil(t, credentials)
}

Expand Down
1 change: 0 additions & 1 deletion cmd/wallet-sdk-gomobile/walleterror/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ type Error struct {
Category string `json:"category"`
Details string `json:"details"`
TraceID string `json:"trace_id"`
Full string `json:"full"`
}

// Parse used to parse exception message on mobile side.
Expand Down
59 changes: 38 additions & 21 deletions cmd/wallet-sdk-gomobile/wrapper/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/otel"
"github.com/trustbloc/wallet-sdk/cmd/wallet-sdk-gomobile/walleterror"
Expand All @@ -27,36 +28,52 @@ func ToMobileErrorWithTrace(err error, trace *otel.Trace) error {
return nil
}

gomobileError := convertToGomobileError(err, trace)

marshalledGomobileError, err := json.Marshal(gomobileError)
if err != nil {
return fmt.Errorf("failed to marshal error: %w", err)
}

return errors.New(string(marshalledGomobileError))
}

func convertToGomobileError(err error, trace *otel.Trace) *walleterror.Error {
traceID := ""
if trace != nil {
traceID = trace.TraceID()
}

var result *walleterror.Error
errorToExamine := err

var walletError *goapiwalleterror.Error
for errorToExamine != nil {
//nolint:errorlint // The linter wants us to use errors.As here, but we need to know the precise spot
// in the error chain where the higher-level non-goapiwalleterror.Errors end.
walletError, ok := errorToExamine.(*goapiwalleterror.Error)
if ok {
// If the highest-level error message it itself a goapiwalleterror.Error, then higherLevelErrorMessage
// will be a blank string, so walletError.ParentError will simply be passed through.
higherLevelErrorMessage := strings.ReplaceAll(err.Error(), walletError.Error(), "")

if errors.As(err, &walletError) {
result = &walleterror.Error{
Code: walletError.Code,
Category: walletError.Scenario,
Details: walletError.ParentError,
TraceID: traceID,
Full: err.Error(),
}
} else {
result = &walleterror.Error{
Code: "UKN2-000",
Category: "UNEXPECTED_ERROR",
Details: err.Error(),
TraceID: traceID,
mergedErrorMessage := higherLevelErrorMessage + walletError.ParentError

return &walleterror.Error{
Code: walletError.Code,
Category: walletError.Scenario,
Details: mergedErrorMessage,
TraceID: traceID,
}
}
}

formatted, fmtErr := json.Marshal(result)
if fmtErr != nil {
return fmt.Errorf("failed to marshal error: %w", fmtErr)
errorToExamine = errors.Unwrap(errorToExamine)
}

return errors.New(string(formatted))
// There's no wallet Error in the chain, and so there's no error code available. Let's create a new
// gomobile wallet error using a generic code.
return &walleterror.Error{
Code: "UKN2-000",
Category: "UNEXPECTED_ERROR",
Details: err.Error(),
TraceID: traceID,
}
}
97 changes: 70 additions & 27 deletions cmd/wallet-sdk-gomobile/wrapper/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestToMobileError(t *testing.T) {
err := wrapper.ToMobileError(nil)
require.NoError(t, err)
})
t.Run("Wallet error passed in", func(t *testing.T) {
t.Run("goapiwalleterror.Error passed in", func(t *testing.T) {
walletError := &goapiwalleterror.Error{
Code: "Code",
Scenario: "Category",
Expand All @@ -39,24 +39,20 @@ func TestToMobileError(t *testing.T) {
require.Equal(t, "Category", parsedErr.Category)
require.Equal(t, "Details", parsedErr.Details)
})
t.Run("goapiwalleterror.Error passed in", func(t *testing.T) {
walletError := &goapiwalleterror.Error{
Code: "Code",
Scenario: "Category",
ParentError: "Details",
}
t.Run("Non-goapiwalleterror.Error passed in", func(t *testing.T) {
goErr := errors.New("regular Go error")

err := wrapper.ToMobileError(walletError)
err := wrapper.ToMobileError(goErr)
require.Error(t, err)

parsedErr := walleterror.Parse(err.Error())

require.Equal(t, "Code", parsedErr.Code)
require.Equal(t, "Category", parsedErr.Category)
require.Equal(t, "Details", parsedErr.Details)
require.Equal(t, "UKN2-000", parsedErr.Code)
require.Equal(t, "UNEXPECTED_ERROR", parsedErr.Category)
require.Equal(t, "regular Go error", parsedErr.Details)
})
t.Run("Non-goapiwalleterror.Error passed in", func(t *testing.T) {
goErr := errors.New("regular Go error")
t.Run("Non-goapiwalleterror.Error wrapped with another Non-goapiwalleterror.Error passed in", func(t *testing.T) {
goErr := fmt.Errorf("higher-level error: %w", errors.New("regular Go error"))

err := wrapper.ToMobileError(goErr)
require.Error(t, err)
Expand All @@ -65,28 +61,75 @@ func TestToMobileError(t *testing.T) {

require.Equal(t, "UKN2-000", parsedErr.Code)
require.Equal(t, "UNEXPECTED_ERROR", parsedErr.Category)
require.Equal(t, "regular Go error", parsedErr.Details)
require.Equal(t, "higher-level error: regular Go error", parsedErr.Details)
})
t.Run("goapiwalleterror.Error wrapped by a higher-level error is passed in", func(t *testing.T) {
walletError := &goapiwalleterror.Error{
Code: "Code",
Scenario: "Category",
ParentError: "Details",
t.Run("goapiwalleterror.Error wrapped by one higher-level non-goapiwalleterror.Error is passed in",
func(t *testing.T) {
walletError := &goapiwalleterror.Error{
Code: "Code",
Scenario: "Category",
ParentError: "Details",
}

wrappedWalletError := fmt.Errorf("higher-level error: %w", walletError)

err := wrapper.ToMobileError(wrappedWalletError)
require.Error(t, err)

parsedErr := walleterror.Parse(err.Error())

require.Equal(t, "Code", parsedErr.Code)
require.Equal(t, "Category", parsedErr.Category)
require.Equal(t, "higher-level error: Details", parsedErr.Details)
})
t.Run("goapiwalleterror.Error wrapped by two higher-level non-goapiwalleterror.Errors is passed in",
func(t *testing.T) {
walletError := &goapiwalleterror.Error{
Code: "Code",
Scenario: "Category",
ParentError: "Details",
}

doubleWrappedWalletError := fmt.Errorf("even-higher-level error: %w",
fmt.Errorf("higher-level error: %w", walletError))

err := wrapper.ToMobileError(doubleWrappedWalletError)
require.Error(t, err)

parsedErr := walleterror.Parse(err.Error())

require.Equal(t, "Code", parsedErr.Code)
require.Equal(t, "Category", parsedErr.Category)
require.Equal(t, "even-higher-level error: higher-level error: Details", parsedErr.Details)
})
t.Run("goapiwalleterror.Error wrapped by another goapiwalleterror.Error", func(t *testing.T) {
// Note: We shouldn't actually do this anywhere in our code. If this happens, then the highest-level
// goapiwalleterror.Error is the one that will be detected and converted properly to the Gomobile error type,
// while the lower one will get "squashed" into the Details field. This test just confirms that this is the
// expected behaviour in such a scenario.

lowerLevelWalletError := &goapiwalleterror.Error{
Code: "Lower-Level-Code",
Scenario: "Lower-Level-Category",
ParentError: "Lower-Level-Details",
}

higherLevelWalletError := &goapiwalleterror.Error{
Code: "Higher-Level-Code",
Scenario: "Higher-Level-Category",
ParentError: lowerLevelWalletError.Error(),
}

wrappedWalletError := fmt.Errorf("higher-level error: %w", walletError)
wrappedWalletError := fmt.Errorf("regular Go error: %w", higherLevelWalletError)

// ToMobileError parses the first goapiwalleterror.Error it finds in the error chain into the
// Code, Category, and Details fields. If there was a higher-level error above the first goapiwalleterror.Error,
// then it'll be captured in the "Full" field.
err := wrapper.ToMobileError(wrappedWalletError)
require.Error(t, err)

parsedErr := walleterror.Parse(err.Error())

require.Equal(t, "Code", parsedErr.Code)
require.Equal(t, "Category", parsedErr.Category)
require.Equal(t, "Details", parsedErr.Details)
require.Equal(t, "higher-level error: Category(Code):Details", parsedErr.Full)
require.Equal(t, "Higher-Level-Code", parsedErr.Code)
require.Equal(t, "Higher-Level-Category", parsedErr.Category)
require.Equal(t, "regular Go error: Lower-Level-Category(Lower-Level-Code):Lower-Level-Details",
parsedErr.Details)
})
}

0 comments on commit 1f6dc9f

Please sign in to comment.