From 60800c1f901f78af739b89551e80f6ccd56f16d5 Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 11 Dec 2017 17:05:53 -0500 Subject: [PATCH 1/9] Have app store hit prod and then sandbox if 21007 According to https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL Always verify your receipt first with the production URL; proceed to verify with the sandbox URL if you receive a 21007 status code. Following this approach ensures that you do not have to switch between URLs while your application is being tested or reviewed in the sandbox or is live in the App Store. --- appstore/validator.go | 24 ++++- appstore/validator_test.go | 179 +++++++++++++++++++++++++++++++++++-- 2 files changed, 194 insertions(+), 9 deletions(-) diff --git a/appstore/validator.go b/appstore/validator.go index 31c9930..02e4922 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -31,6 +31,7 @@ type IAPClient interface { type Client struct { URL string TimeOut time.Duration + SandboxURL string } // HandleError returns error message by status code @@ -81,6 +82,7 @@ func New() Client { client := Client{ URL: SandboxURL, TimeOut: time.Second * 5, + SandboxURL: SandboxURL, } if os.Getenv("IAP_ENVIRONMENT") == "production" { client.URL = ProductionURL @@ -121,6 +123,26 @@ func (c *Client) Verify(req IAPRequest, result interface{}) error { defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(result) + if err != nil { + return err + } - return err + // Always verify your receipt first with the production URL; proceed to verify with the sandbox URL if you receive + // a 21007 status code + // + // https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL + r, ok := result.(*IAPResponse) + if ok && r.Status == 21007 { + b = new(bytes.Buffer) + json.NewEncoder(b).Encode(req) + resp, err := client.Post(c.SandboxURL, "application/json; charset=utf-8", b) + if err != nil { + return err + } + defer resp.Body.Close() + + return json.NewDecoder(resp.Body).Decode(result) + } + + return nil } diff --git a/appstore/validator_test.go b/appstore/validator_test.go index 52436c1..aa78990 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -6,6 +6,8 @@ import ( "reflect" "testing" "time" + "net/http/httptest" + "net/http" ) func TestHandleError(t *testing.T) { @@ -91,8 +93,9 @@ func TestHandleError(t *testing.T) { func TestNew(t *testing.T) { expected := Client{ - URL: "https://sandbox.itunes.apple.com/verifyReceipt", + URL: SandboxURL, TimeOut: time.Second * 5, + SandboxURL:SandboxURL, } actual := New() @@ -103,8 +106,9 @@ func TestNew(t *testing.T) { func TestNewWithEnvironment(t *testing.T) { expected := Client{ - URL: "https://buy.itunes.apple.com/verifyReceipt", + URL: ProductionURL, TimeOut: time.Second * 5, + SandboxURL:SandboxURL, } os.Setenv("IAP_ENVIRONMENT", "production") @@ -123,7 +127,7 @@ func TestNewWithConfig(t *testing.T) { } expected := Client{ - URL: "https://buy.itunes.apple.com/verifyReceipt", + URL: ProductionURL, TimeOut: time.Second * 2, } @@ -139,7 +143,7 @@ func TestNewWithConfigTimeout(t *testing.T) { } expected := Client{ - URL: "https://buy.itunes.apple.com/verifyReceipt", + URL: ProductionURL, TimeOut: time.Second * 5, } @@ -149,9 +153,9 @@ func TestNewWithConfigTimeout(t *testing.T) { } } -func TestVerify(t *testing.T) { +func TestVerifyTimeout(t *testing.T) { client := New() - client.TimeOut = time.Millisecond * 100 + client.TimeOut = time.Millisecond req := IAPRequest{ ReceiptData: "dummy data", @@ -161,13 +165,172 @@ func TestVerify(t *testing.T) { if err == nil { t.Errorf("error should be occurred because of timeout") } +} - client = New() +func TestVerifyBadURL(t *testing.T) { + client := New() + client.URL = "127.0.0.1" + + req := IAPRequest{ + ReceiptData: "dummy data", + } + result := &IAPResponse{} + err := client.Verify(req, result) + if err == nil { + t.Errorf("error should be occurred because the server is not real") + } +} + +func TestVerifyBadPayload(t *testing.T) { + s := httptest.NewServer(badPayload()) + defer s.Close() + + client := New() + client.URL = s.URL expected := &IAPResponse{ Status: 21002, } - client.Verify(req, result) + req := IAPRequest{ + ReceiptData: "dummy data", + } + result := &IAPResponse{} + + err := client.Verify(req, result) + if err != nil { + t.Errorf("got error %s", err) + } if !reflect.DeepEqual(result, expected) { t.Errorf("got %v\nwant %v", result, expected) } } + +func TestVerifyBadResponse(t *testing.T) { + s := httptest.NewServer(invalidResponse()) + defer s.Close() + + client := New() + client.URL = s.URL + req := IAPRequest{ + ReceiptData: "dummy data", + } + result := &IAPResponse{} + + err := client.Verify(req, result) + if err == nil { + t.Errorf("expected an error because Verify could not unmarshal server response") + } +} + +func TestVerifySandboxReceipt(t *testing.T) { + s := httptest.NewServer(redirectToSandbox()) + defer s.Close() + + sandboxServ := httptest.NewServer(sandboxSuccess()) + defer sandboxServ.Close() + + client := New() + client.URL = s.URL + client.TimeOut = time.Second * 100 + client.SandboxURL = sandboxServ.URL + + expected := &IAPResponse{ + Status: 0, + } + req := IAPRequest{ + ReceiptData: "dummy data", + } + result := &IAPResponse{} + + err := client.Verify(req, result) + if err != nil { + t.Errorf("got error %s", err) + } + if !reflect.DeepEqual(result, expected) { + t.Errorf("got %v\nwant %v", result, expected) + } +} + +func TestVerifySandboxReceiptFailure(t *testing.T) { + s := httptest.NewServer(redirectToSandbox()) + defer s.Close() + + sandboxServ := httptest.NewServer(sandboxTimeout()) + defer sandboxServ.Close() + + client := New() + client.URL = s.URL + client.TimeOut = time.Second * 100 + client.SandboxURL = sandboxServ.URL + + req := IAPRequest{ + ReceiptData: "dummy data", + } + result := &IAPResponse{} + + err := client.Verify(req, result) + if err == nil { + t.Errorf("expected error to be not nil since the sandbox is not responding") + } +} + +func badPayload() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if "POST" == r.Method { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status": 21002}`)) + return + } else { + w.Write([]byte(`unsupported request`)) + } + + w.WriteHeader(http.StatusBadRequest) + }) +} + +func redirectToSandbox() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if "POST" == r.Method { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status": 21007}`)) + return + } else { + w.Write([]byte(`unsupported request`)) + } + + w.WriteHeader(http.StatusOK) + }) +} + +func sandboxSuccess() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if "POST" == r.Method { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status": 0}`)) + return + } else { + w.Write([]byte(`unsupported request`)) + } + + w.WriteHeader(http.StatusOK) + }) +} + +func sandboxTimeout() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do nothing and just dont return anything either + }) +} + +func invalidResponse() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if "POST" == r.Method { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`qwerty!@#$%^`)) + return + } else { + w.Write([]byte(`unsupported request`)) + } + + w.WriteHeader(http.StatusOK) + }) +} From 8b1c5e29d2e20f13263a001f8286b80aba68d515 Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 11 Dec 2017 17:12:38 -0500 Subject: [PATCH 2/9] Bring coverage up to 100 Remove sandbox timeout to actually cause POST to fail. --- appstore/validator_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index aa78990..b6829d7 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -254,13 +254,9 @@ func TestVerifySandboxReceiptFailure(t *testing.T) { s := httptest.NewServer(redirectToSandbox()) defer s.Close() - sandboxServ := httptest.NewServer(sandboxTimeout()) - defer sandboxServ.Close() - client := New() client.URL = s.URL client.TimeOut = time.Second * 100 - client.SandboxURL = sandboxServ.URL req := IAPRequest{ ReceiptData: "dummy data", @@ -315,12 +311,6 @@ func sandboxSuccess() http.Handler { }) } -func sandboxTimeout() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Do nothing and just dont return anything either - }) -} - func invalidResponse() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if "POST" == r.Method { From 12a6e01874a1b7c80ccdfae57fe03b1d004b1e7d Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 11 Dec 2017 17:20:32 -0500 Subject: [PATCH 3/9] Correct test Needed to set an invalid SandboxURL to trigger the err. --- appstore/validator_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index b6829d7..f6a338a 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -257,6 +257,7 @@ func TestVerifySandboxReceiptFailure(t *testing.T) { client := New() client.URL = s.URL client.TimeOut = time.Second * 100 + client.SandboxURL = "localhost" req := IAPRequest{ ReceiptData: "dummy data", From 132768c0fe81e5727f6115f37bf7699f3ff97dad Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Wed, 17 Jan 2018 16:02:52 -0500 Subject: [PATCH 4/9] Use new struct to check status & Remove IAP_ENVIRONMENT Use a new struct called `HttpStatusResponse` to check the status code returned in the receipt. This is ios-type independent and will redirect the receipt to sandbox if needed. Following Apple's recommendation, always hit production and then sandbox. There is no more need to specify which environment to hit. --- appstore/model.go | 8 +++++++- appstore/validator.go | 34 +++++++++++-------------------- appstore/validator_test.go | 41 ++++++++++++++++++-------------------- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/appstore/model.go b/appstore/model.go index 37d7a25..9f45d4f 100644 --- a/appstore/model.go +++ b/appstore/model.go @@ -102,7 +102,7 @@ type ( // We defined each field by the current IAP response, but some fields are not mentioned // in the following Apple's document; // https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html - // If you get other types or fileds from the IAP response, you should use the struct you defined. + // If you get other types or fields from the IAP response, you should use the struct you defined. IAPResponse struct { Status int `json:"status"` Environment string `json:"environment"` @@ -112,4 +112,10 @@ type ( PendingRenewalInfo []PendingRenewalInfo `json:"pending_renewal_info"` IsRetryable bool `json:"is-retryable"` } + + // The HttpStatusResponse struct contains the status code returned by the store + // Used as a workaround to detect when to hit the production appstore or sandbox appstore regardless of receipt type + HttpStatusResponse struct { + Status int `json:"status"` + } ) diff --git a/appstore/validator.go b/appstore/validator.go index 02e4922..daaacbb 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "net/http" - "os" "time" ) @@ -18,8 +17,7 @@ const ( // Config is a configuration to initialize client type Config struct { - IsProduction bool - TimeOut time.Duration + TimeOut time.Duration } // IAPClient is an interface to call validation API in App Store @@ -29,9 +27,9 @@ type IAPClient interface { // Client implements IAPClient type Client struct { - URL string - TimeOut time.Duration - SandboxURL string + ProductionURL string + SandboxURL string + TimeOut time.Duration } // HandleError returns error message by status code @@ -80,12 +78,9 @@ func HandleError(status int) error { // New creates a client object func New() Client { client := Client{ - URL: SandboxURL, - TimeOut: time.Second * 5, - SandboxURL: SandboxURL, - } - if os.Getenv("IAP_ENVIRONMENT") == "production" { - client.URL = ProductionURL + ProductionURL: ProductionURL, + SandboxURL: SandboxURL, + TimeOut: time.Second * 5, } return client } @@ -97,11 +92,9 @@ func NewWithConfig(config Config) Client { } client := Client{ - URL: SandboxURL, - TimeOut: config.TimeOut, - } - if config.IsProduction { - client.URL = ProductionURL + ProductionURL: ProductionURL, + SandboxURL: SandboxURL, + TimeOut: config.TimeOut, } return client @@ -116,7 +109,7 @@ func (c *Client) Verify(req IAPRequest, result interface{}) error { b := new(bytes.Buffer) json.NewEncoder(b).Encode(req) - resp, err := client.Post(c.URL, "application/json; charset=utf-8", b) + resp, err := client.Post(c.ProductionURL, "application/json; charset=utf-8", b) if err != nil { return err } @@ -127,11 +120,8 @@ func (c *Client) Verify(req IAPRequest, result interface{}) error { return err } - // Always verify your receipt first with the production URL; proceed to verify with the sandbox URL if you receive - // a 21007 status code - // // https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL - r, ok := result.(*IAPResponse) + r, ok := result.(*HttpStatusResponse) if ok && r.Status == 21007 { b = new(bytes.Buffer) json.NewEncoder(b).Encode(req) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index f6a338a..2b2f831 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -2,12 +2,12 @@ package appstore import ( "errors" + "net/http" + "net/http/httptest" "os" "reflect" "testing" "time" - "net/http/httptest" - "net/http" ) func TestHandleError(t *testing.T) { @@ -93,9 +93,9 @@ func TestHandleError(t *testing.T) { func TestNew(t *testing.T) { expected := Client{ - URL: SandboxURL, - TimeOut: time.Second * 5, - SandboxURL:SandboxURL, + ProductionURL: SandboxURL, + TimeOut: time.Second * 5, + SandboxURL: SandboxURL, } actual := New() @@ -106,9 +106,9 @@ func TestNew(t *testing.T) { func TestNewWithEnvironment(t *testing.T) { expected := Client{ - URL: ProductionURL, - TimeOut: time.Second * 5, - SandboxURL:SandboxURL, + ProductionURL: ProductionURL, + TimeOut: time.Second * 5, + SandboxURL: SandboxURL, } os.Setenv("IAP_ENVIRONMENT", "production") @@ -122,13 +122,12 @@ func TestNewWithEnvironment(t *testing.T) { func TestNewWithConfig(t *testing.T) { config := Config{ - IsProduction: true, - TimeOut: time.Second * 2, + TimeOut: time.Second * 2, } expected := Client{ - URL: ProductionURL, - TimeOut: time.Second * 2, + ProductionURL: ProductionURL, + TimeOut: time.Second * 2, } actual := NewWithConfig(config) @@ -138,13 +137,11 @@ func TestNewWithConfig(t *testing.T) { } func TestNewWithConfigTimeout(t *testing.T) { - config := Config{ - IsProduction: true, - } + config := Config{} expected := Client{ - URL: ProductionURL, - TimeOut: time.Second * 5, + ProductionURL: ProductionURL, + TimeOut: time.Second * 5, } actual := NewWithConfig(config) @@ -169,7 +166,7 @@ func TestVerifyTimeout(t *testing.T) { func TestVerifyBadURL(t *testing.T) { client := New() - client.URL = "127.0.0.1" + client.ProductionURL = "127.0.0.1" req := IAPRequest{ ReceiptData: "dummy data", @@ -186,7 +183,7 @@ func TestVerifyBadPayload(t *testing.T) { defer s.Close() client := New() - client.URL = s.URL + client.ProductionURL = s.URL expected := &IAPResponse{ Status: 21002, } @@ -209,7 +206,7 @@ func TestVerifyBadResponse(t *testing.T) { defer s.Close() client := New() - client.URL = s.URL + client.ProductionURL = s.URL req := IAPRequest{ ReceiptData: "dummy data", } @@ -229,7 +226,7 @@ func TestVerifySandboxReceipt(t *testing.T) { defer sandboxServ.Close() client := New() - client.URL = s.URL + client.ProductionURL = s.URL client.TimeOut = time.Second * 100 client.SandboxURL = sandboxServ.URL @@ -255,7 +252,7 @@ func TestVerifySandboxReceiptFailure(t *testing.T) { defer s.Close() client := New() - client.URL = s.URL + client.ProductionURL = s.URL client.TimeOut = time.Second * 100 client.SandboxURL = "localhost" From fe51698284efb50437c6554ed4fd409131e485f5 Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Wed, 17 Jan 2018 16:47:14 -0500 Subject: [PATCH 5/9] Fix bug involving HttpStatusResponse the `ok` in `r, ok := result.(*HttpStatusResponse)` would always fail unless the `result` being passed in was also of type `HttpStatusResponse` --- appstore/model.go | 2 +- appstore/validator.go | 17 ++++++++++++++--- appstore/validator_test.go | 6 ++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/appstore/model.go b/appstore/model.go index 9f45d4f..a4c3476 100644 --- a/appstore/model.go +++ b/appstore/model.go @@ -115,7 +115,7 @@ type ( // The HttpStatusResponse struct contains the status code returned by the store // Used as a workaround to detect when to hit the production appstore or sandbox appstore regardless of receipt type - HttpStatusResponse struct { + StatusResponse struct { Status int `json:"status"` } ) diff --git a/appstore/validator.go b/appstore/validator.go index daaacbb..42eeba0 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "io/ioutil" "net/http" "time" ) @@ -115,14 +116,24 @@ func (c *Client) Verify(req IAPRequest, result interface{}) error { } defer resp.Body.Close() - err = json.NewDecoder(resp.Body).Decode(result) + // Read the body now so that we can unmarshal it twice + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(buf, &result) if err != nil { return err } // https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL - r, ok := result.(*HttpStatusResponse) - if ok && r.Status == 21007 { + var r StatusResponse + err = json.Unmarshal(buf, &r) + if err != nil { + return err + } + if r.Status == 21007 { b = new(bytes.Buffer) json.NewEncoder(b).Encode(req) resp, err := client.Post(c.SandboxURL, "application/json; charset=utf-8", b) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index 2b2f831..8e21cf0 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -93,9 +93,9 @@ func TestHandleError(t *testing.T) { func TestNew(t *testing.T) { expected := Client{ - ProductionURL: SandboxURL, - TimeOut: time.Second * 5, + ProductionURL: ProductionURL, SandboxURL: SandboxURL, + TimeOut: time.Second * 5, } actual := New() @@ -127,6 +127,7 @@ func TestNewWithConfig(t *testing.T) { expected := Client{ ProductionURL: ProductionURL, + SandboxURL: SandboxURL, TimeOut: time.Second * 2, } @@ -141,6 +142,7 @@ func TestNewWithConfigTimeout(t *testing.T) { expected := Client{ ProductionURL: ProductionURL, + SandboxURL: SandboxURL, TimeOut: time.Second * 5, } From 6d0084274323bc9418bf6ecacb75ce5e673878d5 Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 22 Jan 2018 15:40:28 -0500 Subject: [PATCH 6/9] Bring tests back up to 100% Make a new method called parseResponse which essentially allows us to feed it our own response so that we can feed it bad body so that it cannot unmarshal or even read it. --- appstore/validator.go | 5 ++- appstore/validator_test.go | 82 +++++++++++++++----------------------- 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/appstore/validator.go b/appstore/validator.go index 42eeba0..731109c 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -115,7 +115,10 @@ func (c *Client) Verify(req IAPRequest, result interface{}) error { return err } defer resp.Body.Close() + return c.parseResponse(resp, result, client, req) +} +func (c *Client) parseResponse(resp *http.Response, result interface{}, client http.Client, req IAPRequest) error { // Read the body now so that we can unmarshal it twice buf, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -134,7 +137,7 @@ func (c *Client) Verify(req IAPRequest, result interface{}) error { return err } if r.Status == 21007 { - b = new(bytes.Buffer) + b := new(bytes.Buffer) json.NewEncoder(b).Encode(req) resp, err := client.Post(c.SandboxURL, "application/json; charset=utf-8", b) if err != nil { diff --git a/appstore/validator_test.go b/appstore/validator_test.go index 8e21cf0..c0df87f 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -2,10 +2,12 @@ package appstore import ( "errors" + "io/ioutil" "net/http" "net/http/httptest" "os" "reflect" + "strings" "testing" "time" ) @@ -181,7 +183,7 @@ func TestVerifyBadURL(t *testing.T) { } func TestVerifyBadPayload(t *testing.T) { - s := httptest.NewServer(badPayload()) + s := httptest.NewServer(serverWithResponse(`{"status": 21002}`)) defer s.Close() client := New() @@ -204,7 +206,7 @@ func TestVerifyBadPayload(t *testing.T) { } func TestVerifyBadResponse(t *testing.T) { - s := httptest.NewServer(invalidResponse()) + s := httptest.NewServer(serverWithResponse(`qwerty!@#$%^`)) defer s.Close() client := New() @@ -221,10 +223,10 @@ func TestVerifyBadResponse(t *testing.T) { } func TestVerifySandboxReceipt(t *testing.T) { - s := httptest.NewServer(redirectToSandbox()) + s := httptest.NewServer(serverWithResponse(`{"status": 21007}`)) defer s.Close() - sandboxServ := httptest.NewServer(sandboxSuccess()) + sandboxServ := httptest.NewServer(serverWithResponse(`{"status": 0}`)) defer sandboxServ.Close() client := New() @@ -250,7 +252,7 @@ func TestVerifySandboxReceipt(t *testing.T) { } func TestVerifySandboxReceiptFailure(t *testing.T) { - s := httptest.NewServer(redirectToSandbox()) + s := httptest.NewServer(serverWithResponse(`{"status": 21007}`)) defer s.Close() client := New() @@ -269,11 +271,35 @@ func TestVerifySandboxReceiptFailure(t *testing.T) { } } -func badPayload() http.Handler { +func TestCannotReadBody(t *testing.T) { + client := New() + testResponse := http.Response{Body: ioutil.NopCloser(errReader(0))} + + if client.parseResponse(&testResponse, IAPResponse{}, http.Client{}, IAPRequest{}) == nil { + t.Errorf("expected redirectToSandbox to fail to read the body") + } +} + +func TestCannotUnmarshalBody(t *testing.T) { + client := New() + testResponse := http.Response{Body: ioutil.NopCloser(strings.NewReader(`{"status": true}`))} + + if client.parseResponse(&testResponse, StatusResponse{}, http.Client{}, IAPRequest{}) == nil { + t.Errorf("expected redirectToSandbox to fail to unmarshal the data") + } +} + +type errReader int + +func (errReader) Read(p []byte) (n int, err error) { + return 0, errors.New("test error") +} + +func serverWithResponse(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.Write([]byte(`{"status": 21002}`)) + w.Write([]byte(response)) return } else { w.Write([]byte(`unsupported request`)) @@ -282,45 +308,3 @@ func badPayload() http.Handler { w.WriteHeader(http.StatusBadRequest) }) } - -func redirectToSandbox() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if "POST" == r.Method { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"status": 21007}`)) - return - } else { - w.Write([]byte(`unsupported request`)) - } - - w.WriteHeader(http.StatusOK) - }) -} - -func sandboxSuccess() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if "POST" == r.Method { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"status": 0}`)) - return - } else { - w.Write([]byte(`unsupported request`)) - } - - w.WriteHeader(http.StatusOK) - }) -} - -func invalidResponse() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if "POST" == r.Method { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`qwerty!@#$%^`)) - return - } else { - w.Write([]byte(`unsupported request`)) - } - - w.WriteHeader(http.StatusOK) - }) -} From 594d8d65448439cdf84e397b17162437a86ae3f4 Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 22 Jan 2018 17:17:05 -0500 Subject: [PATCH 7/9] Correct tests serverWithResponse was hardcoded to return http.StatusBadRequest whereas it differed in the tests in the previous commits. This has been corrected by turning it into a parameter. --- appstore/validator_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index c0df87f..696257a 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -183,7 +183,7 @@ func TestVerifyBadURL(t *testing.T) { } func TestVerifyBadPayload(t *testing.T) { - s := httptest.NewServer(serverWithResponse(`{"status": 21002}`)) + s := httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 21002}`)) defer s.Close() client := New() @@ -206,7 +206,7 @@ func TestVerifyBadPayload(t *testing.T) { } func TestVerifyBadResponse(t *testing.T) { - s := httptest.NewServer(serverWithResponse(`qwerty!@#$%^`)) + s := httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)) defer s.Close() client := New() @@ -223,10 +223,10 @@ func TestVerifyBadResponse(t *testing.T) { } func TestVerifySandboxReceipt(t *testing.T) { - s := httptest.NewServer(serverWithResponse(`{"status": 21007}`)) + s := httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)) defer s.Close() - sandboxServ := httptest.NewServer(serverWithResponse(`{"status": 0}`)) + sandboxServ := httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)) defer sandboxServ.Close() client := New() @@ -252,7 +252,7 @@ func TestVerifySandboxReceipt(t *testing.T) { } func TestVerifySandboxReceiptFailure(t *testing.T) { - s := httptest.NewServer(serverWithResponse(`{"status": 21007}`)) + s := httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)) defer s.Close() client := New() @@ -295,7 +295,7 @@ func (errReader) Read(p []byte) (n int, err error) { return 0, errors.New("test error") } -func serverWithResponse(response string) http.Handler { +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") @@ -305,6 +305,6 @@ func serverWithResponse(response string) http.Handler { w.Write([]byte(`unsupported request`)) } - w.WriteHeader(http.StatusBadRequest) + w.WriteHeader(statusCode) }) } From 3b95498cbec9cd2106d73315f01f996102dd26ac Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 29 Jan 2018 17:53:58 -0500 Subject: [PATCH 8/9] Add Success test case + Refactor Comments --- appstore/validator_test.go | 143 +++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index 696257a..6f9e10d 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -182,92 +182,97 @@ func TestVerifyBadURL(t *testing.T) { } } -func TestVerifyBadPayload(t *testing.T) { - s := httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 21002}`)) - defer s.Close() - - client := New() - client.ProductionURL = s.URL - expected := &IAPResponse{ - Status: 21002, - } +func TestResponses(t *testing.T) { req := IAPRequest{ ReceiptData: "dummy data", } result := &IAPResponse{} - err := client.Verify(req, result) - if err != nil { - t.Errorf("got error %s", err) + type testCase struct { + testServer *httptest.Server + sandboxServ *httptest.Server + expected *IAPResponse } - if !reflect.DeepEqual(result, expected) { - t.Errorf("got %v\nwant %v", result, expected) - } -} -func TestVerifyBadResponse(t *testing.T) { - s := httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)) - defer s.Close() + testCases := []testCase{ + // VerifySandboxReceipt + { + testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)), + sandboxServ: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)), + expected: &IAPResponse{ + Status: 0, + }, + }, + // VerifyBadPayload + { + testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 21002}`)), + expected: &IAPResponse{ + Status: 21002, + }, + }, + // SuccessPayload + { + testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 0}`)), + expected: &IAPResponse{ + Status: 0, + }, + }, + } client := New() - client.ProductionURL = s.URL - req := IAPRequest{ - ReceiptData: "dummy data", - } - result := &IAPResponse{} - - err := client.Verify(req, result) - if err == nil { - t.Errorf("expected an error because Verify could not unmarshal server response") - } -} - -func TestVerifySandboxReceipt(t *testing.T) { - s := httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)) - defer s.Close() - - sandboxServ := httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)) - defer sandboxServ.Close() - - client := New() - client.ProductionURL = s.URL - client.TimeOut = time.Second * 100 - client.SandboxURL = sandboxServ.URL - - expected := &IAPResponse{ - Status: 0, - } - req := IAPRequest{ - ReceiptData: "dummy data", - } - result := &IAPResponse{} - - err := client.Verify(req, result) - if err != nil { - t.Errorf("got error %s", err) - } - if !reflect.DeepEqual(result, expected) { - t.Errorf("got %v\nwant %v", result, expected) - } -} - -func TestVerifySandboxReceiptFailure(t *testing.T) { - s := httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)) - defer s.Close() - - client := New() - client.ProductionURL = s.URL client.TimeOut = time.Second * 100 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 + } + + err := client.Verify(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) + } + } +} + +func TestErrors(t *testing.T) { req := IAPRequest{ ReceiptData: "dummy data", } result := &IAPResponse{} - err := client.Verify(req, result) - if err == nil { - t.Errorf("expected error to be not nil since the sandbox is not responding") + type testCase struct { + testServer *httptest.Server + } + + testCases := []testCase{ + // VerifySandboxReceiptFailure + { + testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)), + }, + // VerifyBadResponse + { + testServer: httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)), + }, + } + + client := New() + client.TimeOut = time.Second * 100 + client.SandboxURL = "localhost" + + for i, tc := range testCases { + defer tc.testServer.Close() + client.ProductionURL = tc.testServer.URL + + err := client.Verify(req, result) + if err == nil { + t.Errorf("Test case %d - expected error to be not nil since the sandbox is not responding", i) + } } } From c809bb0ee2933ef03925aa742177e6823c2363d9 Mon Sep 17 00:00:00 2001 From: Timothy Lock Date: Mon, 29 Jan 2018 18:01:54 -0500 Subject: [PATCH 9/9] Fix test case of BadRequest Should have been StatusOK --- appstore/validator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appstore/validator_test.go b/appstore/validator_test.go index 6f9e10d..056e169 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -205,7 +205,7 @@ func TestResponses(t *testing.T) { }, // VerifyBadPayload { - testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 21002}`)), + testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21002}`)), expected: &IAPResponse{ Status: 21002, },