49 Commits

Author SHA1 Message Date
sanjid133
777e3c50ef Add cancellation reason field for notification 2020-05-14 22:05:15 +06:00
sanjid133
4f7b16717e fix auto renew status 2020-03-05 17:35:21 +06:00
sanjid133
a564d013c9 status field not present in initial_buy 2020-03-05 17:35:21 +06:00
sanjid133
134bcb9b89 auto renew status string 2020-03-05 17:35:21 +06:00
sanjid133
5cc0a53c63 Verfiy latest receipt 2020-03-05 17:35:21 +06:00
Minhaz Ahmed Syrus
22c0b76a09 Add missing subscription billing retry flag 2020-03-05 17:33:20 +06:00
Minhaz Ahmed Syrus
0ae05687d4 Add missing notification verification fields 2020-03-05 17:33:20 +06:00
Minhaz Ahmed Syrus
b04c08d22b Add missing subscription billing retry flag 2020-03-05 17:32:57 +06:00
Minhaz Ahmed Syrus
9b772f3398 Add missing notification verification fields 2020-03-05 17:32:19 +06:00
sanjid133
073874b26d Add renewal field 2020-03-05 17:31:39 +06:00
sanjid133
a14bd11b0c Verfiy latest receipt 2020-03-05 17:31:39 +06:00
sanjid133
7f77f5e33e Add missing is_upgrade field 2020-03-05 17:00:02 +06:00
Minhaz Ahmed Syrus
eacfb2d096 Add missing subscription billing retry flag 2020-03-05 17:00:02 +06:00
Minhaz Ahmed Syrus
91cd5b791c Add missing notification verification fields 2020-03-05 16:59:30 +06:00
Minhaz Ahmed Syrus
2700fc2ec0 Add missing subscription billing retry flag 2020-03-05 16:57:47 +06:00
Minhaz Ahmed Syrus
e4338cd607 Add missing notification verification fields 2020-03-05 16:57:47 +06:00
Junpei Tsuji
8ddfbdb30a Merge pull request #114 from jun06t/handle-appstore-http-error
Handle http status 5xx error
2020-02-20 16:41:39 +09:00
Junpei Tsuji
dd8af1ccd2 Update go 1.13 2020-02-20 14:52:46 +09:00
Junpei Tsuji
9ba6e70200 Handle http status 5xx error 2020-02-20 14:52:46 +09:00
Junpei Tsuji
052ce72134 Merge pull request #112 from tockn/did-fail-to-renew
Add NotificationType "DID_FAIL_TO_RENEW"
2020-02-20 14:36:24 +09:00
Junpei Tsuji
a3f651b46e Merge pull request #113 from jun06t/fix-test
Fix test
2020-02-20 13:09:20 +09:00
Junpei Tsuji
588d81166b Fixed test for playstore 2020-02-20 13:04:39 +09:00
Junpei Tsuji
a61c519ac8 Fixed makefile 2020-02-20 13:02:14 +09:00
Junpei Tsuji
3c88ce1648 Merge pull request #110 from shemming/PR-branch
Add ability to acknowledge purchases from playstore
2020-02-20 12:53:56 +09:00
tockn
0aa274084a add notification type DID_FAIL_TO_RENEW 2020-02-19 23:52:19 +09:00
tockn
c61599bf8e go fmt 2020-02-19 23:45:54 +09:00
Sabrina
e54455a635 add mocks for appstore 2020-01-30 16:04:26 -08:00
Junpei Tsuji
d9ae3a1d88 Merge pull request #105 from mhemmings/add-missing-omitempty
appstore: Set IsUpgraded as omitempty
2020-01-29 23:37:14 +09:00
Junpei Tsuji
b739b19032 Merge pull request #111 from ercling/master
Add SubscriptionNotification deprecation comments
2020-01-29 23:35:29 +09:00
Oleksandr Mykhailiuta
be7b768650 Add SubscriptionNotification deprecation comments 2020-01-29 12:24:20 +02:00
Sabrina
98cf7b036f update go mod 2020-01-28 13:40:15 -08:00
Sabrina
8359bd764f create mocks for playstore 2020-01-28 13:31:57 -08:00
Sabrina
3fcc899200 add more unit tests 2020-01-27 15:28:48 -08:00
Sabrina
31f386e220 add acknowledge product, refactor 2020-01-27 14:26:09 -08:00
Junpei Tsuji
ac84b97cd8 Merge pull request #108 from jun06t/go-mod-tidy
go mod tidy
2020-01-20 18:32:12 +09:00
Junpei Tsuji
c4c303e812 go mod tidy 2020-01-20 18:28:13 +09:00
Junpei Tsuji
b5222c00cf Merge pull request #106 from mlesar/hotfix/unified-receipt-status
Fixing NotificationUnifiedReceipt status type
2020-01-20 18:17:13 +09:00
Matija Lesar
c8962b67cb Fixing NotificationUnifiedReceipt Status type 2020-01-16 09:19:57 +01:00
Mark Hemmings
627fa5e7d1 appstore: Set IsUpgraded as omitempty 2019-12-27 15:07:54 +00:00
Junpei Tsuji
b31646baf4 Merge pull request #101 from mlesar/unified_receipt
Adding unified receipt to the Apple S2S notification
2019-11-26 00:20:18 +09:00
Matija Lesar
1bd99243d7 Changed environment property type 2019-11-25 12:42:41 +01:00
Matija Lesar
7d624ad068 Updated order of properties 2019-11-25 11:26:42 +01:00
Matija Lesar
31a625b71e CR changes 2019-11-25 11:25:31 +01:00
Matija Lesar
4209b06a64 Added unified receipt 2019-11-25 10:00:13 +01:00
Junpei Tsuji
3d3bd2b6cf Merge pull request #99 from jun06t/error-message
Fixed test error messages
2019-10-10 11:56:32 +09:00
Junpei Tsuji
934d4ffbbd Fixed test error messages 2019-10-10 10:58:16 +09:00
Junpei Tsuji
9c0e16f820 Merge pull request #96 from jun06t/appstore-grace-period
Added grace period fields
2019-09-17 13:47:52 +09:00
Junpei Tsuji
e895c80eb0 Added grace period fields 2019-09-17 13:23:05 +09:00
kitakitabauer
062102b0f3 Merge pull request #93 from awa/androidpublisher-v3
Update androidpublisher api v3
2019-06-19 16:36:38 +09:00
13 changed files with 512 additions and 274 deletions

View File

@@ -1,6 +1,6 @@
language: go language: go
go: go:
- 1.12.5 - 1.13.x
env: env:
global: global:
- GO111MODULE=on - GO111MODULE=on

View File

@@ -1,15 +1,22 @@
.PHONEY: all
.PHONEY: all setup test cover
all: setup cover all: setup cover
.PHONEY: setup
setup: setup:
go get golang.org/x/tools/cmd/cover go get golang.org/x/tools/cmd/cover
go get google.golang.org/appengine/urlfetch go get google.golang.org/appengine/urlfetch
go get ./... go get ./...
.PHONEY: test
test: test:
go test -v ./... go test -v ./...
.PHONEY: cover
cover: cover:
go test -coverprofile=coverage.txt ./... go test -coverprofile=coverage.txt ./...
.PHONEY: generate
generate:
rm -rf ./appstore/mocks/*
rm -rf ./playstore/mocks/*
go generate ./...

View File

@@ -1,7 +1,7 @@
go-iap 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) [![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) [![codecov.io](https://codecov.io/github/awa/go-iap/coverage.svg?branch=master)](https://codecov.io/github/awa/go-iap?branch=master)

View File

@@ -0,0 +1,49 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/awa/go-iap/appstore (interfaces: IAPClient)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
appstore "github.com/awa/go-iap/appstore"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockIAPClient is a mock of IAPClient interface
type MockIAPClient struct {
ctrl *gomock.Controller
recorder *MockIAPClientMockRecorder
}
// MockIAPClientMockRecorder is the mock recorder for MockIAPClient
type MockIAPClientMockRecorder struct {
mock *MockIAPClient
}
// NewMockIAPClient creates a new mock instance
func NewMockIAPClient(ctrl *gomock.Controller) *MockIAPClient {
mock := &MockIAPClient{ctrl: ctrl}
mock.recorder = &MockIAPClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockIAPClient) EXPECT() *MockIAPClientMockRecorder {
return m.recorder
}
// Verify mocks base method
func (m *MockIAPClient) Verify(arg0 context.Context, arg1 appstore.IAPRequest, arg2 interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Verify indicates an expected call of Verify
func (mr *MockIAPClientMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockIAPClient)(nil).Verify), arg0, arg1, arg2)
}

View File

@@ -76,16 +76,27 @@ type (
CancellationDatePST string `json:"cancellation_date_pst,omitempty"` CancellationDatePST string `json:"cancellation_date_pst,omitempty"`
} }
// The GracePeriodDate type indicates the grace period date for the subscription
GracePeriodDate struct {
GracePeriodDate string `json:"grace_period_expires_date,omitempty"`
GracePeriodDateMS string `json:"grace_period_expires_date_ms,omitempty"`
GracePeriodDatePST string `json:"grace_period_expires_date_pst,omitempty"`
}
// The InApp type has the receipt attributes // The InApp type has the receipt attributes
InApp struct { InApp struct {
Quantity string `json:"quantity"` Quantity string `json:"quantity"`
ProductID string `json:"product_id"` ProductID string `json:"product_id"`
TransactionID string `json:"transaction_id"` TransactionID string `json:"transaction_id"`
OriginalTransactionID string `json:"original_transaction_id"` OriginalTransactionID string `json:"original_transaction_id"`
WebOrderLineItemID string `json:"web_order_line_item_id,omitempty"` WebOrderLineItemID string `json:"web_order_line_item_id,omitempty"`
PromotionalOfferID string `json:"promotional_offer_id"`
SubscriptionGroupIdentifier string `json:"subscription_group_identifier"`
IsTrialPeriod string `json:"is_trial_period"` IsTrialPeriod string `json:"is_trial_period"`
IsInIntroOfferPeriod string `json:"is_in_intro_offer_period,omitempty"` IsInIntroOfferPeriod string `json:"is_in_intro_offer_period,omitempty"`
IsUpgraded string `json:"is_upgraded,omitempty"`
ExpiresDate ExpiresDate
PurchaseDate PurchaseDate
@@ -120,6 +131,8 @@ type (
SubscriptionPriceConsentStatus string `json:"price_consent_status"` SubscriptionPriceConsentStatus string `json:"price_consent_status"`
ProductID string `json:"product_id"` ProductID string `json:"product_id"`
OriginalTransactionID string `json:"original_transaction_id"` OriginalTransactionID string `json:"original_transaction_id"`
GracePeriodDate
} }
// The IAPResponse type has the response properties // The IAPResponse type has the response properties
@@ -137,6 +150,19 @@ type (
IsRetryable bool `json:"is-retryable,omitempty"` IsRetryable bool `json:"is-retryable,omitempty"`
} }
// The IAPLatestResponse type has the response properties
// If you use latest_receipt as token to verify, response should be like following struct
IAPLatestResponse struct {
Status int `json:"status,omitempty"`
Receipt InApp `json:"receipt"`
LatestReceiptInfo InApp `json:"latest_receipt_info,omitempty"`
LatestExpiredReceiptInfo InApp `json:"latest_expired_receipt_info,omitempty"`
LatestReceipt string `json:"latest_receipt,omitempty"`
SubscriptionAutoRenewStatus interface{} `json:"auto_renew_status,omitempty"`
SubscriptionAutoRenewProductID string `json:"auto_renew_product_id,omitempty"`
SubscriptionRetryFlag string `json:"is_in_billing_retry_period,omitempty"`
}
// The HttpStatusResponse struct contains the status code returned by the store // 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 // Used as a workaround to detect when to hit the production appstore or sandbox appstore regardless of receipt type
StatusResponse struct { StatusResponse struct {

View File

@@ -9,13 +9,18 @@ const (
// Subscription was canceled by Apple customer support. // Subscription was canceled by Apple customer support.
NotificationTypeCancel NotificationType = "CANCEL" NotificationTypeCancel NotificationType = "CANCEL"
// Automatic renewal was successful for an expired subscription. // Automatic renewal was successful for an expired subscription.
// Deprecated: DID_RECOVER should be used instead of RENEWAL
NotificationTypeRenewal NotificationType = "RENEWAL" NotificationTypeRenewal NotificationType = "RENEWAL"
// Expired subscription recovered through a billing retry.
NotificationTypeDidRecover NotificationType = "DID_RECOVER"
// Customer renewed a subscription interactively after it lapsed. // Customer renewed a subscription interactively after it lapsed.
NotificationTypeInteractiveRenewal NotificationType = "INTERACTIVE_RENEWAL" NotificationTypeInteractiveRenewal NotificationType = "INTERACTIVE_RENEWAL"
// Customer changed the plan that takes affect at the next subscription renewal. Current active plan is not affected. // Customer changed the plan that takes affect at the next subscription renewal. Current active plan is not affected.
NotificationTypeDidChangeRenewalPreference NotificationType = "DID_CHANGE_RENEWAL_PREF" NotificationTypeDidChangeRenewalPreference NotificationType = "DID_CHANGE_RENEWAL_PREF"
// Customer changed the subscription renewal status. Current active plan is not affected. // Customer changed the subscription renewal status. Current active plan is not affected.
NotificationTypeDidChangeRenewalStatus NotificationType = "DID_CHANGE_RENEWAL_STATUS" NotificationTypeDidChangeRenewalStatus NotificationType = "DID_CHANGE_RENEWAL_STATUS"
// Subscription failed to renew due to a billing issue.
NotificationTypeDidFailToRenew NotificationType = "DID_FAIL_TO_RENEW"
) )
type NotificationEnvironment string type NotificationEnvironment string
@@ -51,6 +56,15 @@ type NotificationReceipt struct {
OriginalPurchaseDate OriginalPurchaseDate
NotificationExpiresDate NotificationExpiresDate
CancellationDate CancellationDate
CancellationReason string `json:"cancellation_reason,omitempty"`
}
type NotificationUnifiedReceipt struct {
Status int `json:"status"`
Environment Environment `json:"environment"`
LatestReceipt string `json:"latest_receipt"`
LatestReceiptInfo []InApp `json:"latest_receipt_info"`
PendingRenewalInfo []PendingRenewalInfo `json:"pending_renewal_info,omitempty"`
} }
type SubscriptionNotification struct { type SubscriptionNotification struct {
@@ -74,16 +88,29 @@ type SubscriptionNotification struct {
AutoRenewStatus string `json:"auto_renew_status"` // false or true AutoRenewStatus string `json:"auto_renew_status"` // false or true
AutoRenewProductID string `json:"auto_renew_product_id"` AutoRenewProductID string `json:"auto_renew_product_id"`
// HACK (msyrus): Separate Subscriptiton Notification from Notification verification response
Status int `json:"status,omitempty"`
Receipt NotificationReceipt `json:"recipt"`
SubscriptionRetryFlag string `json:"is_in_billing_retry_period"`
// Posted if the notification_type is RENEWAL or INTERACTIVE_RENEWAL, and only if the renewal is successful. // Posted if the notification_type is RENEWAL or INTERACTIVE_RENEWAL, and only if the renewal is successful.
// Posted also if the notification_type is INITIAL_BUY. // Posted also if the notification_type is INITIAL_BUY.
// Not posted for notification_type CANCEL. // Not posted for notification_type CANCEL.
LatestReceipt string `json:"latest_receipt"` // Deprecated: use UnifiedReceipt.LatestReceipt instead. See details: https://developer.apple.com/documentation/appstoreservernotifications/ .
LatestReceipt string `json:"latest_receipt"`
// Deprecated: use UnifiedReceipt.LatestReceiptInfo instead. See details: https://developer.apple.com/documentation/appstoreservernotifications/ .
LatestReceiptInfo NotificationReceipt `json:"latest_receipt_info"` LatestReceiptInfo NotificationReceipt `json:"latest_receipt_info"`
// In the new notifications above properties latest_receipt, latest_receipt_info are moved under this one
UnifiedReceipt NotificationUnifiedReceipt `json:"unified_receipt"`
// Posted only if the notification_type is RENEWAL or CANCEL or if renewal failed and subscription expired. // Posted only if the notification_type is RENEWAL or CANCEL or if renewal failed and subscription expired.
LatestExpiredReceipt string `json:"latest_expired_receipt"` // Deprecated: see details: https://developer.apple.com/documentation/appstoreservernotifications/ .
LatestExpiredReceipt string `json:"latest_expired_receipt"`
// Deprecated: see details: https://developer.apple.com/documentation/appstoreservernotifications/ .
LatestExpiredReceiptInfo NotificationReceipt `json:"latest_expired_receipt_info"` LatestExpiredReceiptInfo NotificationReceipt `json:"latest_expired_receipt_info"`
// Posted only if the notification_type is CANCEL. // Posted only if the notification_type is CANCEL.
CancellationDate CancellationDate
CancellationReason string `json:"cancellation_reason,omitempty"`
} }

View File

@@ -5,11 +5,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time" "time"
) )
//go:generate mockgen -destination=mocks/appstore.go -package=mocks github.com/awa/go-iap/appstore IAPClient
const ( const (
// SandboxURL is the endpoint for sandbox environment. // SandboxURL is the endpoint for sandbox environment.
SandboxURL string = "https://sandbox.itunes.apple.com/verifyReceipt" SandboxURL string = "https://sandbox.itunes.apple.com/verifyReceipt"
@@ -31,47 +34,53 @@ type Client struct {
httpCli *http.Client 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 // HandleError returns error message by status code
func HandleError(status int) error { func HandleError(status int) error {
var message string var e error
switch status { switch status {
case 0: case 0:
return nil return nil
case 21000: case 21000:
message = "The App Store could not read the JSON object you provided." e = ErrInvalidJSON
case 21002: case 21002:
message = "The data in the receipt-data property was malformed or missing." e = ErrInvalidReceiptData
case 21003: case 21003:
message = "The receipt could not be authenticated." e = ErrReceiptUnauthenticated
case 21004: case 21004:
message = "The shared secret you provided does not match the shared secret on file for your account." e = ErrInvalidSharedSecret
case 21005: case 21005:
message = "The receipt server is not currently available." e = ErrServerUnavailable
case 21007: 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: 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: case 21010:
message = "This receipt could not be authorized. Treat this the same as if a purchase was never made." e = ErrReceiptUnauthorized
default: default:
if status >= 21100 && status <= 21199 { if status >= 21100 && status <= 21199 {
message = "Internal data access error." e = ErrInternalDataAccessError
} else { } 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 // New creates a client object
@@ -96,64 +105,71 @@ func NewWithClient(client *http.Client) *Client {
} }
// Verify sends receipts and gets validation result // Verify sends receipts and gets validation result
func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interface{}) error { func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interface{}) (Environment, error) {
b := new(bytes.Buffer) b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(reqBody); err != nil { if err := json.NewEncoder(b).Encode(reqBody); err != nil {
return err return "", err
} }
req, err := http.NewRequest("POST", c.ProductionURL, b) req, err := http.NewRequest("POST", c.ProductionURL, b)
if err != nil { if err != nil {
return err return "", err
} }
req.Header.Set("Content-Type", ContentType) req.Header.Set("Content-Type", ContentType)
req = req.WithContext(ctx) req = req.WithContext(ctx)
resp, err := c.httpCli.Do(req) resp, err := c.httpCli.Do(req)
if err != nil { if err != nil {
return err return "", err
} }
defer resp.Body.Close() 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) return c.parseResponse(resp, result, ctx, reqBody)
} }
func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx context.Context, reqBody IAPRequest) error { func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx context.Context, reqBody IAPRequest) (Environment, error) {
// Read the body now so that we can unmarshal it twice // Read the body now so that we can unmarshal it twice
buf, err := ioutil.ReadAll(resp.Body) buf, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return "", err
} }
err = json.Unmarshal(buf, &result) err = json.Unmarshal(buf, &result)
if err != nil { if err != nil {
return err return "", err
} }
// https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL // https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL
var r StatusResponse var r StatusResponse
err = json.Unmarshal(buf, &r) err = json.Unmarshal(buf, &r)
if err != nil { if err != nil {
return err return "", err
} }
if r.Status == 21007 { if r.Status == 21007 {
b := new(bytes.Buffer) b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(reqBody); err != nil { if err := json.NewEncoder(b).Encode(reqBody); err != nil {
return err return "", err
} }
req, err := http.NewRequest("POST", c.SandboxURL, b) req, err := http.NewRequest("POST", c.SandboxURL, b)
if err != nil { if err != nil {
return err return "", err
} }
req.Header.Set("Content-Type", ContentType) req.Header.Set("Content-Type", ContentType)
req = req.WithContext(ctx) req = req.WithContext(ctx)
resp, err := c.httpCli.Do(req) resp, err := c.httpCli.Do(req)
if err != nil { if err != nil {
return err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= 500 {
return Sandbox, fmt.Errorf("Received http status code %d from the App Store Sandbox: %w", resp.StatusCode, ErrAppStoreServer)
}
return json.NewDecoder(resp.Body).Decode(result) // 21007 is found when the receipt is from the test environment
return Sandbox, json.NewDecoder(resp.Body).Decode(result)
} }
return nil return Production, nil
} }

View File

@@ -26,52 +26,52 @@ func TestHandleError(t *testing.T) {
{ {
name: "status 21000", name: "status 21000",
in: 21000, in: 21000,
out: errors.New("The App Store could not read the JSON object you provided."), out: ErrInvalidJSON,
}, },
{ {
name: "status 21002", name: "status 21002",
in: 21002, in: 21002,
out: errors.New("The data in the receipt-data property was malformed or missing."), out: ErrInvalidReceiptData,
}, },
{ {
name: "status 21003", name: "status 21003",
in: 21003, in: 21003,
out: errors.New("The receipt could not be authenticated."), out: ErrReceiptUnauthenticated,
}, },
{ {
name: "status 21004", name: "status 21004",
in: 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", name: "status 21005",
in: 21005, in: 21005,
out: errors.New("The receipt server is not currently available."), out: ErrServerUnavailable,
}, },
{ {
name: "status 21007", name: "status 21007",
in: 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", name: "status 21008",
in: 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", name: "status 21010",
in: 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", name: "status 21100 ~ 21199",
in: 21100, in: 21100,
out: errors.New("Internal data access error."), out: ErrInternalDataAccessError,
}, },
{ {
name: "status unknown", name: "status unknown",
in: 100, 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) { t.Run(v.name, func(t *testing.T) {
out := HandleError(v.in) 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) 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{} result := &IAPResponse{}
type testCase struct { type testCase struct {
name string
testServer *httptest.Server testServer *httptest.Server
sandboxServ *httptest.Server sandboxServ *httptest.Server
expected *IAPResponse expected *IAPResponse
} }
testCases := []testCase{ testCases := []testCase{
// VerifySandboxReceipt
{ {
name: "VerifySandboxReceipt",
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)), testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)),
sandboxServ: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)), sandboxServ: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)),
expected: &IAPResponse{ expected: &IAPResponse{
Status: 0, Status: 0,
}, },
}, },
// VerifyBadPayload
{ {
name: "VerifyBadPayload",
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21002}`)), testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21002}`)),
expected: &IAPResponse{ expected: &IAPResponse{
Status: 21002, Status: 21002,
}, },
}, },
// SuccessPayload
{ {
name: "SuccessPayload",
testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 0}`)), testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 0}`)),
expected: &IAPResponse{ expected: &IAPResponse{
Status: 0, Status: 0,
@@ -213,57 +214,65 @@ func TestResponses(t *testing.T) {
client := New() client := New()
client.SandboxURL = "localhost" client.SandboxURL = "localhost"
for i, tc := range testCases { for _, tc := range testCases {
defer tc.testServer.Close() t.Run(tc.name, func(t *testing.T) {
client.ProductionURL = tc.testServer.URL defer tc.testServer.Close()
if tc.sandboxServ != nil { client.ProductionURL = tc.testServer.URL
client.SandboxURL = tc.sandboxServ.URL if tc.sandboxServ != nil {
} client.SandboxURL = tc.sandboxServ.URL
}
ctx := context.Background() ctx := context.Background()
err := client.Verify(ctx, req, result) err := client.Verify(ctx, req, result)
if err != nil { if err != nil {
t.Errorf("Test case %d - %s", i, err.Error()) t.Errorf("%s", err)
} }
if !reflect.DeepEqual(result, tc.expected) { if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Test case %d - got %v\nwant %v", i, result, tc.expected) t.Errorf("got %v\nwant %v", result, tc.expected)
} }
})
} }
} }
func TestErrors(t *testing.T) { func TestHttpStatusErrors(t *testing.T) {
req := IAPRequest{ req := IAPRequest{
ReceiptData: "dummy data", ReceiptData: "dummy data",
} }
result := &IAPResponse{} result := &IAPResponse{}
type testCase struct { type testCase struct {
name string
testServer *httptest.Server testServer *httptest.Server
err error
} }
testCases := []testCase{ 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!@#$%^`)), testServer: httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)),
err: ErrAppStoreServer,
}, },
} }
client := New() client := New()
client.SandboxURL = "localhost" client.SandboxURL = "localhost"
for i, tc := range testCases { for _, tc := range testCases {
defer tc.testServer.Close() t.Run(tc.name, func(t *testing.T) {
client.ProductionURL = tc.testServer.URL defer tc.testServer.Close()
client.ProductionURL = tc.testServer.URL
ctx := context.Background() ctx := context.Background()
err := client.Verify(ctx, req, result) err := client.Verify(ctx, req, result)
if err == nil { if !errors.Is(err, tc.err) {
t.Errorf("Test case %d - expected error to be not nil since the sandbox is not responding", i) 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if "POST" == r.Method { if "POST" == r.Method {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
w.Write([]byte(response)) w.Write([]byte(response))
return
} else { } else {
w.Write([]byte(`unsupported request`)) w.Write([]byte(`unsupported request`))
} }
w.WriteHeader(statusCode)
}) })
} }

17
go.mod
View File

@@ -4,24 +4,13 @@ go 1.12
require ( require (
cloud.google.com/go v0.39.0 // indirect cloud.google.com/go v0.39.0 // indirect
github.com/golang/mock v1.3.1 // indirect github.com/golang/mock v1.4.0
github.com/google/btree v1.0.0 // indirect
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f // indirect
github.com/kr/pty v1.1.4 // indirect
go.opencensus.io v0.22.0 // indirect go.opencensus.io v0.22.0 // indirect
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 // indirect golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 // indirect
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff // indirect
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect
golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171 // indirect
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
golang.org/x/tools v0.0.0-20190530215528-75312fb06703 // indirect
google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f
google.golang.org/appengine v1.6.0 google.golang.org/appengine v1.6.5
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 // indirect google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 // indirect
google.golang.org/grpc v1.21.0 // indirect google.golang.org/grpc v1.21.0 // indirect
honnef.co/go/tools v0.0.0-20190530170028-a1efa522b896 // indirect
) )

56
go.sum
View File

@@ -4,71 +4,52 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw= cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw=
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07 h1:XC1K3wNjuz44KaI+cj85C9TW85w/46RH7J+DTXNH5Wk=
golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0= golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0=
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -78,27 +59,22 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190530215528-75312fb06703/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM= google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f h1:tAgkkP6ovjCY8HraRtpXwh0CVqHwGqEAVLHwXyfFjIM= google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f h1:tAgkkP6ovjCY8HraRtpXwh0CVqHwGqEAVLHwXyfFjIM=
@@ -106,15 +82,13 @@ google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f/go.mod h1:8k5glujaEP+
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69 h1:4rNOqY4ULrKzS6twXa619uQgI7h9PaVd4ZhjFQ7C5zs=
google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
@@ -122,9 +96,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
honnef.co/go/tools v0.0.0-20190530170028-a1efa522b896/go.mod h1:wtc9q0E9zm8PjdRMh29DPlTlCCHVzKDwnkT4GskQVzg= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -0,0 +1,158 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/awa/go-iap/playstore (interfaces: IABProduct,IABSubscription)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
gomock "github.com/golang/mock/gomock"
v3 "google.golang.org/api/androidpublisher/v3"
reflect "reflect"
)
// MockIABProduct is a mock of IABProduct interface
type MockIABProduct struct {
ctrl *gomock.Controller
recorder *MockIABProductMockRecorder
}
// MockIABProductMockRecorder is the mock recorder for MockIABProduct
type MockIABProductMockRecorder struct {
mock *MockIABProduct
}
// NewMockIABProduct creates a new mock instance
func NewMockIABProduct(ctrl *gomock.Controller) *MockIABProduct {
mock := &MockIABProduct{ctrl: ctrl}
mock.recorder = &MockIABProductMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockIABProduct) EXPECT() *MockIABProductMockRecorder {
return m.recorder
}
// AcknowledgeProduct mocks base method
func (m *MockIABProduct) AcknowledgeProduct(arg0 context.Context, arg1, arg2, arg3, arg4 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AcknowledgeProduct", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)
return ret0
}
// AcknowledgeProduct indicates an expected call of AcknowledgeProduct
func (mr *MockIABProductMockRecorder) AcknowledgeProduct(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcknowledgeProduct", reflect.TypeOf((*MockIABProduct)(nil).AcknowledgeProduct), arg0, arg1, arg2, arg3, arg4)
}
// VerifyProduct mocks base method
func (m *MockIABProduct) VerifyProduct(arg0 context.Context, arg1, arg2, arg3 string) (*v3.ProductPurchase, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifyProduct", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*v3.ProductPurchase)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyProduct indicates an expected call of VerifyProduct
func (mr *MockIABProductMockRecorder) VerifyProduct(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyProduct", reflect.TypeOf((*MockIABProduct)(nil).VerifyProduct), arg0, arg1, arg2, arg3)
}
// MockIABSubscription is a mock of IABSubscription interface
type MockIABSubscription struct {
ctrl *gomock.Controller
recorder *MockIABSubscriptionMockRecorder
}
// MockIABSubscriptionMockRecorder is the mock recorder for MockIABSubscription
type MockIABSubscriptionMockRecorder struct {
mock *MockIABSubscription
}
// NewMockIABSubscription creates a new mock instance
func NewMockIABSubscription(ctrl *gomock.Controller) *MockIABSubscription {
mock := &MockIABSubscription{ctrl: ctrl}
mock.recorder = &MockIABSubscriptionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockIABSubscription) EXPECT() *MockIABSubscriptionMockRecorder {
return m.recorder
}
// AcknowledgeSubscription mocks base method
func (m *MockIABSubscription) AcknowledgeSubscription(arg0 context.Context, arg1, arg2, arg3 string, arg4 *v3.SubscriptionPurchasesAcknowledgeRequest) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AcknowledgeSubscription", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(error)
return ret0
}
// AcknowledgeSubscription indicates an expected call of AcknowledgeSubscription
func (mr *MockIABSubscriptionMockRecorder) AcknowledgeSubscription(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcknowledgeSubscription", reflect.TypeOf((*MockIABSubscription)(nil).AcknowledgeSubscription), arg0, arg1, arg2, arg3, arg4)
}
// CancelSubscription mocks base method
func (m *MockIABSubscription) CancelSubscription(arg0 context.Context, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CancelSubscription", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
return ret0
}
// CancelSubscription indicates an expected call of CancelSubscription
func (mr *MockIABSubscriptionMockRecorder) CancelSubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelSubscription", reflect.TypeOf((*MockIABSubscription)(nil).CancelSubscription), arg0, arg1, arg2, arg3)
}
// RefundSubscription mocks base method
func (m *MockIABSubscription) RefundSubscription(arg0 context.Context, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RefundSubscription", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
return ret0
}
// RefundSubscription indicates an expected call of RefundSubscription
func (mr *MockIABSubscriptionMockRecorder) RefundSubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefundSubscription", reflect.TypeOf((*MockIABSubscription)(nil).RefundSubscription), arg0, arg1, arg2, arg3)
}
// RevokeSubscription mocks base method
func (m *MockIABSubscription) RevokeSubscription(arg0 context.Context, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RevokeSubscription", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(error)
return ret0
}
// RevokeSubscription indicates an expected call of RevokeSubscription
func (mr *MockIABSubscriptionMockRecorder) RevokeSubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeSubscription", reflect.TypeOf((*MockIABSubscription)(nil).RevokeSubscription), arg0, arg1, arg2, arg3)
}
// VerifySubscription mocks base method
func (m *MockIABSubscription) VerifySubscription(arg0 context.Context, arg1, arg2, arg3 string) (*v3.SubscriptionPurchase, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VerifySubscription", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(*v3.SubscriptionPurchase)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifySubscription indicates an expected call of VerifySubscription
func (mr *MockIABSubscriptionMockRecorder) VerifySubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifySubscription", reflect.TypeOf((*MockIABSubscription)(nil).VerifySubscription), arg0, arg1, arg2, arg3)
}

View File

@@ -11,14 +11,19 @@ import (
"net/http" "net/http"
"time" "time"
"google.golang.org/api/option"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
androidpublisher "google.golang.org/api/androidpublisher/v3" androidpublisher "google.golang.org/api/androidpublisher/v3"
) )
//go:generate mockgen -destination=mocks/playstore.go -package=mocks github.com/awa/go-iap/playstore IABProduct,IABSubscription
// The IABProduct type is an interface for product service // The IABProduct type is an interface for product service
type IABProduct interface { type IABProduct interface {
VerifyProduct(context.Context, string, string, string) (*androidpublisher.ProductPurchase, error) VerifyProduct(context.Context, string, string, string) (*androidpublisher.ProductPurchase, error)
AcknowledgeProduct(context.Context, string, string, string, string) error
} }
// The IABSubscription type is an interface for subscription service // The IABSubscription type is an interface for subscription service
@@ -32,22 +37,41 @@ type IABSubscription interface {
// The Client type implements VerifySubscription method // The Client type implements VerifySubscription method
type Client struct { type Client struct {
httpCli *http.Client service *androidpublisher.Service
} }
// New returns http client which includes the credentials to access androidpublisher API. // New returns http client which includes the credentials to access androidpublisher API.
// You should create a service account for your project at // You should create a service account for your project at
// https://console.developers.google.com and download a JSON key file to set this argument. // https://console.developers.google.com and download a JSON key file to set this argument.
func New(jsonKey []byte) (*Client, error) { func New(jsonKey []byte) (*Client, error) {
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Timeout: 10 * time.Second}) c := &http.Client{Timeout: 10 * time.Second}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope) conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
if err != nil {
return nil, err
}
return &Client{conf.Client(ctx)}, err val := conf.Client(ctx).Transport.(*oauth2.Transport)
_, err = val.Source.Token()
if err != nil {
return nil, err
}
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
if err != nil {
return nil, err
}
return &Client{service}, err
} }
// NewWithClient returns http client which includes the custom http client. // NewWithClient returns http client which includes the custom http client.
func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) { func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
if cli == nil {
return nil, fmt.Errorf("client is nil")
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli) ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli)
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope) conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
@@ -55,7 +79,12 @@ func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
return nil, err return nil, err
} }
return &Client{conf.Client(ctx)}, err service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
if err != nil {
return nil, err
}
return &Client{service}, err
} }
// AcknowledgeSubscription acknowledges a subscription purchase. // AcknowledgeSubscription acknowledges a subscription purchase.
@@ -66,13 +95,8 @@ func (c *Client) AcknowledgeSubscription(
token string, token string,
req *androidpublisher.SubscriptionPurchasesAcknowledgeRequest, req *androidpublisher.SubscriptionPurchasesAcknowledgeRequest,
) error { ) error {
service, err := androidpublisher.New(c.httpCli) ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
if err != nil { err := ps.Acknowledge(packageName, subscriptionID, token, req).Context(ctx).Do()
return err
}
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
err = ps.Acknowledge(packageName, subscriptionID, token, req).Context(ctx).Do()
return err return err
} }
@@ -84,12 +108,7 @@ func (c *Client) VerifySubscription(
subscriptionID string, subscriptionID string,
token string, token string,
) (*androidpublisher.SubscriptionPurchase, error) { ) (*androidpublisher.SubscriptionPurchase, error) {
service, err := androidpublisher.New(c.httpCli) ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
if err != nil {
return nil, err
}
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
result, err := ps.Get(packageName, subscriptionID, token).Context(ctx).Do() result, err := ps.Get(packageName, subscriptionID, token).Context(ctx).Do()
return result, err return result, err
@@ -102,26 +121,24 @@ func (c *Client) VerifyProduct(
productID string, productID string,
token string, token string,
) (*androidpublisher.ProductPurchase, error) { ) (*androidpublisher.ProductPurchase, error) {
service, err := androidpublisher.New(c.httpCli) ps := androidpublisher.NewPurchasesProductsService(c.service)
if err != nil {
return nil, err
}
ps := androidpublisher.NewPurchasesProductsService(service)
result, err := ps.Get(packageName, productID, token).Context(ctx).Do() result, err := ps.Get(packageName, productID, token).Context(ctx).Do()
return result, err return result, err
} }
func (c *Client) AcknowledgeProduct(ctx context.Context, packageName, productID, token, developerPayload string) error {
ps := androidpublisher.NewPurchasesProductsService(c.service)
acknowledgeRequest := &androidpublisher.ProductPurchasesAcknowledgeRequest{DeveloperPayload: developerPayload}
err := ps.Acknowledge(packageName, productID, token, acknowledgeRequest).Context(ctx).Do()
return err
}
// CancelSubscription cancels a user's subscription purchase. // CancelSubscription cancels a user's subscription purchase.
func (c *Client) CancelSubscription(ctx context.Context, 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.httpCli) ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
if err != nil { err := ps.Cancel(packageName, subscriptionID, token).Context(ctx).Do()
return err
}
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
err = ps.Cancel(packageName, subscriptionID, token).Context(ctx).Do()
return err return err
} }
@@ -129,13 +146,8 @@ func (c *Client) CancelSubscription(ctx context.Context, packageName string, sub
// RefundSubscription refunds a user's subscription purchase, but the subscription remains valid // RefundSubscription refunds a user's subscription purchase, but the subscription remains valid
// until its expiration time and it will continue to recur. // until its expiration time and it will continue to recur.
func (c *Client) RefundSubscription(ctx context.Context, 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.httpCli) ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
if err != nil { err := ps.Refund(packageName, subscriptionID, token).Context(ctx).Do()
return err
}
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
err = ps.Refund(packageName, subscriptionID, token).Context(ctx).Do()
return err return err
} }
@@ -143,13 +155,8 @@ func (c *Client) RefundSubscription(ctx context.Context, packageName string, sub
// RevokeSubscription refunds and immediately revokes a user's subscription purchase. // RevokeSubscription refunds and immediately revokes a user's subscription purchase.
// Access to the subscription will be terminated immediately and it will stop recurring. // Access to the subscription will be terminated immediately and it will stop recurring.
func (c *Client) RevokeSubscription(ctx context.Context, 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.httpCli) ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
if err != nil { err := ps.Revoke(packageName, subscriptionID, token).Context(ctx).Do()
return err
}
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
err = ps.Revoke(packageName, subscriptionID, token).Context(ctx).Do()
return err return err
} }

View File

@@ -7,8 +7,7 @@ import (
"reflect" "reflect"
"testing" "testing"
"golang.org/x/oauth2" "google.golang.org/api/androidpublisher/v3"
androidpublisher "google.golang.org/api/androidpublisher/v3"
"google.golang.org/appengine/urlfetch" "google.golang.org/appengine/urlfetch"
) )
@@ -36,22 +35,19 @@ func TestNew(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario // Exception scenario
expected := "oauth2: cannot fetch token: 400 Bad Request\nResponse: {\n \"error\": \"invalid_grant\",\n \"error_description\": \"Invalid issuer: Not a service account.\"\n}" expected := "oauth2: cannot fetch token: 400 Bad Request\nResponse: {\"error\":\"invalid_grant\",\"error_description\":\"Invalid JWT: iss field missing.\"}"
actual, _ := New(dummyKey) _, err := New(dummyKey)
val := actual.httpCli.Transport.(*oauth2.Transport) if err == nil || err.Error() != expected {
token, err := val.Source.Token()
if token != nil {
t.Errorf("got %#v", token)
}
if err.Error() != expected {
t.Errorf("got %v\nwant %v", err, expected) t.Errorf("got %v\nwant %v", err, expected)
} }
// TODO Normal scenario _, actual := New(nil)
actual, _ = New(jsonKey) if actual == nil || actual.Error() != "unexpected end of JSON input" {
val = actual.httpCli.Transport.(*oauth2.Transport) t.Errorf("got %v\nwant %v", actual, expected)
token, err = val.Source.Token() }
_, err = New(jsonKey)
if err != nil { if err != nil {
t.Errorf("got %#v", err) t.Errorf("got %#v", err)
} }
@@ -63,18 +59,35 @@ func TestNewWithClient(t *testing.T) {
ctx := context.Background() ctx := context.Background()
httpClient := urlfetch.Client(ctx) httpClient := urlfetch.Client(ctx)
cli, _ := NewWithClient(dummyKey, httpClient) _, err := NewWithClient(dummyKey, httpClient)
tr, _ := cli.httpCli.Transport.(*oauth2.Transport) if err != nil {
if !reflect.DeepEqual(tr.Base, httpClient.Transport) {
t.Errorf("transport should be urlfetch's one") t.Errorf("transport should be urlfetch's one")
} }
} }
func TestNewWithClientErrors(t *testing.T) {
t.Parallel()
expected := errors.New("client is nil")
_, actual := NewWithClient(dummyKey, nil)
if !reflect.DeepEqual(actual, expected) {
t.Errorf("got %v\nwant %v", actual, expected)
}
ctx := context.Background()
httpClient := urlfetch.Client(ctx)
_, actual = NewWithClient(nil, httpClient)
if actual == nil || actual.Error() != "unexpected end of JSON input" {
t.Errorf("got %v\nwant %v", actual, expected)
}
}
func TestAcknowledgeSubscription(t *testing.T) { func TestAcknowledgeSubscription(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario // Exception scenario
expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" expected := "googleapi: Error 400: Invalid Value, invalid"
client, _ := New(jsonKey) client, _ := New(jsonKey)
ctx := context.Background() ctx := context.Background()
@@ -83,7 +96,7 @@ func TestAcknowledgeSubscription(t *testing.T) {
} }
err := client.AcknowledgeSubscription(ctx, "package", "subscriptionID", "purchaseToken", req) err := client.AcknowledgeSubscription(ctx, "package", "subscriptionID", "purchaseToken", req)
if err.Error() != expected { if err == nil || err.Error() != expected {
t.Errorf("got %v\nwant %v", err, expected) t.Errorf("got %v\nwant %v", err, expected)
} }
@@ -93,76 +106,59 @@ func TestAcknowledgeSubscription(t *testing.T) {
func TestVerifySubscription(t *testing.T) { func TestVerifySubscription(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario // Exception scenario
expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" expected := "googleapi: Error 400: Invalid Value, invalid"
client, _ := New(jsonKey) client, _ := New(jsonKey)
ctx := context.Background() ctx := context.Background()
_, err := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken") _, err := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken")
if err.Error() != expected { if err == nil || err.Error() != expected {
t.Errorf("got %v\nwant %v", err, expected) t.Errorf("got %v\nwant %v", err, expected)
} }
// TODO Normal scenario // TODO Normal scenario
} }
func TestVerifySubscriptionAndroidPublisherError(t *testing.T) {
t.Parallel()
client := Client{nil}
expected := errors.New("client is nil")
ctx := context.Background()
_, actual := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken")
if !reflect.DeepEqual(actual, expected) {
t.Errorf("got %v\nwant %v", actual, expected)
}
}
func TestVerifyProduct(t *testing.T) { func TestVerifyProduct(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario // Exception scenario
expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound" expected := "googleapi: Error 400: Invalid Value, invalid"
client, _ := New(jsonKey) client, _ := New(jsonKey)
ctx := context.Background() ctx := context.Background()
_, err := client.VerifyProduct(ctx, "package", "productID", "purchaseToken") _, err := client.VerifyProduct(ctx, "package", "productID", "purchaseToken")
if err.Error() != expected { if err == nil || err.Error() != expected {
t.Errorf("got %v", err) t.Errorf("got %v", err)
} }
// TODO Normal scenario // TODO Normal scenario
} }
func TestVerifyProductAndroidPublisherError(t *testing.T) { func TestAcknowledgeProduct(t *testing.T) {
t.Parallel() t.Parallel()
client := Client{nil} // Exception scenario
expected := errors.New("client is nil") expected := "googleapi: Error 400: Invalid Value, invalid"
ctx := context.Background()
_, actual := client.VerifyProduct(ctx, "package", "productID", "purchaseToken")
if !reflect.DeepEqual(actual, expected) { client, _ := New(jsonKey)
t.Errorf("got %v\nwant %v", actual, expected) ctx := context.Background()
err := client.AcknowledgeProduct(ctx, "package", "productID", "purchaseToken", "")
if err == nil || err.Error() != expected {
t.Errorf("got %v", err)
} }
// TODO Normal scenario
} }
func TestCancelSubscription(t *testing.T) { func TestCancelSubscription(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario
client := &Client{nil}
expected := errors.New("client is nil")
ctx := context.Background() ctx := context.Background()
client, _ := New(jsonKey)
expectedStr := "googleapi: Error 400: Invalid Value, invalid"
actual := client.CancelSubscription(ctx, "package", "productID", "purchaseToken") actual := client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
if !reflect.DeepEqual(actual, expected) { if actual == nil || actual.Error() != expectedStr {
t.Errorf("got %v\nwant %v", actual, expected)
}
client, _ = New(jsonKey)
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
actual = client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
if actual.Error() != expectedStr {
t.Errorf("got %v\nwant %v", actual, expectedStr) t.Errorf("got %v\nwant %v", actual, expectedStr)
} }
@@ -171,21 +167,13 @@ func TestCancelSubscription(t *testing.T) {
func TestRefundSubscription(t *testing.T) { func TestRefundSubscription(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario
client := &Client{nil}
expected := errors.New("client is nil")
ctx := context.Background() ctx := context.Background()
client, _ := New(jsonKey)
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
actual := client.RefundSubscription(ctx, "package", "productID", "purchaseToken") actual := client.RefundSubscription(ctx, "package", "productID", "purchaseToken")
if !reflect.DeepEqual(actual, expected) { if actual == nil || actual.Error() != expectedStr {
t.Errorf("got %v\nwant %v", actual, expected)
}
client, _ = New(jsonKey)
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
actual = client.RefundSubscription(ctx, "package", "productID", "purchaseToken")
if actual.Error() != expectedStr {
t.Errorf("got %v\nwant %v", actual, expectedStr) t.Errorf("got %v\nwant %v", actual, expectedStr)
} }
@@ -194,21 +182,13 @@ func TestRefundSubscription(t *testing.T) {
func TestRevokeSubscription(t *testing.T) { func TestRevokeSubscription(t *testing.T) {
t.Parallel() t.Parallel()
// Exception scenario
client := &Client{nil}
expected := errors.New("client is nil")
ctx := context.Background() ctx := context.Background()
client, _ := New(jsonKey)
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
actual := client.RevokeSubscription(ctx, "package", "productID", "purchaseToken") actual := client.RevokeSubscription(ctx, "package", "productID", "purchaseToken")
if !reflect.DeepEqual(actual, expected) { if actual == nil || actual.Error() != expectedStr {
t.Errorf("got %v\nwant %v", actual, expected)
}
client, _ = New(jsonKey)
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
actual = client.RevokeSubscription(ctx, "package", "productID", "purchaseToken")
if actual.Error() != expectedStr {
t.Errorf("got %v\nwant %v", actual, expectedStr) t.Errorf("got %v\nwant %v", actual, expectedStr)
} }