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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 414f845cc6a6fa6a27f814866675412925ed8a85
4
- data.tar.gz: a2699ef81d97d170e964bf08bcbd5949284f00ac
3
+ metadata.gz: 45e40e19795d3d805ea61453fe6bea1d5e94ec3a
4
+ data.tar.gz: 9ba798923e9c36b324aa93c860f6ea75687e56bc
5
5
  SHA512:
6
- metadata.gz: 9449d7d55b94fa560d8be6bdc2f1d082287e78561f6c935d4a2ca3836759ddd0ac3d6385052624436257c21f889e4e5bd087d48b57fe4b6736fd580b554d496d
7
- data.tar.gz: 15579d8441886b67bcb4d28a9c9404cc836e553821e4d9c46e8dfec8090fb929b5d6ac1baa7fc5c58c396869a79840143400b1bb632d32f4c016b4cd00b1dfcc
6
+ metadata.gz: f6e4a4bd35ab65c5ad309052f8b27408b3bd570701a39674f9b6120900242ebe2199bf5814788a8fa9572774a8686cc3b57908eeda35223a5c7786b4e921181e
7
+ data.tar.gz: 8f3e4cd0adff6af8067667294fb439e05a840e69c9b24d73c0af0a607d348d4a8ffcbe3392e469fd2fab36e26155241739936ae91a8c1509f32595f7a1bf299d
@@ -1,7 +1,7 @@
1
1
  AllCops:
2
2
  Exclude:
3
3
  - 'spec/support/*'
4
- - 'bin/*'
5
- - 'vendor/*'
4
+ - 'bin/**/*'
5
+ - 'vendor/**/*'
6
6
  Style/Documentation:
7
7
  Enabled: false
@@ -1 +1 @@
1
- 2.1.5
1
+ 2.3.1
@@ -2,6 +2,8 @@ language: ruby
2
2
  sudo: false
3
3
  cache: bundler
4
4
  rvm:
5
+ - '2.3.1'
6
+ - '2.3.0'
5
7
  - '2.2'
6
8
  - '2.1'
7
9
  - '2.0'
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:
@@ -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.28'
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', '~> 10.0'
30
- spec.add_development_dependency 'coveralls', '~> 0.7'
31
- spec.add_development_dependency 'minitest', '~> 5.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.20'
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
- Verification.new(endpoint_url, receipt_data, secret).call!
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
- self.config = config
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
- self.api_client = Google::APIClient.new(
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
- api_client.execute(
52
- api_method: rpc.purchases.products.get,
53
- parameters: {
54
- 'packageName' => package,
55
- 'productId' => product_id,
56
- 'token' => token
57
- }
58
- ).data.to_hash
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
- attr_accessor :config, :api_client, :rpc
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
- self.rpc = load_discover_dump || request_discover
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 @client
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 @client
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
@@ -1,4 +1,4 @@
1
1
  module CandyCheck
2
2
  # The current gem's version
3
- VERSION = '0.0.5'.freeze
3
+ VERSION = '0.1.0.pre'.freeze
4
4
  end
@@ -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.5
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-04-12 00:00:00.000000000 Z
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.28'
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.28'
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: '10.0'
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: '10.0'
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.7'
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.7'
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.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.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.20'
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.20'
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: '0'
270
+ version: 1.3.1
263
271
  requirements: []
264
272
  rubyforge_project:
265
- rubygems_version: 2.4.8
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