diff --git a/client_test.go b/client_test.go index a1bbd07..d76a0eb 100644 --- a/client_test.go +++ b/client_test.go @@ -1,12 +1,16 @@ package gcloudcx_test import ( + "context" + "encoding/json" "fmt" "reflect" "strings" "testing" "time" + "github.com/gildas/go-core" + "github.com/gildas/go-errors" "github.com/gildas/go-gcloudcx" "github.com/gildas/go-logger" "github.com/google/uuid" @@ -35,6 +39,7 @@ func (suite *ClientSuite) SetupSuite() { Path: fmt.Sprintf("./log/test-%s.log", strings.ToLower(suite.Name)), Unbuffered: true, FilterLevel: logger.TRACE, + SourceInfo: true, }, ).Child("test", "test") suite.Logger.Infof("Suite Start: %s %s", suite.Name, strings.Repeat("=", 80-14-len(suite.Name))) @@ -79,3 +84,92 @@ func (suite *ClientSuite) TestCanInitializeWithoutOptions() { client := gcloudcx.NewClient(nil) suite.Require().NotNil(client, "GCloudCX Client is nil") } + +func (suite *ClientSuite) TestClientNotFoundErrorShouldBeBadRequest() { + payload := `{"error":"invalid_client","description":"client not found","error_description":"client not found"}` + var apiError gcloudcx.APIError + + err := json.Unmarshal([]byte(payload), &apiError) + suite.Require().NoError(err, "Unmarshalling should have succeeded") + suite.Logger.Errorf("Expected Error", apiError) + suite.Assert().ErrorIs(apiError, gcloudcx.BadCredentialsError) + suite.Assert().NotErrorIs(apiError, errors.RuntimeError) + suite.Assert().Equal(gcloudcx.BadCredentialsError.Status, apiError.Status) +} + +func (suite *ClientSuite) TestAuthFailedErrorShouldBeBadRequest() { + payload := `{"error":"invalid_client","description":"authentication failed","error_description":"authentication failed"}` + var apiError gcloudcx.APIError + + err := json.Unmarshal([]byte(payload), &apiError) + suite.Require().NoError(err, "Unmarshalling should have succeeded") + suite.Logger.Errorf("Expected Error", apiError) + suite.Assert().ErrorIs(apiError, gcloudcx.BadCredentialsError) + suite.Assert().NotErrorIs(apiError, errors.RuntimeError) + suite.Assert().Equal(gcloudcx.BadCredentialsError.Status, apiError.Status) +} + +func (suite *ClientSuite) TestCanLoginWithClientCredentials() { + clientID := uuid.New() + + if value := core.GetEnvAsString("PURECLOUD_CLIENTID", ""); len(value) > 0 { + clientID = uuid.MustParse(value) + } + + client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: core.GetEnvAsString("PURECLOUD_REGION", "mypurecloud.com"), + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: clientID, + Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "s3cr3t"), + }) + err := client.Login(context.Background()) + suite.Require().NoError(err, "Login should have succeeded") + suite.Require().NotEmpty(client.Grant.AccessToken(), "Access Token should not be empty") +} + +func (suite *ClientSuite) TestShouldFailLoginWithInvalidClientCredentialsSecret() { + clientID := uuid.New() + + if value := core.GetEnvAsString("PURECLOUD_CLIENTID", ""); len(value) > 0 { + clientID = uuid.MustParse(value) + } + + client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: core.GetEnvAsString("PURECLOUD_REGION", "mypurecloud.com"), + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: clientID, + Secret: "s3cr3t", + }) + err := client.Login(context.Background()) + suite.Require().Error(err, "Login should have failed") + suite.Logger.Errorf("Expected Error", err) + suite.Assert().NotErrorIs(err, errors.RuntimeError) + suite.Assert().ErrorIs(err, gcloudcx.BadCredentialsError) + + var apiError gcloudcx.APIError + suite.Require().ErrorAs(err, &apiError, "Error should be an APIError") + suite.Require().NotEmpty(apiError.MessageParams, "Error should have some parameters") + suite.Assert().Equal("authentication failed", apiError.MessageParams["description"]) +} + +func (suite *ClientSuite) TestShouldFailLoginWithInvalidClientCredentialsClientID() { + client := gcloudcx.NewClient(&gcloudcx.ClientOptions{ + Region: core.GetEnvAsString("PURECLOUD_REGION", "mypurecloud.com"), + Logger: suite.Logger, + }).SetAuthorizationGrant(&gcloudcx.ClientCredentialsGrant{ + ClientID: uuid.New(), // The chances of this being a valid Client ID are very low + Secret: core.GetEnvAsString("PURECLOUD_CLIENTSECRET", "s3cr3t"), + }) + err := client.Login(context.Background()) + suite.Require().Error(err, "Login should have failed") + suite.Logger.Errorf("Expected Error", err) + suite.Assert().NotErrorIs(err, errors.RuntimeError) + suite.Assert().ErrorIs(err, gcloudcx.BadCredentialsError) + + var apiError gcloudcx.APIError + suite.Require().ErrorAs(err, &apiError, "Error should be an APIError") + suite.Require().NotEmpty(apiError.MessageParams, "Error should have some parameters") + suite.Assert().Equal("client not found", apiError.MessageParams["description"]) +} diff --git a/error.go b/error.go index ba1071e..61d0349 100644 --- a/error.go +++ b/error.go @@ -38,7 +38,7 @@ var ( // AuthenticationRequiredError means the request should authenticate first AuthenticationRequiredError = APIError{Status: 401, Code: "authentication.required", Message: "No authentication bearer token specified in authorization header."} // BadCredentialsError means the credentials are invalid - BadCredentialsError = APIError{Status: 401, Code: "bad.credentials", Message: "Invalid login credentials."} + BadCredentialsError = APIError{Status: 401, Code: "bad.credentials", Message: "Invalid login credentials (%s)."} // CredentialsExpiredError means the credentials are expired CredentialsExpiredError = APIError{Status: 401, Code: "credentials.expired", Message: "The supplied credentials are expired and cannot be used."} @@ -138,6 +138,16 @@ func (e APIError) As(target interface{}) bool { return false } +// With creates a new Error from a given sentinel telling "what" is wrong and eventually their value. +// +// With also records the stack trace at the point it was called. +func (e APIError) With(what string, values ...interface{}) error { + final := e + final.MessageWithParams = fmt.Sprintf(final.Message, what) + final.Stack.Initialize() + return final +} + // WithStack creates a new error from a given Error and records its stack. func (e APIError) WithStack() error { final := e @@ -154,9 +164,20 @@ func (e *APIError) UnmarshalJSON(payload []byte) (err error) { }{} err = json.Unmarshal(payload, &oauthError) if err == nil && len(oauthError.Error) > 0 && len(oauthError.Description) > 0 { - *e = APIError{ - Code: BadCredentialsError.Code, - Message: fmt.Sprintf("%s: %s", oauthError.Description, oauthError.Error), + switch oauthError.Error { + case "invalid_client": + *e = BadCredentialsError + e.Message = fmt.Sprintf(e.Message, oauthError.Description) + e.MessageParams = map[string]string{ + "reason": oauthError.Error, + "description": oauthError.Description, + } + default: + *e = APIError{ + Status: BadCredentialsError.Status, + Code: BadCredentialsError.Code, + Message: fmt.Sprintf("%s: %s", oauthError.Description, oauthError.Error), + } } return nil } diff --git a/request.go b/request.go index 4dc9381..2322eba 100644 --- a/request.go +++ b/request.go @@ -83,7 +83,7 @@ func (client *Client) SendRequest(context context.Context, path URI, options *re correlationID := "" if res != nil { correlationID = res.Headers.Get("Inin-Correlation-Id") - log = log.Record("correlationId", correlationID) + log = log.Record("gcloudcx-correlationId", correlationID) } if err != nil { urlError := &url.Error{} @@ -119,9 +119,9 @@ func (client *Client) SendRequest(context context.Context, path URI, options *re apiError.Status = errors.HTTPUnauthorized.Code apiError.Code = errors.HTTPUnauthorized.ID } - return errors.WithStack(apiError) + return apiError.WithStack() } - return err + return errors.WithStack(err) } log.Debugf("Successfuly sent request in %s", duration) return nil diff --git a/version.go b/version.go index a127c8f..9379251 100644 --- a/version.go +++ b/version.go @@ -4,7 +4,7 @@ package gcloudcx var commit string // VERSION is the version of this application -var VERSION = "0.7.15" + commit +var VERSION = "0.7.16" + commit // APP is the name of the application const APP string = "GCloudCX Client"