candy_check 0.0.5 → 0.1.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/README.md +18 -2
- data/candy_check.gemspec +5 -5
- data/lib/candy_check/app_store.rb +2 -0
- data/lib/candy_check/app_store/receipt.rb +12 -0
- data/lib/candy_check/app_store/receipt_collection.rb +41 -0
- data/lib/candy_check/app_store/subscription_verification.rb +26 -0
- data/lib/candy_check/app_store/verifier.rb +18 -3
- data/lib/candy_check/play_store.rb +2 -0
- data/lib/candy_check/play_store/client.rb +36 -12
- data/lib/candy_check/play_store/subscription.rb +138 -0
- data/lib/candy_check/play_store/subscription_verification.rb +30 -0
- data/lib/candy_check/play_store/verifier.rb +20 -2
- data/lib/candy_check/utils/attribute_reader.rb +8 -0
- data/lib/candy_check/version.rb +1 -1
- data/spec/app_store/receipt_collection_spec.rb +65 -0
- data/spec/app_store/receipt_spec.rb +12 -1
- data/spec/app_store/subscription_verification_spec.rb +78 -0
- data/spec/app_store/verification_spec.rb +0 -10
- data/spec/app_store/verifier_spec.rb +33 -0
- data/spec/play_store/client_spec.rb +21 -0
- data/spec/play_store/subscription_spec.rb +138 -0
- data/spec/play_store/subscription_verification_spec.rb +98 -0
- data/spec/play_store/verifier_spec.rb +16 -0
- metadata +27 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45e40e19795d3d805ea61453fe6bea1d5e94ec3a
|
|
4
|
+
data.tar.gz: 9ba798923e9c36b324aa93c860f6ea75687e56bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6e4a4bd35ab65c5ad309052f8b27408b3bd570701a39674f9b6120900242ebe2199bf5814788a8fa9572774a8686cc3b57908eeda35223a5c7786b4e921181e
|
|
7
|
+
data.tar.gz: 8f3e4cd0adff6af8067667294fb439e05a840e69c9b24d73c0af0a607d348d4a8ffcbe3392e469fd2fab36e26155241739936ae91a8c1509f32595f7a1bf299d
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.1
|
|
1
|
+
2.3.1
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
|
@@ -18,8 +18,7 @@ gem install candy_check
|
|
|
18
18
|
|
|
19
19
|
## Introduction
|
|
20
20
|
|
|
21
|
-
This gem tries to simplify the process of server-side in-app purchase validation for Apple's AppStore and
|
|
22
|
-
Google's PlayStore.
|
|
21
|
+
This gem tries to simplify the process of server-side in-app purchase and subscription validation for Apple's AppStore and Google's PlayStore.
|
|
23
22
|
|
|
24
23
|
### AppStore
|
|
25
24
|
|
|
@@ -75,6 +74,15 @@ verifier.verify(your_receipt_data, your_secret)
|
|
|
75
74
|
|
|
76
75
|
Please see the class documenations [`CandyCheck::AppStore::Receipt`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/AppStore/Receipt) and [`CandyCheck::AppStore::VerificationFailure`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/AppStore/VerificationFailure) for further details about the responses.
|
|
77
76
|
|
|
77
|
+
For **subscription verification**, Apple also returns a list of the user's purchases. Essentially, this is a collection of receipts. To verify a subscription, do the following:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# ... create your verifier as above
|
|
81
|
+
verifier.verify_subscription(your_receipt_data, your_secret) # => ReceiptCollection or VerificationFailure
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Please see the class documentation for [`CandyCheck::AppStore::ReceiptCollection`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/AppStore/ReceiptCollection) for further details.
|
|
85
|
+
|
|
78
86
|
### PlayStore
|
|
79
87
|
|
|
80
88
|
First initialize and **boot** a verifier instance for your application. This loads the API discovery and
|
|
@@ -102,6 +110,14 @@ verifier.verify(package, product_id, token) # => Receipt or VerificationFailure
|
|
|
102
110
|
|
|
103
111
|
Please see the class documenations [`CandyCheck::PlayStore::Receipt`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/PlayStore/Receipt) and [`CandyCheck::PlayStore::VerificationFailure`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/PlayStore/VerificationFailure) for further details about the responses.
|
|
104
112
|
|
|
113
|
+
In order to **verify a subscription** from the Play Store, do the following:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
verifier.verify_subscription(package, subscription_id, token) # => Subscription or VerificationFailure
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Please see documenation for [`CandyCheck::PlayStore::Subscription`](http://www.rubydoc.info/github/jnbt/candy_check/master/CandyCheck/PlayStore/Subscription) for further details.
|
|
120
|
+
|
|
105
121
|
## CLI
|
|
106
122
|
|
|
107
123
|
This gem ships with an executable to verify in-app purchases directly from your terminal:
|
data/candy_check.gemspec
CHANGED
|
@@ -23,12 +23,12 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.add_dependency 'google-api-client', '~> 0.8.6'
|
|
24
24
|
spec.add_dependency 'thor', '~> 0.19'
|
|
25
25
|
|
|
26
|
-
spec.add_development_dependency 'rubocop', '~> 0.
|
|
26
|
+
spec.add_development_dependency 'rubocop', '~> 0.39'
|
|
27
27
|
spec.add_development_dependency 'inch', '~> 0.5'
|
|
28
28
|
spec.add_development_dependency 'bundler', '~> 1.7'
|
|
29
|
-
spec.add_development_dependency 'rake', '~>
|
|
30
|
-
spec.add_development_dependency 'coveralls', '~> 0.
|
|
31
|
-
spec.add_development_dependency 'minitest', '~> 5.
|
|
29
|
+
spec.add_development_dependency 'rake', '~> 11.1'
|
|
30
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
|
31
|
+
spec.add_development_dependency 'minitest', '~> 5.9'
|
|
32
32
|
spec.add_development_dependency 'minitest-around', '~> 0.3'
|
|
33
|
-
spec.add_development_dependency 'webmock', '~> 1
|
|
33
|
+
spec.add_development_dependency 'webmock', '~> 2.1'
|
|
34
34
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
require 'candy_check/app_store/client'
|
|
2
2
|
require 'candy_check/app_store/config'
|
|
3
3
|
require 'candy_check/app_store/receipt'
|
|
4
|
+
require 'candy_check/app_store/receipt_collection'
|
|
4
5
|
require 'candy_check/app_store/verification'
|
|
6
|
+
require 'candy_check/app_store/subscription_verification'
|
|
5
7
|
require 'candy_check/app_store/verification_failure'
|
|
6
8
|
require 'candy_check/app_store/verifier'
|
|
7
9
|
|
|
@@ -84,6 +84,18 @@ module CandyCheck
|
|
|
84
84
|
def cancellation_date
|
|
85
85
|
read_datetime_from_string('cancellation_date')
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
# The date of a subscription's expiration
|
|
89
|
+
# @return [DateTime]
|
|
90
|
+
def expires_date
|
|
91
|
+
read_datetime_from_string('expires_date')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# rubocop:disable PredicateName
|
|
95
|
+
def is_trial_period
|
|
96
|
+
# rubocop:enable PredicateName
|
|
97
|
+
read_bool('is_trial_period')
|
|
98
|
+
end
|
|
87
99
|
end
|
|
88
100
|
end
|
|
89
101
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module CandyCheck
|
|
2
|
+
module AppStore
|
|
3
|
+
# Store multiple {Receipt}s in order to perform collective operation on them
|
|
4
|
+
class ReceiptCollection
|
|
5
|
+
# Multiple receipts as in verfication response
|
|
6
|
+
# @return [Array<Receipt>]
|
|
7
|
+
attr_reader :receipts
|
|
8
|
+
|
|
9
|
+
# Initializes a new instance which bases on a JSON result
|
|
10
|
+
# from Apple's verification server
|
|
11
|
+
# @param attributes [Array<Hash>]
|
|
12
|
+
def initialize(attributes)
|
|
13
|
+
@receipts = attributes.map { |r| Receipt.new(r) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if the latest expiration date is passed
|
|
17
|
+
# @return [bool]
|
|
18
|
+
def expired?
|
|
19
|
+
overdue_days > 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if in trial
|
|
23
|
+
# @return [bool]
|
|
24
|
+
def trial?
|
|
25
|
+
@receipts.last.is_trial_period
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get latest expiration date
|
|
29
|
+
# @return [DateTime]
|
|
30
|
+
def expires_at
|
|
31
|
+
@receipts.last.expires_date
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get number of overdue days. If this is negative, it is not overdue.
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
def overdue_days
|
|
37
|
+
(Date.today - expires_at.to_date).to_i
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module CandyCheck
|
|
2
|
+
module AppStore
|
|
3
|
+
# Verifies a latest_receipt_info block against a verification server.
|
|
4
|
+
# The call return either an {ReceiptCollection} or a {VerificationFailure}
|
|
5
|
+
class SubscriptionVerification < CandyCheck::AppStore::Verification
|
|
6
|
+
# Performs the verification against the remote server
|
|
7
|
+
# @return [ReceiptCollection] if successful
|
|
8
|
+
# @return [VerificationFailure] otherwise
|
|
9
|
+
def call!
|
|
10
|
+
verify!
|
|
11
|
+
if valid?
|
|
12
|
+
ReceiptCollection.new(@response['latest_receipt_info'])
|
|
13
|
+
else
|
|
14
|
+
VerificationFailure.fetch(@response['status'])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
status_is_ok = @response['status'] == STATUS_OK
|
|
22
|
+
@response && status_is_ok && @response['latest_receipt_info']
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -30,6 +30,23 @@ module CandyCheck
|
|
|
30
30
|
# @return [Receipt] if successful
|
|
31
31
|
# @return [VerificationFailure] otherwise
|
|
32
32
|
def verify(receipt_data, secret = nil)
|
|
33
|
+
@verifier = Verification
|
|
34
|
+
fetch_receipt_information(receipt_data, secret)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Calls a subscription verification for the given input
|
|
38
|
+
# @param receipt_data [String] the raw data to be verified
|
|
39
|
+
# @param secret [string] the optional shared secret
|
|
40
|
+
# @return [ReceiptCollection] if successful
|
|
41
|
+
# @return [Verification] otherwise
|
|
42
|
+
def verify_subscription(receipt_data, secret = nil)
|
|
43
|
+
@verifier = SubscriptionVerification
|
|
44
|
+
fetch_receipt_information(receipt_data, secret)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def fetch_receipt_information(receipt_data, secret = nil)
|
|
33
50
|
default_endpoint, opposite_endpoint = endpoints
|
|
34
51
|
result = call_for(default_endpoint, receipt_data, secret)
|
|
35
52
|
if should_retry?(result)
|
|
@@ -38,10 +55,8 @@ module CandyCheck
|
|
|
38
55
|
result
|
|
39
56
|
end
|
|
40
57
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
58
|
def call_for(endpoint_url, receipt_data, secret)
|
|
44
|
-
|
|
59
|
+
@verifier.new(endpoint_url, receipt_data, secret).call!
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
def should_retry?(result)
|
|
@@ -4,7 +4,9 @@ require 'candy_check/play_store/discovery_repository'
|
|
|
4
4
|
require 'candy_check/play_store/client'
|
|
5
5
|
require 'candy_check/play_store/config'
|
|
6
6
|
require 'candy_check/play_store/receipt'
|
|
7
|
+
require 'candy_check/play_store/subscription'
|
|
7
8
|
require 'candy_check/play_store/verification'
|
|
9
|
+
require 'candy_check/play_store/subscription_verification'
|
|
8
10
|
require 'candy_check/play_store/verification_failure'
|
|
9
11
|
require 'candy_check/play_store/verifier'
|
|
10
12
|
|
|
@@ -26,14 +26,14 @@ module CandyCheck
|
|
|
26
26
|
# Initializes a client using a configuration.
|
|
27
27
|
# @param config [ClientConfig]
|
|
28
28
|
def initialize(config)
|
|
29
|
-
|
|
29
|
+
@config = config
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# Boots a client by discovering the API's services and then authorizes
|
|
33
33
|
# by fetching an access token.
|
|
34
34
|
# If the config has a cache_file the client tries to load discovery
|
|
35
35
|
def boot!
|
|
36
|
-
|
|
36
|
+
@api_client = Google::APIClient.new(
|
|
37
37
|
application_name: config.application_name,
|
|
38
38
|
application_version: config.application_version
|
|
39
39
|
)
|
|
@@ -48,22 +48,46 @@ module CandyCheck
|
|
|
48
48
|
# @param token [String] the purchase token
|
|
49
49
|
# @return [Hash] result of the API call
|
|
50
50
|
def verify(package, product_id, token)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
parameters = {
|
|
52
|
+
'packageName' => package,
|
|
53
|
+
'productId' => product_id,
|
|
54
|
+
'token' => token
|
|
55
|
+
}
|
|
56
|
+
execute(parameters, rpc.purchases.products.get)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Calls the remote API to load the product information for a specific
|
|
60
|
+
# combination of parameter which should be loaded from the client.
|
|
61
|
+
# @param package [String] the app's package name
|
|
62
|
+
# @param subscription_id [String] the app's item id
|
|
63
|
+
# @param token [String] the purchase token
|
|
64
|
+
# @return [Hash] result of the API call
|
|
65
|
+
def verify_subscription(package, subscription_id, token)
|
|
66
|
+
parameters = {
|
|
67
|
+
'packageName' => package,
|
|
68
|
+
'subscriptionId' => subscription_id,
|
|
69
|
+
'token' => token
|
|
70
|
+
}
|
|
71
|
+
execute(parameters, rpc.purchases.subscriptions.get)
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
private
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
attr_reader :config, :api_client, :rpc
|
|
77
|
+
|
|
78
|
+
# Execute api call through the API Client's HTTP command class
|
|
79
|
+
# @param parameters [hash] the parameters to send to the command
|
|
80
|
+
# @param api_method [Method] which api method to call
|
|
81
|
+
# @return [hash] the data response, as a hash
|
|
82
|
+
def execute(parameters, api_method)
|
|
83
|
+
api_client.execute(
|
|
84
|
+
api_method: api_method,
|
|
85
|
+
parameters: parameters
|
|
86
|
+
).data.to_hash
|
|
87
|
+
end
|
|
64
88
|
|
|
65
89
|
def discover!
|
|
66
|
-
|
|
90
|
+
@rpc = load_discover_dump || request_discover
|
|
67
91
|
validate_rpc!
|
|
68
92
|
write_discover_dump
|
|
69
93
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module CandyCheck
|
|
2
|
+
module PlayStore
|
|
3
|
+
# Describes a succeful subscription validation
|
|
4
|
+
class Subscription
|
|
5
|
+
include Utils::AttributeReader
|
|
6
|
+
|
|
7
|
+
# @return [Hash] the raw attributes returned from the server
|
|
8
|
+
attr_reader :attributes
|
|
9
|
+
|
|
10
|
+
# The payment of the subscription is pending (paymentState)
|
|
11
|
+
PAYMENT_PENDING = 0
|
|
12
|
+
# The payment of the subscript is received (paymentState)
|
|
13
|
+
PAYMENT_RECEIVED = 1
|
|
14
|
+
# The subscription was canceled by the user (cancelReason)
|
|
15
|
+
PAYMENT_CANCELED = 0
|
|
16
|
+
# The payment failed during processing (cancelReason)
|
|
17
|
+
PAYMENT_FAILED = 1
|
|
18
|
+
|
|
19
|
+
# Initializes a new instance which bases on a JSON result
|
|
20
|
+
# from Google's servers
|
|
21
|
+
# @param attributes [Hash]
|
|
22
|
+
def initialize(attributes)
|
|
23
|
+
@attributes = attributes
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if the expiration date is passed
|
|
27
|
+
# @return [bool]
|
|
28
|
+
def expired?
|
|
29
|
+
overdue_days > 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Check if in trial. This is actually not given by Google, but we assume
|
|
33
|
+
# that it is a trial going on if the paid amount is 0 and
|
|
34
|
+
# renewal is activated.
|
|
35
|
+
# @return [bool]
|
|
36
|
+
def trial?
|
|
37
|
+
price_is_zero = price_amount_micros == 0
|
|
38
|
+
price_is_zero && payment_received?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# see if payment is ok
|
|
42
|
+
# @return [bool]
|
|
43
|
+
def payment_received?
|
|
44
|
+
payment_state == PAYMENT_RECEIVED
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# see if payment is pending
|
|
48
|
+
# @return [bool]
|
|
49
|
+
def payment_pending?
|
|
50
|
+
payment_state == PAYMENT_PENDING
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# see if payment has failed according to Google
|
|
54
|
+
# @return [bool]
|
|
55
|
+
def payment_failed?
|
|
56
|
+
cancel_reason == PAYMENT_FAILED
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# see if this the user has canceled its subscription
|
|
60
|
+
# @return [bool]
|
|
61
|
+
def canceled_by_user?
|
|
62
|
+
cancel_reason == PAYMENT_CANCELED
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get number of overdue days. If this is negative, it is not overdue.
|
|
66
|
+
# @return [Integer]
|
|
67
|
+
def overdue_days
|
|
68
|
+
(Date.today - expires_at.to_date).to_i
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the auto renewal status as given by Google
|
|
72
|
+
# @return [bool] true if renewing automatically, false otherwise
|
|
73
|
+
def auto_renewing?
|
|
74
|
+
read_bool('autoRenewing')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get the payment state as given by Google
|
|
78
|
+
# @return [Integer]
|
|
79
|
+
def payment_state
|
|
80
|
+
read_integer('paymentState')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get the price amount for the subscription in micros in the payd currency
|
|
84
|
+
# @return [Integer]
|
|
85
|
+
def price_amount_micros
|
|
86
|
+
read_integer('priceAmountMicros')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get the cancel reason, as given by Google
|
|
90
|
+
# @return [Integer]
|
|
91
|
+
def cancel_reason
|
|
92
|
+
read_integer('cancelReason')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get the kind of subscription as stored in the android publisher service
|
|
96
|
+
# @return [String]
|
|
97
|
+
def kind
|
|
98
|
+
read('kind')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get developer-specified supplemental information about the order
|
|
102
|
+
# @return [String]
|
|
103
|
+
def developer_payload
|
|
104
|
+
read('developerPayload')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get the currency code in ISO 4217 format, e.g. "GBP" for British pounds
|
|
108
|
+
# @return [String]
|
|
109
|
+
def price_currency_code
|
|
110
|
+
read('priceCurrencyCode')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get start time for subscription in milliseconds since Epoch
|
|
114
|
+
# @return [Integer]
|
|
115
|
+
def start_time_millis
|
|
116
|
+
read_integer('startTimeMillis')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Get expiry time for subscription in milliseconds since Epoch
|
|
120
|
+
# @return [Integer]
|
|
121
|
+
def expiry_time_millis
|
|
122
|
+
read_integer('expiryTimeMillis')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get start time in UTC
|
|
126
|
+
# @return [DateTime]
|
|
127
|
+
def starts_at
|
|
128
|
+
read_datetime_from_millis('startTimeMillis')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get expiration time in UTC
|
|
132
|
+
# @return [DateTime]
|
|
133
|
+
def expires_at
|
|
134
|
+
read_datetime_from_millis('expiryTimeMillis')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module CandyCheck
|
|
2
|
+
module PlayStore
|
|
3
|
+
# Verifies a purchase token against the Google API
|
|
4
|
+
# The call return either an {Receipt} or an {VerificationFailure}
|
|
5
|
+
class SubscriptionVerification < Verification
|
|
6
|
+
# Performs the verification against the remote server
|
|
7
|
+
# @return [Subscription] if successful
|
|
8
|
+
# @return [VerificationFailure] otherwise
|
|
9
|
+
def call!
|
|
10
|
+
verify!
|
|
11
|
+
if valid?
|
|
12
|
+
Subscription.new(@response)
|
|
13
|
+
else
|
|
14
|
+
VerificationFailure.new(@response['error'])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
ok_kind = @response['kind'] == 'androidpublisher#subscriptionPurchase'
|
|
22
|
+
@response && @response['expiryTimeMillis'] && ok_kind
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verify!
|
|
26
|
+
@response = @client.verify_subscription(package, product_id, token)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -19,7 +19,7 @@ module CandyCheck
|
|
|
19
19
|
|
|
20
20
|
# Boot the module
|
|
21
21
|
def boot!
|
|
22
|
-
boot_error('You\'re only allowed to boot the verifier once') if
|
|
22
|
+
boot_error('You\'re only allowed to boot the verifier once') if booted?
|
|
23
23
|
@client = Client.new(config)
|
|
24
24
|
@client.boot!
|
|
25
25
|
end
|
|
@@ -36,10 +36,28 @@ module CandyCheck
|
|
|
36
36
|
verification.call!
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Contacts the Google API and requests the product state
|
|
40
|
+
# @param package [String] to query
|
|
41
|
+
# @param subscription_id [String] to query
|
|
42
|
+
# @param token [String] to use for authentication
|
|
43
|
+
# @return [Receipt] if successful
|
|
44
|
+
# @return [VerificationFailure] otherwise
|
|
45
|
+
def verify_subscription(package, subscription_id, token)
|
|
46
|
+
check_boot!
|
|
47
|
+
v = SubscriptionVerification.new(
|
|
48
|
+
@client, package, subscription_id, token
|
|
49
|
+
)
|
|
50
|
+
v.call!
|
|
51
|
+
end
|
|
52
|
+
|
|
39
53
|
private
|
|
40
54
|
|
|
55
|
+
def booted?
|
|
56
|
+
instance_variable_defined?(:@client)
|
|
57
|
+
end
|
|
58
|
+
|
|
41
59
|
def check_boot!
|
|
42
|
-
return if
|
|
60
|
+
return if booted?
|
|
43
61
|
boot_error 'You need to boot the verifier service first: '\
|
|
44
62
|
'CandyCheck::PlayStore::Verifier#boot!'
|
|
45
63
|
end
|
|
@@ -18,6 +18,14 @@ module CandyCheck
|
|
|
18
18
|
(val = read(field)) && val.to_i
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# @return [bool] if value is either 'true' or 'false'
|
|
22
|
+
# @return [nil] if value is not 'true'/'false'
|
|
23
|
+
def read_bool(field)
|
|
24
|
+
val = read(field).to_s
|
|
25
|
+
return nil unless %w(false true).include?(val)
|
|
26
|
+
val == 'true'
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
def read_datetime_from_string(field)
|
|
22
30
|
(val = read(field)) && DateTime.parse(val)
|
|
23
31
|
end
|
data/lib/candy_check/version.rb
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe CandyCheck::AppStore::ReceiptCollection do
|
|
4
|
+
subject { CandyCheck::AppStore::ReceiptCollection.new(attributes) }
|
|
5
|
+
|
|
6
|
+
describe 'overdue subscription' do
|
|
7
|
+
let(:attributes) do
|
|
8
|
+
[{
|
|
9
|
+
'expires_date' => '2014-04-15 12:52:40 Etc/GMT',
|
|
10
|
+
'expires_date_pst' => '2014-04-15 05:52:40 America/Los_Angeles',
|
|
11
|
+
'is_trial_period' => 'false'
|
|
12
|
+
}, {
|
|
13
|
+
'expires_date' => '2015-04-15 12:52:40 Etc/GMT',
|
|
14
|
+
'expires_date_pst' => '2015-04-15 05:52:40 America/Los_Angeles',
|
|
15
|
+
'is_trial_period' => 'false'
|
|
16
|
+
}]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'is expired' do
|
|
20
|
+
subject.expired?.must_be_true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'is not a trial' do
|
|
24
|
+
subject.trial?.must_be_false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'has positive overdue days' do
|
|
28
|
+
overdue = subject.overdue_days
|
|
29
|
+
overdue.must_be_instance_of Fixnum
|
|
30
|
+
assert overdue > 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'has a last expires date' do
|
|
34
|
+
expected = DateTime.new(2015, 4, 15, 12, 52, 40)
|
|
35
|
+
subject.expires_at.must_equal expected
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'unexpired trial subscription' do
|
|
40
|
+
two_days_from_now = DateTime.now + 2
|
|
41
|
+
|
|
42
|
+
let(:attributes) do
|
|
43
|
+
[{
|
|
44
|
+
'expires_date' => '2016-04-15 12:52:40 Etc/GMT',
|
|
45
|
+
'is_trial_period' => 'true'
|
|
46
|
+
}, {
|
|
47
|
+
'expires_date' =>
|
|
48
|
+
two_days_from_now.strftime('%Y-%m-%d %H:%M:%S Etc/GMT'),
|
|
49
|
+
'is_trial_period' => 'true'
|
|
50
|
+
}]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'has not expired' do
|
|
54
|
+
subject.expired?.must_be_false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'it is a trial' do
|
|
58
|
+
subject.trial?.must_be_true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'expires in two days' do
|
|
62
|
+
subject.overdue_days.must_equal(-2)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -23,7 +23,9 @@ describe CandyCheck::AppStore::Receipt do
|
|
|
23
23
|
'purchase_date_pst' => '2015-01-09 03:40:46' \
|
|
24
24
|
' America/Los_Angeles',
|
|
25
25
|
'bid' => 'some.test.app',
|
|
26
|
-
'original_purchase_date_ms' => '1420717246868'
|
|
26
|
+
'original_purchase_date_ms' => '1420717246868',
|
|
27
|
+
'expires_date' => '2016-06-09 13:59:40 Etc/GMT',
|
|
28
|
+
'is_trial_period' => 'false'
|
|
27
29
|
}
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -77,6 +79,15 @@ describe CandyCheck::AppStore::Receipt do
|
|
|
77
79
|
it 'returns raw attributes' do
|
|
78
80
|
subject.attributes.must_be_same_as attributes
|
|
79
81
|
end
|
|
82
|
+
|
|
83
|
+
it 'returns the subscription expiration date' do
|
|
84
|
+
expected = DateTime.new(2016, 6, 9, 13, 59, 40)
|
|
85
|
+
subject.expires_date.must_equal expected
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'returns the trial status' do
|
|
89
|
+
subject.is_trial_period.must_be_false
|
|
90
|
+
end
|
|
80
91
|
end
|
|
81
92
|
|
|
82
93
|
describe 'valid transaction' do
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe CandyCheck::AppStore::SubscriptionVerification do
|
|
4
|
+
subject do
|
|
5
|
+
CandyCheck::AppStore::SubscriptionVerification.new(endpoint, data, secret)
|
|
6
|
+
end
|
|
7
|
+
let(:endpoint) { 'https://some.endpoint' }
|
|
8
|
+
let(:data) { 'some_data' }
|
|
9
|
+
let(:secret) { 'some_secret' }
|
|
10
|
+
|
|
11
|
+
it 'returns a verification failure for status != 0' do
|
|
12
|
+
with_mocked_response('status' => 21_000) do |client, recorded|
|
|
13
|
+
result = subject.call!
|
|
14
|
+
client.receipt_data.must_equal data
|
|
15
|
+
client.secret.must_equal secret
|
|
16
|
+
|
|
17
|
+
recorded.first.must_equal [endpoint]
|
|
18
|
+
|
|
19
|
+
result.must_be_instance_of CandyCheck::AppStore::VerificationFailure
|
|
20
|
+
result.code.must_equal 21_000
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns a verification failure when receipt is missing' do
|
|
25
|
+
with_mocked_response({}) do |client, recorded|
|
|
26
|
+
result = subject.call!
|
|
27
|
+
client.receipt_data.must_equal data
|
|
28
|
+
client.secret.must_equal secret
|
|
29
|
+
|
|
30
|
+
recorded.first.must_equal [endpoint]
|
|
31
|
+
|
|
32
|
+
result.must_be_instance_of CandyCheck::AppStore::VerificationFailure
|
|
33
|
+
result.code.must_equal(-1)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns a collection of receipt when status is 0 and receipts exists' do
|
|
38
|
+
response = {
|
|
39
|
+
'status' => 0,
|
|
40
|
+
'latest_receipt_info' => [
|
|
41
|
+
{ 'item_id' => 'some_id' },
|
|
42
|
+
{ 'item_id' => 'some_other_id' }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
with_mocked_response(response) do
|
|
46
|
+
result = subject.call!
|
|
47
|
+
result.must_be_instance_of CandyCheck::AppStore::ReceiptCollection
|
|
48
|
+
result.receipts.must_be_instance_of Array
|
|
49
|
+
last = result.receipts.last
|
|
50
|
+
last.must_be_instance_of CandyCheck::AppStore::Receipt
|
|
51
|
+
last.item_id.must_equal('some_other_id')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
DummyClient = Struct.new(:response) do
|
|
58
|
+
attr_reader :receipt_data, :secret
|
|
59
|
+
|
|
60
|
+
def verify(receipt_data, secret)
|
|
61
|
+
@receipt_data = receipt_data
|
|
62
|
+
@secret = secret
|
|
63
|
+
response
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def with_mocked_response(response)
|
|
68
|
+
recorded = []
|
|
69
|
+
dummy = DummyClient.new(response)
|
|
70
|
+
stub = proc do |*args|
|
|
71
|
+
recorded << args
|
|
72
|
+
dummy
|
|
73
|
+
end
|
|
74
|
+
CandyCheck::AppStore::Client.stub :new, stub do
|
|
75
|
+
yield dummy, recorded
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -43,16 +43,6 @@ describe CandyCheck::AppStore::Verification do
|
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
|
-
DummyClient = Struct.new(:response) do
|
|
47
|
-
attr_reader :receipt_data, :secret
|
|
48
|
-
|
|
49
|
-
def verify(receipt_data, secret)
|
|
50
|
-
@receipt_data = receipt_data
|
|
51
|
-
@secret = secret
|
|
52
|
-
response
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
46
|
def with_mocked_response(response)
|
|
57
47
|
recorded = []
|
|
58
48
|
dummy = DummyClient.new(response)
|
|
@@ -9,6 +9,7 @@ describe CandyCheck::AppStore::Verifier do
|
|
|
9
9
|
let(:data) { 'some_data' }
|
|
10
10
|
let(:secret) { 'some_secret' }
|
|
11
11
|
let(:receipt) { CandyCheck::AppStore::Receipt.new({}) }
|
|
12
|
+
let(:receipt_collection) { CandyCheck::AppStore::ReceiptCollection.new({}) }
|
|
12
13
|
let(:production_endpoint) do
|
|
13
14
|
'https://buy.itunes.apple.com/verifyReceipt'
|
|
14
15
|
end
|
|
@@ -80,6 +81,38 @@ describe CandyCheck::AppStore::Verifier do
|
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
84
|
+
describe 'subscription' do
|
|
85
|
+
let(:environment) { :production }
|
|
86
|
+
|
|
87
|
+
it 'uses production endpoint without retry on success' do
|
|
88
|
+
with_mocked_verifier(receipt_collection) do
|
|
89
|
+
subject.verify_subscription(
|
|
90
|
+
data, secret
|
|
91
|
+
).must_be_same_as receipt_collection
|
|
92
|
+
assert_recorded([production_endpoint, data, secret])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'only uses production endpoint for normal failures' do
|
|
97
|
+
failure = get_failure(21_000)
|
|
98
|
+
with_mocked_verifier(failure) do
|
|
99
|
+
subject.verify_subscription(data, secret).must_be_same_as failure
|
|
100
|
+
assert_recorded([production_endpoint, data, secret])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'retries production endpoint for redirect error' do
|
|
105
|
+
failure = get_failure(21_007)
|
|
106
|
+
with_mocked_verifier(failure, receipt) do
|
|
107
|
+
subject.verify_subscription(data, secret).must_be_same_as receipt
|
|
108
|
+
assert_recorded(
|
|
109
|
+
[production_endpoint, data, secret],
|
|
110
|
+
[sandbox_endpoint, data, secret]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
83
116
|
private
|
|
84
117
|
|
|
85
118
|
def with_mocked_verifier(*results)
|
|
@@ -64,6 +64,20 @@ describe CandyCheck::PlayStore::Client do
|
|
|
64
64
|
result['error']['errors'].size.must_equal 1
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
it 'returns the products call result\'s data even if it is a failure' \
|
|
68
|
+
' when verifying subscription' do
|
|
69
|
+
bootup!
|
|
70
|
+
|
|
71
|
+
mock_subscriptions_request!('products_failure.txt')
|
|
72
|
+
result = subject.verify_subscription('the_package', 'the_id', 'the_token')
|
|
73
|
+
result.must_be_instance_of Hash
|
|
74
|
+
|
|
75
|
+
result['error']['code'].must_equal 401
|
|
76
|
+
result['error']['message'].must_equal 'The current user has insufficient' \
|
|
77
|
+
' permissions to perform the requested operation.'
|
|
78
|
+
result['error']['errors'].size.must_equal 1
|
|
79
|
+
end
|
|
80
|
+
|
|
67
81
|
it 'returns the products call result\'s data for a successful call' do
|
|
68
82
|
bootup!
|
|
69
83
|
mock_request!('products_success.txt')
|
|
@@ -101,4 +115,11 @@ describe CandyCheck::PlayStore::Client do
|
|
|
101
115
|
'applications/the_package/purchases/products/the_id/tokens/the_token')
|
|
102
116
|
.to_return(fixture_content('play_store', file))
|
|
103
117
|
end
|
|
118
|
+
|
|
119
|
+
def mock_subscriptions_request!(file)
|
|
120
|
+
stub_request(:get, 'https://www.googleapis.com/androidpublisher/v2/' \
|
|
121
|
+
'applications/the_package/purchases/subscriptions/' \
|
|
122
|
+
'the_id/tokens/the_token')
|
|
123
|
+
.to_return(fixture_content('play_store', file))
|
|
124
|
+
end
|
|
104
125
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe CandyCheck::PlayStore::Subscription do
|
|
4
|
+
subject { CandyCheck::PlayStore::Subscription.new(attributes) }
|
|
5
|
+
|
|
6
|
+
describe 'expired and canceled subscription' do
|
|
7
|
+
let(:attributes) do
|
|
8
|
+
{
|
|
9
|
+
'kind' => 'androidpublisher#subscriptionPurchase',
|
|
10
|
+
'startTimeMillis' => '1459540113244',
|
|
11
|
+
'expiryTimeMillis' => '1462132088610',
|
|
12
|
+
'autoRenewing' => false,
|
|
13
|
+
'developerPayload' => 'payload that gets stored and returned',
|
|
14
|
+
'cancelReason' => 0,
|
|
15
|
+
'paymentState' => '1'
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'is expired?' do
|
|
20
|
+
subject.expired?.must_be_true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'is canceled by user' do
|
|
24
|
+
subject.canceled_by_user?.must_be_true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'returns the payment_state' do
|
|
28
|
+
subject.payment_state.must_equal 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'considers a payment as valid' do
|
|
32
|
+
subject.payment_received?.must_be_true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'checks that auto renewal status is false' do
|
|
36
|
+
subject.auto_renewing?.must_be_false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'returns the developer_payload' do
|
|
40
|
+
subject.developer_payload.must_equal \
|
|
41
|
+
'payload that gets stored and returned'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'returns the kind' do
|
|
45
|
+
subject.kind.must_equal 'androidpublisher#subscriptionPurchase'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'returns the start_time_millis' do
|
|
49
|
+
subject.start_time_millis.must_equal 145_954_011_324_4
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns the expiry_time_millis' do
|
|
53
|
+
subject.expiry_time_millis.must_equal 146_213_208_861_0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'returns the starts_at' do
|
|
57
|
+
expected = DateTime.new(2016, 4, 1, 19, 48, 33)
|
|
58
|
+
subject.starts_at.must_equal expected
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'returns the expires_at' do
|
|
62
|
+
expected = DateTime.new(2016, 5, 1, 19, 48, 8)
|
|
63
|
+
subject.expires_at.must_equal expected
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
describe 'unexpired and renewing subscription' do
|
|
68
|
+
two_days_from_now = DateTime.now + 2
|
|
69
|
+
let(:attributes) do
|
|
70
|
+
{
|
|
71
|
+
'expiryTimeMillis' => (two_days_from_now.to_time.to_i * 1000).to_s,
|
|
72
|
+
'autoRenewing' => true
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'is expired?' do
|
|
77
|
+
subject.expired?.must_be_false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'is two days left until it is overdue' do
|
|
81
|
+
subject.overdue_days.must_equal(-2)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe 'expired due to payment failure' do
|
|
86
|
+
let(:attributes) do
|
|
87
|
+
{
|
|
88
|
+
'expiryTimeMillis' => '1462132088610',
|
|
89
|
+
'autoRenewing' => true,
|
|
90
|
+
'cancelReason' => 1
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'is expired?' do
|
|
95
|
+
subject.expired?.must_be_true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'is payment_failed?' do
|
|
99
|
+
subject.payment_failed?.must_be_true
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
describe 'expired with pending payment' do
|
|
104
|
+
let(:attributes) do
|
|
105
|
+
{
|
|
106
|
+
'expiryTimeMillis' => '1462132088610',
|
|
107
|
+
'autoRenewing' => true,
|
|
108
|
+
'paymentState' => 0
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'is expired?' do
|
|
113
|
+
subject.expired?.must_be_true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'is payment_pending?' do
|
|
117
|
+
subject.payment_pending?.must_be_true
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe 'trial' do
|
|
122
|
+
let(:attributes) do
|
|
123
|
+
{
|
|
124
|
+
'paymentState' => 1,
|
|
125
|
+
'priceCurrencyCode' => 'SOMECODE',
|
|
126
|
+
'priceAmountMicros' => '0'
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'is trual?' do
|
|
131
|
+
subject.trial?.must_be_true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'returns the price_currency_code' do
|
|
135
|
+
subject.price_currency_code.must_equal 'SOMECODE'
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe CandyCheck::PlayStore::SubscriptionVerification do
|
|
4
|
+
subject do
|
|
5
|
+
CandyCheck::PlayStore::SubscriptionVerification.new(
|
|
6
|
+
client, package, product_id, token
|
|
7
|
+
)
|
|
8
|
+
end
|
|
9
|
+
let(:client) { DummyGoogleSubsClient.new(response) }
|
|
10
|
+
let(:package) { 'the_package' }
|
|
11
|
+
let(:product_id) { 'the_product' }
|
|
12
|
+
let(:token) { 'the_token' }
|
|
13
|
+
|
|
14
|
+
describe 'valid' do
|
|
15
|
+
let(:response) do
|
|
16
|
+
{
|
|
17
|
+
'kind' => 'androidpublisher#subscriptionPurchase',
|
|
18
|
+
'startTimeMillis' => '1459540113244',
|
|
19
|
+
'expiryTimeMillis' => '1462132088610',
|
|
20
|
+
'autoRenewing' => false,
|
|
21
|
+
'developerPayload' => 'payload that gets stored and returned',
|
|
22
|
+
'cancelReason' => 0,
|
|
23
|
+
'paymentState' => '1'
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'calls the client with the correct paramters' do
|
|
28
|
+
subject.call!
|
|
29
|
+
client.package.must_equal package
|
|
30
|
+
client.product_id.must_equal product_id
|
|
31
|
+
client.token.must_equal token
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'returns a subscription' do
|
|
35
|
+
result = subject.call!
|
|
36
|
+
result.must_be_instance_of CandyCheck::PlayStore::Subscription
|
|
37
|
+
result.expired?.must_be_true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe 'failure' do
|
|
42
|
+
let(:response) do
|
|
43
|
+
{
|
|
44
|
+
'error' => {
|
|
45
|
+
'code' => 401,
|
|
46
|
+
'message' => 'The current user has insufficient permissions'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'returns a verification failure' do
|
|
52
|
+
result = subject.call!
|
|
53
|
+
result.must_be_instance_of CandyCheck::PlayStore::VerificationFailure
|
|
54
|
+
result.code.must_equal 401
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe 'empty' do
|
|
59
|
+
let(:response) do
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns a verification failure' do
|
|
64
|
+
result = subject.call!
|
|
65
|
+
result.must_be_instance_of CandyCheck::PlayStore::VerificationFailure
|
|
66
|
+
result.code.must_equal(-1)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe 'invalid response kind' do
|
|
71
|
+
let(:response) do
|
|
72
|
+
{
|
|
73
|
+
'kind' => 'something weird'
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'returns a verification failure' do
|
|
78
|
+
result = subject.call!
|
|
79
|
+
result.must_be_instance_of CandyCheck::PlayStore::VerificationFailure
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
DummyGoogleSubsClient = Struct.new(:response) do
|
|
86
|
+
attr_reader :package, :product_id, :token
|
|
87
|
+
|
|
88
|
+
def boot!
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def verify_subscription(package, product_id, token)
|
|
92
|
+
@package = package
|
|
93
|
+
@product_id = product_id
|
|
94
|
+
@token = token
|
|
95
|
+
response
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -52,6 +52,22 @@ describe CandyCheck::PlayStore::Verifier do
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
it 'uses a subscription verifier when booted' do
|
|
56
|
+
result = :stubbed
|
|
57
|
+
with_mocked_client do
|
|
58
|
+
subject.boot!
|
|
59
|
+
end
|
|
60
|
+
with_mocked_verifier(result) do
|
|
61
|
+
subject.verify_subscription(
|
|
62
|
+
package, product_id, token
|
|
63
|
+
).must_be_same_as result
|
|
64
|
+
|
|
65
|
+
assert_recorded(
|
|
66
|
+
[@client, package, product_id, token]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
55
71
|
private
|
|
56
72
|
|
|
57
73
|
def with_mocked_verifier(*results)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: candy_check
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.1.0.pre
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonas Thiel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2016-
|
|
11
|
+
date: 2016-10-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: multi_json
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0.
|
|
61
|
+
version: '0.39'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0.
|
|
68
|
+
version: '0.39'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: inch
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,42 +100,42 @@ dependencies:
|
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
103
|
+
version: '11.1'
|
|
104
104
|
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
110
|
+
version: '11.1'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
112
|
name: coveralls
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
114
114
|
requirements:
|
|
115
115
|
- - "~>"
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0.
|
|
117
|
+
version: '0.8'
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0.
|
|
124
|
+
version: '0.8'
|
|
125
125
|
- !ruby/object:Gem::Dependency
|
|
126
126
|
name: minitest
|
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
|
128
128
|
requirements:
|
|
129
129
|
- - "~>"
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '5.
|
|
131
|
+
version: '5.9'
|
|
132
132
|
type: :development
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '5.
|
|
138
|
+
version: '5.9'
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
140
|
name: minitest-around
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -156,14 +156,14 @@ dependencies:
|
|
|
156
156
|
requirements:
|
|
157
157
|
- - "~>"
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: '1
|
|
159
|
+
version: '2.1'
|
|
160
160
|
type: :development
|
|
161
161
|
prerelease: false
|
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
163
|
requirements:
|
|
164
164
|
- - "~>"
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: '1
|
|
166
|
+
version: '2.1'
|
|
167
167
|
description:
|
|
168
168
|
email:
|
|
169
169
|
- jonas@thiel.io
|
|
@@ -187,6 +187,8 @@ files:
|
|
|
187
187
|
- lib/candy_check/app_store/client.rb
|
|
188
188
|
- lib/candy_check/app_store/config.rb
|
|
189
189
|
- lib/candy_check/app_store/receipt.rb
|
|
190
|
+
- lib/candy_check/app_store/receipt_collection.rb
|
|
191
|
+
- lib/candy_check/app_store/subscription_verification.rb
|
|
190
192
|
- lib/candy_check/app_store/verification.rb
|
|
191
193
|
- lib/candy_check/app_store/verification_failure.rb
|
|
192
194
|
- lib/candy_check/app_store/verifier.rb
|
|
@@ -203,6 +205,8 @@ files:
|
|
|
203
205
|
- lib/candy_check/play_store/config.rb
|
|
204
206
|
- lib/candy_check/play_store/discovery_repository.rb
|
|
205
207
|
- lib/candy_check/play_store/receipt.rb
|
|
208
|
+
- lib/candy_check/play_store/subscription.rb
|
|
209
|
+
- lib/candy_check/play_store/subscription_verification.rb
|
|
206
210
|
- lib/candy_check/play_store/verification.rb
|
|
207
211
|
- lib/candy_check/play_store/verification_failure.rb
|
|
208
212
|
- lib/candy_check/play_store/verifier.rb
|
|
@@ -212,7 +216,9 @@ files:
|
|
|
212
216
|
- lib/candy_check/version.rb
|
|
213
217
|
- spec/app_store/client_spec.rb
|
|
214
218
|
- spec/app_store/config_spec.rb
|
|
219
|
+
- spec/app_store/receipt_collection_spec.rb
|
|
215
220
|
- spec/app_store/receipt_spec.rb
|
|
221
|
+
- spec/app_store/subscription_verification_spec.rb
|
|
216
222
|
- spec/app_store/verifcation_failure_spec.rb
|
|
217
223
|
- spec/app_store/verification_spec.rb
|
|
218
224
|
- spec/app_store/verifier_spec.rb
|
|
@@ -235,6 +241,8 @@ files:
|
|
|
235
241
|
- spec/play_store/config_spec.rb
|
|
236
242
|
- spec/play_store/discovery_respository_spec.rb
|
|
237
243
|
- spec/play_store/receipt_spec.rb
|
|
244
|
+
- spec/play_store/subscription_spec.rb
|
|
245
|
+
- spec/play_store/subscription_verification_spec.rb
|
|
238
246
|
- spec/play_store/verification_failure_spec.rb
|
|
239
247
|
- spec/play_store/verification_spec.rb
|
|
240
248
|
- spec/play_store/verifier_spec.rb
|
|
@@ -257,19 +265,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
257
265
|
version: '2.0'
|
|
258
266
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
259
267
|
requirements:
|
|
260
|
-
- - "
|
|
268
|
+
- - ">"
|
|
261
269
|
- !ruby/object:Gem::Version
|
|
262
|
-
version:
|
|
270
|
+
version: 1.3.1
|
|
263
271
|
requirements: []
|
|
264
272
|
rubyforge_project:
|
|
265
|
-
rubygems_version: 2.
|
|
273
|
+
rubygems_version: 2.5.1
|
|
266
274
|
signing_key:
|
|
267
275
|
specification_version: 4
|
|
268
276
|
summary: Check and verify in-app receipts
|
|
269
277
|
test_files:
|
|
270
278
|
- spec/app_store/client_spec.rb
|
|
271
279
|
- spec/app_store/config_spec.rb
|
|
280
|
+
- spec/app_store/receipt_collection_spec.rb
|
|
272
281
|
- spec/app_store/receipt_spec.rb
|
|
282
|
+
- spec/app_store/subscription_verification_spec.rb
|
|
273
283
|
- spec/app_store/verifcation_failure_spec.rb
|
|
274
284
|
- spec/app_store/verification_spec.rb
|
|
275
285
|
- spec/app_store/verifier_spec.rb
|
|
@@ -292,6 +302,8 @@ test_files:
|
|
|
292
302
|
- spec/play_store/config_spec.rb
|
|
293
303
|
- spec/play_store/discovery_respository_spec.rb
|
|
294
304
|
- spec/play_store/receipt_spec.rb
|
|
305
|
+
- spec/play_store/subscription_spec.rb
|
|
306
|
+
- spec/play_store/subscription_verification_spec.rb
|
|
295
307
|
- spec/play_store/verification_failure_spec.rb
|
|
296
308
|
- spec/play_store/verification_spec.rb
|
|
297
309
|
- spec/play_store/verifier_spec.rb
|