Repository: newrelic/newrelic-client-go
Pull request #866 (2022-04-08 19:25:10)

feat(errors): handle 402 payment required HTTP response scenario


Patch:
diff --git a/internal/http/client.go b/internal/http/client.go
index 6b20bc8..3baeaca 100644
--- a/internal/http/client.go
+++ b/internal/http/client.go
@@ -357,6 +357,10 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
 			return nil, nrErrors.NewUnauthorizedError()
 		}
 
+		if errorValue.IsPaymentRequired(resp) {
+			return nil, nrErrors.NewPaymentRequiredError()
+		}
+
 		return nil, nrErrors.NewUnexpectedStatusCode(resp.StatusCode, errorValue.Error())
 	}
 
@@ -450,6 +454,10 @@ func (c *Client) innerDo(req *Request, errorValue ErrorResponse, i int) (*http.R
 		return resp, body, false, nrErrors.NewNotFound(errorValue.Error())
 	}
 
+	if errorValue.IsPaymentRequired(resp) {
+		return resp, body, false, nrErrors.NewPaymentRequiredError()
+	}
+
 	if !errorValue.IsRetryableError() {
 		return resp, body, false, nil
 	}
diff --git a/internal/http/errors.go b/internal/http/errors.go
index 60e7b18..148b09e 100644
--- a/internal/http/errors.go
+++ b/internal/http/errors.go
@@ -12,6 +12,7 @@ type ErrorResponse interface {
 	IsNotFound() bool
 	IsRetryableError() bool
 	IsUnauthorized(resp *http.Response) bool
+	IsPaymentRequired(resp *http.Response) bool
 	IsDeprecated() bool
 	Error() string
 	New() ErrorResponse
@@ -49,6 +50,10 @@ func (e *DefaultErrorResponse) IsDeprecated() bool {
 	return false
 }
 
+func (e *DefaultErrorResponse) IsPaymentRequired(resp *http.Response) bool {
+	return resp.StatusCode == http.StatusPaymentRequired
+}
+
 // IsUnauthorized checks a response for a 401 Unauthorize HTTP status code.
 func (e *DefaultErrorResponse) IsUnauthorized(resp *http.Response) bool {
 	return resp.StatusCode == http.StatusUnauthorized
diff --git a/internal/http/graphql.go b/internal/http/graphql.go
index ce53384..0d4fb09 100644
--- a/internal/http/graphql.go
+++ b/internal/http/graphql.go
@@ -51,7 +51,6 @@ func (r *GraphQLErrorResponse) IsNotFound() bool {
 
 // IsRetryableError determines if the error is due to a server timeout, or another error that we might want to retry.
 func (r *GraphQLErrorResponse) IsRetryableError() bool {
-
 	if len(r.Errors) == 0 {
 		return false
 	}
@@ -87,6 +86,10 @@ func (r *GraphQLErrorResponse) IsUnauthorized(resp *http.Response) bool {
 	return false
 }
 
+func (r *GraphQLErrorResponse) IsPaymentRequired(resp *http.Response) bool {
+	return resp.StatusCode == http.StatusPaymentRequired
+}
+
 // IsDeprecated parses error messages for warnings that a field being used
 // is deprecated.  We want to bubble that up, but not stop returning data
 //
diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go
index a94d4c4..5a777d2 100644
--- a/pkg/errors/errors.go
+++ b/pkg/errors/errors.go
@@ -143,3 +143,23 @@ func (e *InvalidInput) Error() string {
 
 	return e.err
 }
+
+// PaymentRequiredError is returned when a 402 HTTP status code is returned
+// from New Relic's APIs.
+type PaymentRequiredError struct {
+	err        string
+	statusCode int
+}
+
+func (e *PaymentRequiredError) Error() string {
+	return e.err
+}
+
+// NewPaymentRequiredError returns a new instance of PaymentRequiredError
+// with an optional custom message.
+func NewPaymentRequiredError() *PaymentRequiredError {
+	return &PaymentRequiredError{
+		err:        http.StatusText(http.StatusPaymentRequired),
+		statusCode: http.StatusPaymentRequired,
+	}
+}
diff --git a/pkg/infrastructure/infrastructure.go b/pkg/infrastructure/infrastructure.go
index 13ca0b7..98118cd 100644
--- a/pkg/infrastructure/infrastructure.go
+++ b/pkg/infrastructure/infrastructure.go
@@ -53,3 +53,7 @@ func (e *ErrorResponse) IsDeprecated() bool {
 func (e *ErrorResponse) IsUnauthorized(resp *http.Response) bool {
 	return resp.StatusCode == http.StatusUnauthorized
 }
+
+func (e *ErrorResponse) IsPaymentRequired(resp *http.Response) bool {
+	return resp.StatusCode == http.StatusPaymentRequired
+}


Test patch:
diff --git a/internal/http/client_test.go b/internal/http/client_test.go
index d55f7af..4769a20 100644
--- a/internal/http/client_test.go
+++ b/internal/http/client_test.go
@@ -129,6 +129,10 @@ func (c *CustomErrorResponse) IsUnauthorized(resp *http.Response) bool {
 	return resp.StatusCode == http.StatusUnauthorized
 }
 
+func (c *CustomErrorResponse) IsPaymentRequired(resp *http.Response) bool {
+	return resp.StatusCode == http.StatusPaymentRequired
+}
+
 func TestCustomErrorValue(t *testing.T) {
 	t.Parallel()
 	c := NewTestAPIClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -445,3 +449,14 @@ func TestDelete(t *testing.T) {
 
 	assert.NoError(t, err)
 }
+
+func TestPaymentRequiredError(t *testing.T) {
+	t.Parallel()
+	c := NewTestAPIClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusPaymentRequired)
+	}))
+
+	_, err := c.Get(c.config.Region().RestURL("path"), nil, nil)
+
+	assert.IsType(t, &errors.PaymentRequiredError{}, err)
+}
diff --git a/pkg/entities/entity_integration_test.go b/pkg/entities/entity_integration_test.go
index eae71cb..d08a666 100644
--- a/pkg/entities/entity_integration_test.go
+++ b/pkg/entities/entity_integration_test.go
@@ -201,14 +201,19 @@ func TestIntegrationGetEntity_BrowserEntity(t *testing.T) {
 	}
 	require.NotNil(t, result)
 
-	actual := (*result).(*BrowserApplicationEntity)
+	ref := *result
+	actual, ok := ref.(*BrowserApplicationEntity)
+
+	if actual == nil || !ok {
+		t.Skip("Skipping `TestIntegrationGetEntity_BrowserEntity` integration test due to missing test entity. This entity might have been deleted from this test account.")
+		return
+	}
 
 	// These are a bit fragile, if the above GUID ever changes...
 	// from BrowserApplicationEntity / BrowserApplicationEntityOutline
 	assert.Equal(t, 204261628, actual.ApplicationID)
 	assert.Equal(t, 204261368, actual.ServingApmApplicationID)
 	assert.Equal(t, EntityAlertSeverityTypes.NOT_CONFIGURED, actual.AlertSeverity)
-
 }
 
 // Looking at a Mobile Application, and the result set here.
diff --git a/pkg/entities/tags_integration_test.go b/pkg/entities/tags_integration_test.go
index fb0a059..60d204b 100644
--- a/pkg/entities/tags_integration_test.go
+++ b/pkg/entities/tags_integration_test.go
@@ -15,8 +15,8 @@ func TestIntegrationListTags(t *testing.T) {
 	t.Parallel()
 
 	var (
-		// GUID of WebPortal
-		testGUID = common.EntityGUID("MjUwODI1OXxBUE18QVBQTElDQVRJT058MjA0MjYxMzY4")
+		// GUID of Dummy App
+		testGUID = common.EntityGUID("MjUyMDUyOHxBUE18QVBQTElDQVRJT058MjE1MDM3Nzk1")
 	)
 
 	client := newIntegrationTestClient(t)
diff --git a/pkg/plugins/components_integration_test.go b/pkg/plugins/components_integration_test.go
index 31ca53f..6269d35 100644
--- a/pkg/plugins/components_integration_test.go
+++ b/pkg/plugins/components_integration_test.go
@@ -12,6 +12,7 @@ import (
 )
 
 func TestIntegrationComponents(t *testing.T) {
+	t.Skip("Skipping `GetComponent` integration test due API reaching EOL (returns 410 HTTP status)")
 	t.Parallel()
 
 	tc := mock.NewIntegrationTestConfig(t)
diff --git a/pkg/plugins/plugins_integration_test.go b/pkg/plugins/plugins_integration_test.go
index 804d6fa..9ebcd0b 100644
--- a/pkg/plugins/plugins_integration_test.go
+++ b/pkg/plugins/plugins_integration_test.go
@@ -12,6 +12,8 @@ import (
 )
 
 func TestIntegrationPlugins(t *testing.T) {
+	t.Skip("Skipping `Plugins` integration test due API reaching EOL (returns 410 HTTP status)")
+
 	t.Parallel()
 
 	tc := mock.NewIntegrationTestConfig(t)