diff --git a/README.md b/README.md index 3cbfb10..4452db3 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ func main() { ReceiptData: "your receipt data encoded by base64", } resp := &appstore.IAPResponse{} - err := client.Verify(req, resp) + ctx := context.Background() + err := client.Verify(ctx, req, resp) } ``` @@ -66,7 +67,8 @@ func main() { } client := playstore.New(jsonKey) - resp, err := client.VerifySubscription("package", "subscriptionID", "purchaseToken") + ctx := context.Background() + resp, err := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken") } ``` diff --git a/appstore/validator.go b/appstore/validator.go index 1a8c2fd..1e4af86 100644 --- a/appstore/validator.go +++ b/appstore/validator.go @@ -2,11 +2,11 @@ package appstore import ( "bytes" + "context" "encoding/json" "errors" "io/ioutil" "net/http" - "time" ) const ( @@ -18,14 +18,9 @@ const ( ContentType string = "application/json; charset=utf-8" ) -// Config is a configuration to initialize client -type Config struct { - TimeOut time.Duration -} - // IAPClient is an interface to call validation API in App Store type IAPClient interface { - Verify(IAPRequest, interface{}) error + Verify(ctx context.Context, reqBody IAPRequest, resp interface{}) error } // Client implements IAPClient @@ -98,7 +93,7 @@ func NewWithClient(client *http.Client) *Client { } // Verify sends receipts and gets validation result -func (c *Client) Verify(reqBody IAPRequest, result interface{}) error { +func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interface{}) error { b := new(bytes.Buffer) json.NewEncoder(b).Encode(reqBody) @@ -107,15 +102,16 @@ func (c *Client) Verify(reqBody IAPRequest, result interface{}) error { return err } req.Header.Set("Content-Type", ContentType) + req = req.WithContext(ctx) resp, err := c.httpCli.Do(req) if err != nil { return err } defer resp.Body.Close() - return c.parseResponse(resp, result, reqBody) + return c.parseResponse(resp, result, ctx, reqBody) } -func (c *Client) parseResponse(resp *http.Response, result interface{}, reqBody IAPRequest) error { +func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx context.Context, reqBody IAPRequest) error { // Read the body now so that we can unmarshal it twice buf, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -142,6 +138,7 @@ func (c *Client) parseResponse(resp *http.Response, result interface{}, reqBody return err } req.Header.Set("Content-Type", ContentType) + req = req.WithContext(ctx) resp, err := c.httpCli.Do(req) if err != nil { return err diff --git a/appstore/validator_test.go b/appstore/validator_test.go index e7c8972..408bbc2 100644 --- a/appstore/validator_test.go +++ b/appstore/validator_test.go @@ -1,6 +1,7 @@ package appstore import ( + "context" "errors" "io/ioutil" "net/http" @@ -128,10 +129,31 @@ func TestVerifyTimeout(t *testing.T) { ReceiptData: "dummy data", } result := &IAPResponse{} - err := client.Verify(req, result) + ctx := context.Background() + err := client.Verify(ctx, req, result) if err == nil { t.Errorf("error should be occurred because of timeout") } + t.Log(err) +} + +func TestVerifyWithCancel(t *testing.T) { + client := New() + + req := IAPRequest{ + ReceiptData: "dummy data", + } + result := &IAPResponse{} + ctx, cancelFunc := context.WithCancel(context.Background()) + go func() { + time.Sleep(10 * time.Millisecond) + cancelFunc() + }() + err := client.Verify(ctx, req, result) + if err == nil { + t.Errorf("error should be occurred because of context cancel") + } + t.Log(err) } func TestVerifyBadURL(t *testing.T) { @@ -142,7 +164,8 @@ func TestVerifyBadURL(t *testing.T) { ReceiptData: "dummy data", } result := &IAPResponse{} - err := client.Verify(req, result) + ctx := context.Background() + err := client.Verify(ctx, req, result) if err == nil { t.Errorf("error should be occurred because the server is not real") } @@ -195,7 +218,8 @@ func TestResponses(t *testing.T) { client.SandboxURL = tc.sandboxServ.URL } - err := client.Verify(req, result) + ctx := context.Background() + err := client.Verify(ctx, req, result) if err != nil { t.Errorf("Test case %d - %s", i, err.Error()) } @@ -233,7 +257,8 @@ func TestErrors(t *testing.T) { defer tc.testServer.Close() client.ProductionURL = tc.testServer.URL - err := client.Verify(req, result) + 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) } @@ -244,7 +269,8 @@ func TestCannotReadBody(t *testing.T) { client := New() testResponse := http.Response{Body: ioutil.NopCloser(errReader(0))} - if client.parseResponse(&testResponse, IAPResponse{}, IAPRequest{}) == nil { + ctx := context.Background() + if client.parseResponse(&testResponse, IAPResponse{}, ctx, IAPRequest{}) == nil { t.Errorf("expected redirectToSandbox to fail to read the body") } } @@ -253,7 +279,8 @@ func TestCannotUnmarshalBody(t *testing.T) { client := New() testResponse := http.Response{Body: ioutil.NopCloser(strings.NewReader(`{"status": true}`))} - if client.parseResponse(&testResponse, StatusResponse{}, IAPRequest{}) == nil { + ctx := context.Background() + if client.parseResponse(&testResponse, StatusResponse{}, ctx, IAPRequest{}) == nil { t.Errorf("expected redirectToSandbox to fail to unmarshal the data") } } diff --git a/playstore/validator.go b/playstore/validator.go index a95f80e..923ad40 100644 --- a/playstore/validator.go +++ b/playstore/validator.go @@ -29,11 +29,11 @@ func SetTimeout(t time.Duration) { // The IABClient type is an interface to verify purchase token type IABClient interface { - VerifySubscription(string, string, string) (*androidpublisher.SubscriptionPurchase, error) - VerifyProduct(string, string, string) (*androidpublisher.ProductPurchase, error) - CancelSubscription(string, string, string) error - RefundSubscription(string, string, string) error - RevokeSubscription(string, string, string) error + VerifySubscription(context.Context, string, string, string) (*androidpublisher.SubscriptionPurchase, error) + VerifyProduct(context.Context, string, string, string) (*androidpublisher.ProductPurchase, error) + CancelSubscription(context.Context, string, string, string) error + RefundSubscription(context.Context, string, string, string) error + RevokeSubscription(context.Context, string, string, string) error } // The Client type implements VerifySubscription method @@ -56,6 +56,7 @@ func New(jsonKey []byte) (Client, error) { // VerifySubscription verifies subscription status func (c *Client) VerifySubscription( + ctx context.Context, packageName string, subscriptionID string, token string, @@ -66,13 +67,14 @@ func (c *Client) VerifySubscription( } ps := androidpublisher.NewPurchasesSubscriptionsService(service) - result, err := ps.Get(packageName, subscriptionID, token).Do() + result, err := ps.Get(packageName, subscriptionID, token).Context(ctx).Do() return result, err } // VerifyProduct verifies product status func (c *Client) VerifyProduct( + ctx context.Context, packageName string, productID string, token string, @@ -83,48 +85,48 @@ func (c *Client) VerifyProduct( } ps := androidpublisher.NewPurchasesProductsService(service) - result, err := ps.Get(packageName, productID, token).Do() + result, err := ps.Get(packageName, productID, token).Context(ctx).Do() return result, err } // CancelSubscription cancels a user's subscription purchase. -func (c *Client) CancelSubscription(packageName string, subscriptionID string, token string) error { +func (c *Client) CancelSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error { service, err := androidpublisher.New(c.httpClient) if err != nil { return err } ps := androidpublisher.NewPurchasesSubscriptionsService(service) - err = ps.Cancel(packageName, subscriptionID, token).Do() + err = ps.Cancel(packageName, subscriptionID, token).Context(ctx).Do() return err } // RefundSubscription refunds a user's subscription purchase, but the subscription remains valid // until its expiration time and it will continue to recur. -func (c *Client) RefundSubscription(packageName string, subscriptionID string, token string) error { +func (c *Client) RefundSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error { service, err := androidpublisher.New(c.httpClient) if err != nil { return err } ps := androidpublisher.NewPurchasesSubscriptionsService(service) - err = ps.Refund(packageName, subscriptionID, token).Do() + err = ps.Refund(packageName, subscriptionID, token).Context(ctx).Do() return err } // RevokeSubscription refunds and immediately revokes a user's subscription purchase. // Access to the subscription will be terminated immediately and it will stop recurring. -func (c *Client) RevokeSubscription(packageName string, subscriptionID string, token string) error { +func (c *Client) RevokeSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error { service, err := androidpublisher.New(c.httpClient) if err != nil { return err } ps := androidpublisher.NewPurchasesSubscriptionsService(service) - err = ps.Revoke(packageName, subscriptionID, token).Do() + err = ps.Revoke(packageName, subscriptionID, token).Context(ctx).Do() return err } diff --git a/playstore/validator_test.go b/playstore/validator_test.go index 7c28c2c..4e022f2 100644 --- a/playstore/validator_test.go +++ b/playstore/validator_test.go @@ -1,6 +1,7 @@ package playstore import ( + "context" "encoding/base64" "errors" "reflect" @@ -70,7 +71,8 @@ func TestVerifySubscription(t *testing.T) { expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) - _, err := client.VerifySubscription("package", "subscriptionID", "purchaseToken") + ctx := context.Background() + _, err := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken") if err.Error() != expected { t.Errorf("got %v\nwant %v", err, expected) @@ -83,7 +85,8 @@ func TestVerifySubscriptionAndroidPublisherError(t *testing.T) { t.Parallel() client := Client{nil} expected := errors.New("client is nil") - _, actual := client.VerifySubscription("package", "subscriptionID", "purchaseToken") + ctx := context.Background() + _, actual := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken") if !reflect.DeepEqual(actual, expected) { t.Errorf("got %v\nwant %v", actual, expected) @@ -96,7 +99,8 @@ func TestVerifyProduct(t *testing.T) { expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" client, _ := New(jsonKey) - _, err := client.VerifyProduct("package", "productID", "purchaseToken") + ctx := context.Background() + _, err := client.VerifyProduct(ctx, "package", "productID", "purchaseToken") if err.Error() != expected { t.Errorf("got %v", err) @@ -109,7 +113,8 @@ func TestVerifyProductAndroidPublisherError(t *testing.T) { t.Parallel() client := Client{nil} expected := errors.New("client is nil") - _, actual := client.VerifyProduct("package", "productID", "purchaseToken") + ctx := context.Background() + _, actual := client.VerifyProduct(ctx, "package", "productID", "purchaseToken") if !reflect.DeepEqual(actual, expected) { t.Errorf("got %v\nwant %v", actual, expected) @@ -121,7 +126,8 @@ func TestCancelSubscription(t *testing.T) { // Exception scenario client := Client{nil} expected := errors.New("client is nil") - actual := client.CancelSubscription("package", "productID", "purchaseToken") + ctx := context.Background() + actual := client.CancelSubscription(ctx, "package", "productID", "purchaseToken") if !reflect.DeepEqual(actual, expected) { t.Errorf("got %v\nwant %v", actual, expected) @@ -129,7 +135,7 @@ func TestCancelSubscription(t *testing.T) { client, _ = New(jsonKey) expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" - actual = client.CancelSubscription("package", "productID", "purchaseToken") + actual = client.CancelSubscription(ctx, "package", "productID", "purchaseToken") if actual.Error() != expectedStr { t.Errorf("got %v\nwant %v", actual, expectedStr) @@ -143,7 +149,8 @@ func TestRefundSubscription(t *testing.T) { // Exception scenario client := Client{nil} expected := errors.New("client is nil") - actual := client.RefundSubscription("package", "productID", "purchaseToken") + ctx := context.Background() + actual := client.RefundSubscription(ctx, "package", "productID", "purchaseToken") if !reflect.DeepEqual(actual, expected) { t.Errorf("got %v\nwant %v", actual, expected) @@ -151,7 +158,7 @@ func TestRefundSubscription(t *testing.T) { client, _ = New(jsonKey) expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" - actual = client.RefundSubscription("package", "productID", "purchaseToken") + actual = client.RefundSubscription(ctx, "package", "productID", "purchaseToken") if actual.Error() != expectedStr { t.Errorf("got %v\nwant %v", actual, expectedStr) @@ -165,7 +172,8 @@ func TestRevokeSubscription(t *testing.T) { // Exception scenario client := Client{nil} expected := errors.New("client is nil") - actual := client.RevokeSubscription("package", "productID", "purchaseToken") + ctx := context.Background() + actual := client.RevokeSubscription(ctx, "package", "productID", "purchaseToken") if !reflect.DeepEqual(actual, expected) { t.Errorf("got %v\nwant %v", actual, expected) @@ -173,7 +181,7 @@ func TestRevokeSubscription(t *testing.T) { client, _ = New(jsonKey) expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" - actual = client.RevokeSubscription("package", "productID", "purchaseToken") + actual = client.RevokeSubscription(ctx, "package", "productID", "purchaseToken") if actual.Error() != expectedStr { t.Errorf("got %v\nwant %v", actual, expectedStr)