From 9ba6e7020000682877d8fd953c9059822872d7e7 Mon Sep 17 00:00:00 2001 From: Junpei Tsuji Date: Thu, 20 Feb 2020 14:03:07 +0900 Subject: [PATCH 1/2] Handle http status 5xx error --- appstore/validator.go | 57 ++++++++++++++--------- appstore/validator_test.go | 93 ++++++++++++++++++++------------------ 2 files changed, 85 insertions(+), 65 deletions(-) diff --git a/appstore/validator.go b/appstore/validator.go index f68f2b6..dee06bc 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io/ioutil" "net/http" "time" @@ -33,47 +34,53 @@ type Client struct { httpCli *http.Client } +var ( + ErrAppStoreServer = errors.New("AppStore server error") + + ErrInvalidJSON = errors.New("The App Store could not read the JSON object you provided.") + ErrInvalidReceiptData = errors.New("The data in the receipt-data property was malformed or missing.") + ErrReceiptUnauthenticated = errors.New("The receipt could not be authenticated.") + ErrInvalidSharedSecret = errors.New("The shared secret you provided does not match the shared secret on file for your account.") + ErrServerUnavailable = errors.New("The receipt server is not currently available.") + ErrReceiptIsForTest = errors.New("This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.") + ErrReceiptIsForProduction = errors.New("This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.") + ErrReceiptUnauthorized = errors.New("This receipt could not be authorized. Treat this the same as if a purchase was never made.") + + ErrInternalDataAccessError = errors.New("Internal data access error.") + ErrUnknown = errors.New("An unknown error occurred") +) + // HandleError returns error message by status code func HandleError(status int) error { - var message string - + var e error switch status { case 0: return nil - case 21000: - message = "The App Store could not read the JSON object you provided." - + e = ErrInvalidJSON case 21002: - message = "The data in the receipt-data property was malformed or missing." - + e = ErrInvalidReceiptData case 21003: - message = "The receipt could not be authenticated." - + e = ErrReceiptUnauthenticated case 21004: - message = "The shared secret you provided does not match the shared secret on file for your account." - + e = ErrInvalidSharedSecret case 21005: - message = "The receipt server is not currently available." - + e = ErrServerUnavailable case 21007: - message = "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead." - + e = ErrReceiptIsForTest case 21008: - message = "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead." - + e = ErrReceiptIsForProduction case 21010: - message = "This receipt could not be authorized. Treat this the same as if a purchase was never made." - + e = ErrReceiptUnauthorized default: if status >= 21100 && status <= 21199 { - message = "Internal data access error." + e = ErrInternalDataAccessError } else { - message = "An unknown error occurred" + e = ErrUnknown } } - return errors.New(message) + return fmt.Errorf("status %d: %w", status, e) } // New creates a client object @@ -115,6 +122,9 @@ func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interfac return err } defer resp.Body.Close() + if resp.StatusCode >= 500 { + return fmt.Errorf("Received http status code %d from the App Store: %w", resp.StatusCode, ErrAppStoreServer) + } return c.parseResponse(resp, result, ctx, reqBody) } @@ -153,6 +163,9 @@ func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx cont return err } defer resp.Body.Close() + if resp.StatusCode >= 500 { + return fmt.Errorf("Received http status code %d from the App Store Sandbox: %w", resp.StatusCode, ErrAppStoreServer) + } return json.NewDecoder(resp.Body).Decode(result) } diff --git a/appstore/validator_test.go b/appstore/validator_test.go index 0ba92e8..2981cb9 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -26,52 +26,52 @@ func TestHandleError(t *testing.T) { { name: "status 21000", in: 21000, - out: errors.New("The App Store could not read the JSON object you provided."), + out: ErrInvalidJSON, }, { name: "status 21002", in: 21002, - out: errors.New("The data in the receipt-data property was malformed or missing."), + out: ErrInvalidReceiptData, }, { name: "status 21003", in: 21003, - out: errors.New("The receipt could not be authenticated."), + out: ErrReceiptUnauthenticated, }, { name: "status 21004", in: 21004, - out: errors.New("The shared secret you provided does not match the shared secret on file for your account."), + out: ErrInvalidSharedSecret, }, { name: "status 21005", in: 21005, - out: errors.New("The receipt server is not currently available."), + out: ErrServerUnavailable, }, { name: "status 21007", in: 21007, - out: errors.New("This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead."), + out: ErrReceiptIsForTest, }, { name: "status 21008", in: 21008, - out: errors.New("This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead."), + out: ErrReceiptIsForProduction, }, { name: "status 21010", in: 21010, - out: errors.New("This receipt could not be authorized. Treat this the same as if a purchase was never made."), + out: ErrReceiptUnauthorized, }, { name: "status 21100 ~ 21199", in: 21100, - out: errors.New("Internal data access error."), + out: ErrInternalDataAccessError, }, { name: "status unknown", in: 100, - out: errors.New("An unknown error occurred"), + out: ErrUnknown, }, } @@ -79,7 +79,7 @@ func TestHandleError(t *testing.T) { t.Run(v.name, func(t *testing.T) { out := HandleError(v.in) - if !reflect.DeepEqual(out, v.out) { + if !errors.Is(out, v.out) { t.Errorf("input: %d\ngot: %v\nwant: %v\n", v.in, out, v.out) } }) @@ -180,29 +180,30 @@ func TestResponses(t *testing.T) { result := &IAPResponse{} type testCase struct { + name string testServer *httptest.Server sandboxServ *httptest.Server expected *IAPResponse } testCases := []testCase{ - // VerifySandboxReceipt { + name: "VerifySandboxReceipt", testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)), sandboxServ: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)), expected: &IAPResponse{ Status: 0, }, }, - // VerifyBadPayload { + name: "VerifyBadPayload", testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21002}`)), expected: &IAPResponse{ Status: 21002, }, }, - // SuccessPayload { + name: "SuccessPayload", testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 0}`)), expected: &IAPResponse{ Status: 0, @@ -213,57 +214,65 @@ func TestResponses(t *testing.T) { client := New() client.SandboxURL = "localhost" - for i, tc := range testCases { - defer tc.testServer.Close() - client.ProductionURL = tc.testServer.URL - if tc.sandboxServ != nil { - client.SandboxURL = tc.sandboxServ.URL - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer tc.testServer.Close() + client.ProductionURL = tc.testServer.URL + if tc.sandboxServ != nil { + client.SandboxURL = tc.sandboxServ.URL + } - ctx := context.Background() - err := client.Verify(ctx, req, result) - if err != nil { - t.Errorf("Test case %d - %s", i, err.Error()) - } - if !reflect.DeepEqual(result, tc.expected) { - t.Errorf("Test case %d - got %v\nwant %v", i, result, tc.expected) - } + ctx := context.Background() + err := client.Verify(ctx, req, result) + if err != nil { + t.Errorf("%s", err) + } + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("got %v\nwant %v", result, tc.expected) + } + }) } } -func TestErrors(t *testing.T) { +func TestHttpStatusErrors(t *testing.T) { req := IAPRequest{ ReceiptData: "dummy data", } result := &IAPResponse{} type testCase struct { + name string testServer *httptest.Server + err error } testCases := []testCase{ - // VerifySandboxReceiptFailure { - testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)), + name: "status 200", + testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21000}`)), + err: nil, }, - // VerifyBadResponse { + name: "status 500", testServer: httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)), + err: ErrAppStoreServer, }, } client := New() client.SandboxURL = "localhost" - for i, tc := range testCases { - defer tc.testServer.Close() - client.ProductionURL = tc.testServer.URL + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer tc.testServer.Close() + client.ProductionURL = tc.testServer.URL - ctx := context.Background() - err := client.Verify(ctx, req, result) - if err == nil { - t.Errorf("Test case %d - expected error to be not nil since the sandbox is not responding", i) - } + ctx := context.Background() + err := client.Verify(ctx, req, result) + if !errors.Is(err, tc.err) { + t.Errorf("expected error to be not nil since the sandbox is not responding") + } + }) } } @@ -297,12 +306,10 @@ func serverWithResponse(statusCode int, response string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if "POST" == r.Method { w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) w.Write([]byte(response)) - return } else { w.Write([]byte(`unsupported request`)) } - - w.WriteHeader(statusCode) }) } From dd8af1ccd21b7c18a80d4649e8c3350243ceac06 Mon Sep 17 00:00:00 2001 From: Junpei Tsuji Date: Thu, 20 Feb 2020 14:10:28 +0900 Subject: [PATCH 2/2] Update go 1.13 --- .travis.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a085821..cc7ad5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: -- 1.12.5 +- 1.13.x env: global: - GO111MODULE=on diff --git a/README.md b/README.md index 282a117..36590aa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ go-iap ====== -![](https://img.shields.io/badge/golang-1.12-blue.svg?style=flat) +![](https://img.shields.io/badge/golang-1.13-blue.svg?style=flat) [![Build Status](https://travis-ci.org/awa/go-iap.svg?branch=master)](https://travis-ci.org/awa/go-iap) [![codecov.io](https://codecov.io/github/awa/go-iap/coverage.svg?branch=master)](https://codecov.io/github/awa/go-iap?branch=master)