candy_check 0.0.5 → 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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