candy_check 0.1.0.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +23 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +7 -8
  5. data/Guardfile +42 -0
  6. data/MIGRATION_GUIDE_0_2_0.md +141 -0
  7. data/README.md +86 -26
  8. data/Rakefile +1 -1
  9. data/candy_check.gemspec +33 -25
  10. data/lib/candy_check/app_store/receipt_collection.rb +5 -3
  11. data/lib/candy_check/app_store/subscription_verification.rb +25 -1
  12. data/lib/candy_check/app_store/verification.rb +1 -1
  13. data/lib/candy_check/app_store/verifier.rb +11 -11
  14. data/lib/candy_check/cli/app.rb +16 -33
  15. data/lib/candy_check/cli/commands/play_store.rb +12 -13
  16. data/lib/candy_check/play_store.rb +20 -10
  17. data/lib/candy_check/play_store/acknowledger.rb +19 -0
  18. data/lib/candy_check/play_store/android_publisher_service.rb +6 -0
  19. data/lib/candy_check/play_store/product_acknowledgements/acknowledgement.rb +45 -0
  20. data/lib/candy_check/play_store/product_acknowledgements/response.rb +24 -0
  21. data/lib/candy_check/play_store/product_purchases/product_purchase.rb +90 -0
  22. data/lib/candy_check/play_store/product_purchases/product_verification.rb +53 -0
  23. data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +154 -0
  24. data/lib/candy_check/play_store/subscription_purchases/subscription_verification.rb +55 -0
  25. data/lib/candy_check/play_store/verification_failure.rb +8 -6
  26. data/lib/candy_check/play_store/verifier.rb +24 -49
  27. data/lib/candy_check/utils/config.rb +5 -3
  28. data/lib/candy_check/version.rb +1 -1
  29. data/spec/app_store/receipt_collection_spec.rb +33 -0
  30. data/spec/app_store/subscription_verification_spec.rb +35 -2
  31. data/spec/app_store/verifier_spec.rb +24 -5
  32. data/spec/candy_check_spec.rb +2 -2
  33. data/spec/cli/commands/play_store_spec.rb +10 -43
  34. data/spec/fixtures/play_store/random_dummy_key.json +12 -0
  35. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml +105 -0
  36. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml +124 -0
  37. data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml +122 -0
  38. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml +196 -0
  39. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml +183 -0
  40. data/spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml +122 -0
  41. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml +196 -0
  42. data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml +127 -0
  43. data/spec/play_store/acknowledger_spec.rb +48 -0
  44. data/spec/play_store/product_acknowledgements/acknowledgement_spec.rb +54 -0
  45. data/spec/play_store/product_acknowledgements/response_spec.rb +66 -0
  46. data/spec/play_store/product_purchases/product_purchase_spec.rb +110 -0
  47. data/spec/play_store/product_purchases/product_verification_spec.rb +49 -0
  48. data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +237 -0
  49. data/spec/play_store/subscription_purchases/subscription_verification_spec.rb +65 -0
  50. data/spec/play_store/verification_failure_spec.rb +18 -18
  51. data/spec/play_store/verifier_spec.rb +32 -96
  52. data/spec/spec_helper.rb +32 -10
  53. metadata +175 -75
  54. data/lib/candy_check/play_store/client.rb +0 -126
  55. data/lib/candy_check/play_store/config.rb +0 -51
  56. data/lib/candy_check/play_store/discovery_repository.rb +0 -33
  57. data/lib/candy_check/play_store/receipt.rb +0 -81
  58. data/lib/candy_check/play_store/subscription.rb +0 -138
  59. data/lib/candy_check/play_store/subscription_verification.rb +0 -30
  60. data/lib/candy_check/play_store/verification.rb +0 -48
  61. data/spec/fixtures/api_cache.dump +0 -1
  62. data/spec/fixtures/play_store/api_cache.dump +0 -1
  63. data/spec/fixtures/play_store/auth_failure.txt +0 -18
  64. data/spec/fixtures/play_store/auth_success.txt +0 -20
  65. data/spec/fixtures/play_store/discovery.txt +0 -2841
  66. data/spec/fixtures/play_store/dummy.p12 +0 -0
  67. data/spec/fixtures/play_store/empty.txt +0 -17
  68. data/spec/fixtures/play_store/products_failure.txt +0 -29
  69. data/spec/fixtures/play_store/products_success.txt +0 -22
  70. data/spec/play_store/client_spec.rb +0 -125
  71. data/spec/play_store/config_spec.rb +0 -96
  72. data/spec/play_store/discovery_respository_spec.rb +0 -31
  73. data/spec/play_store/receipt_spec.rb +0 -88
  74. data/spec/play_store/subscription_spec.rb +0 -138
  75. data/spec/play_store/subscription_verification_spec.rb +0 -98
  76. data/spec/play_store/verification_spec.rb +0 -82
@@ -1,17 +0,0 @@
1
- HTTP/1.1 200 OK
2
- Expires: Mon, 19 Jan 2015 12:14:41 GMT
3
- Date: Mon, 19 Jan 2015 12:09:41 GMT
4
- Cache-Control: public, max-age=300, must-revalidate, no-transform
5
- ETag: "ye6orv2F-1npMW3u9suM3a7C5Bo/xy4W57xFkYdtUdm4rpPIU-7grtg"
6
- Vary: Origin
7
- Vary: X-Origin
8
- Content-Type: application/json; charset=UTF-8
9
- X-Content-Type-Options: nosniff
10
- X-Frame-Options: SAMEORIGIN
11
- X-XSS-Protection: 1; mode=block
12
- Content-Length: 83495
13
- Server: GSE
14
- Alternate-Protocol: 443:quic,p=0.02
15
-
16
- {
17
- }
@@ -1,29 +0,0 @@
1
- HTTP/1.1 401 Unauthorized
2
- Expires: Mon, 19 Jan 2015 12:14:41 GMT
3
- Date: Mon, 19 Jan 2015 12:09:41 GMT
4
- Cache-Control: public, max-age=300, must-revalidate, no-transform
5
- ETag: "ye6orv2F-1npMW3u9suM3a7C5Bo/xy4W57xFkYdtUdm4rpPIU-7grtg"
6
- Vary: Origin
7
- Vary: X-Origin
8
- WWW-Authenticate: Bearer realm="https://accounts.google.com/AuthSubRequest"
9
- Content-Type: application/json; charset=UTF-8
10
- X-Content-Type-Options: nosniff
11
- X-Frame-Options: SAMEORIGIN
12
- X-XSS-Protection: 1; mode=block
13
- Content-Length: 83495
14
- Server: GSE
15
- Alternate-Protocol: 443:quic,p=0.02
16
-
17
- {
18
- "error": {
19
- "errors": [
20
- {
21
- "domain": "androidpublisher",
22
- "reason": "permissionDenied",
23
- "message": "The current user has insufficient permissions to perform the requested operation."
24
- }
25
- ],
26
- "code": 401,
27
- "message": "The current user has insufficient permissions to perform the requested operation."
28
- }
29
- }
@@ -1,22 +0,0 @@
1
- HTTP/1.1 200 OK
2
- Expires: Mon, 19 Jan 2015 12:14:41 GMT
3
- Date: Mon, 19 Jan 2015 12:09:41 GMT
4
- Cache-Control: public, max-age=300, must-revalidate, no-transform
5
- ETag: "ye6orv2F-1npMW3u9suM3a7C5Bo/xy4W57xFkYdtUdm4rpPIU-7grtg"
6
- Vary: Origin
7
- Vary: X-Origin
8
- Content-Type: application/json; charset=UTF-8
9
- X-Content-Type-Options: nosniff
10
- X-Frame-Options: SAMEORIGIN
11
- X-XSS-Protection: 1; mode=block
12
- Content-Length: 83495
13
- Server: GSE
14
- Alternate-Protocol: 443:quic,p=0.02
15
-
16
- {
17
- "kind": "androidpublisher#productPurchase",
18
- "purchaseTimeMillis": "1421676237413",
19
- "purchaseState": 0,
20
- "consumptionState": 0,
21
- "developerPayload": "payload that gets stored and returned"
22
- }
@@ -1,125 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe CandyCheck::PlayStore::Client do
4
- include WithTempFile
5
- include WithFixtures
6
-
7
- with_temp_file :cache_file
8
-
9
- subject { CandyCheck::PlayStore::Client.new(config) }
10
-
11
- let(:config) do
12
- CandyCheck::PlayStore::Config.new(
13
- application_name: 'demo_app',
14
- application_version: '1.0',
15
- issuer: 'test_issuer',
16
- key_file: fixture_path('play_store', 'dummy.p12'),
17
- cache_file: cache_file_path,
18
- key_secret: 'notasecret'
19
- )
20
- end
21
-
22
- describe 'discovery' do
23
- describe 'w/o cache file' do
24
- it 'boot loads and dumps discovery file' do
25
- mock_discovery!('discovery.txt')
26
- mock_authorize!('auth_success.txt')
27
- subject.boot!
28
- File.exist?(cache_file_path).must_be_true
29
- end
30
-
31
- it 'fails if discovery fails' do
32
- mock_discovery!('empty.txt')
33
- proc { subject.boot! }.must_raise \
34
- CandyCheck::PlayStore::Client::DiscoveryError
35
- end
36
- end
37
-
38
- describe 'with cache file' do
39
- let(:cache_file_path) { fixture_path('play_store', 'api_cache.dump') }
40
-
41
- it 'loads the discovery from cache file' do
42
- mock_authorize!('auth_success.txt')
43
- subject.boot!
44
- end
45
- end
46
- end
47
-
48
- it 'fails if authentication fails' do
49
- mock_discovery!('discovery.txt')
50
- mock_authorize!('auth_failure.txt')
51
- proc { subject.boot! }.must_raise Signet::AuthorizationError
52
- end
53
-
54
- it 'returns the products call result\'s data even if it is a failure' do
55
- bootup!
56
-
57
- mock_request!('products_failure.txt')
58
- result = subject.verify('the_package', 'the_id', 'the_token')
59
- result.must_be_instance_of Hash
60
-
61
- result['error']['code'].must_equal 401
62
- result['error']['message'].must_equal 'The current user has insufficient' \
63
- ' permissions to perform the requested operation.'
64
- result['error']['errors'].size.must_equal 1
65
- end
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
-
81
- it 'returns the products call result\'s data for a successful call' do
82
- bootup!
83
- mock_request!('products_success.txt')
84
- result = subject.verify('the_package', 'the_id', 'the_token')
85
- result.must_be_instance_of Hash
86
- result['purchaseState'].must_equal 0
87
- result['consumptionState'].must_equal 0
88
- result['developerPayload'].must_equal \
89
- 'payload that gets stored and returned'
90
- result['purchaseTimeMillis'].must_equal '1421676237413'
91
- result['kind'].must_equal 'androidpublisher#productPurchase'
92
- end
93
-
94
- private
95
-
96
- def bootup!
97
- mock_discovery!('discovery.txt')
98
- mock_authorize!('auth_success.txt')
99
- subject.boot!
100
- end
101
-
102
- def mock_discovery!(file)
103
- stub_request(:get, 'https://www.googleapis.com/discovery/' \
104
- 'v1/apis/androidpublisher/v2/rest')
105
- .to_return(fixture_content('play_store', file))
106
- end
107
-
108
- def mock_authorize!(file)
109
- stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
110
- .to_return(fixture_content('play_store', file))
111
- end
112
-
113
- def mock_request!(file)
114
- stub_request(:get, 'https://www.googleapis.com/androidpublisher/v2/' \
115
- 'applications/the_package/purchases/products/the_id/tokens/the_token')
116
- .to_return(fixture_content('play_store', file))
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
125
- end
@@ -1,96 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe CandyCheck::PlayStore::Config do
4
- subject { CandyCheck::PlayStore::Config.new(attributes) }
5
-
6
- let(:attributes) do
7
- {
8
- application_name: 'the_name',
9
- application_version: 'the_version',
10
- issuer: 'the_issuer',
11
- key_file: 'the_key_file',
12
- key_secret: 'the_key_secret'
13
- }
14
- end
15
-
16
- describe 'minimal attributes' do
17
- it 'initializes and validates correctly' do
18
- subject.application_name.must_equal 'the_name'
19
- subject.application_version.must_equal 'the_version'
20
- subject.issuer.must_equal 'the_issuer'
21
- subject.key_file.must_equal 'the_key_file'
22
- subject.key_secret.must_equal 'the_key_secret'
23
- end
24
- end
25
-
26
- describe 'maximal attributes' do
27
- let(:attributes) do
28
- {
29
- application_name: 'the_name',
30
- application_version: 'the_version',
31
- issuer: 'the_issuer',
32
- key_file: 'the_key_file',
33
- key_secret: 'the_key_secret',
34
- cache_file: 'the_cache_file'
35
- }
36
- end
37
-
38
- it 'initializes and validates correctly' do
39
- subject.application_name.must_equal 'the_name'
40
- subject.application_version.must_equal 'the_version'
41
- subject.issuer.must_equal 'the_issuer'
42
- subject.key_file.must_equal 'the_key_file'
43
- subject.key_secret.must_equal 'the_key_secret'
44
- subject.cache_file.must_equal 'the_cache_file'
45
- end
46
- end
47
-
48
- describe 'invalid attributes' do
49
- it 'needs application_name' do
50
- assert_raises_missing :application_name
51
- end
52
-
53
- it 'needs application_version' do
54
- assert_raises_missing :application_version
55
- end
56
-
57
- it 'needs issuer' do
58
- assert_raises_missing :issuer
59
- end
60
-
61
- it 'needs key_file' do
62
- assert_raises_missing :key_file
63
- end
64
-
65
- it 'needs key_secret' do
66
- assert_raises_missing :key_secret
67
- end
68
-
69
- private
70
-
71
- def assert_raises_missing(name)
72
- attributes.delete(name)
73
- proc do
74
- subject
75
- end.must_raise ArgumentError
76
- end
77
- end
78
-
79
- describe 'p12 certificate' do
80
- include WithFixtures
81
-
82
- let(:attributes) do
83
- {
84
- application_name: 'the_name',
85
- application_version: 'the_version',
86
- issuer: 'the_issuer',
87
- key_file: fixture_path('play_store', 'dummy.p12'),
88
- key_secret: 'notasecret'
89
- }
90
- end
91
-
92
- it 'load the api_key from a file' do
93
- subject.api_key.wont_be_nil
94
- end
95
- end
96
- end
@@ -1,31 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe CandyCheck::PlayStore::DiscoveryRepository do
4
- subject { CandyCheck::PlayStore::DiscoveryRepository.new(discovery_path) }
5
-
6
- let(:data) do
7
- { 'demo' => 1 }
8
- end
9
-
10
- describe 'empty file path' do
11
- let(:discovery_path) { nil }
12
-
13
- it 'returns nil for nil path' do
14
- subject.load.must_be_nil
15
- end
16
-
17
- it 'does not save' do
18
- subject.save(data)
19
- end
20
- end
21
-
22
- describe 'valid file path' do
23
- include WithTempFile
24
- with_temp_file :discovery
25
-
26
- it 'saves and loads the file content' do
27
- subject.save(data)
28
- subject.load.must_equal data
29
- end
30
- end
31
- end
@@ -1,88 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe CandyCheck::PlayStore::Receipt do
4
- subject { CandyCheck::PlayStore::Receipt.new(attributes) }
5
-
6
- describe 'valid and non-consumed product' do
7
- let(:attributes) do
8
- {
9
- 'kind' => 'androidpublisher#productPurchase',
10
- 'purchaseTimeMillis' => '1421676237413',
11
- 'purchaseState' => 0,
12
- 'consumptionState' => 0,
13
- 'developerPayload' => 'payload that gets stored and returned'
14
- }
15
- end
16
-
17
- it 'is valid?' do
18
- subject.valid?.must_be_true
19
- end
20
-
21
- it 'is not consumed' do
22
- subject.consumed?.must_be_false
23
- end
24
-
25
- it 'returns the purchase_state' do
26
- subject.purchase_state.must_equal 0
27
- end
28
-
29
- it 'returns the consumption_state' do
30
- subject.consumption_state.must_equal 0
31
- end
32
-
33
- it 'returns the developer_payload' do
34
- subject.developer_payload.must_equal \
35
- 'payload that gets stored and returned'
36
- end
37
-
38
- it 'returns the kind' do
39
- subject.kind.must_equal \
40
- 'androidpublisher#productPurchase'
41
- end
42
-
43
- it 'returns the purchase_time_millis' do
44
- subject.purchase_time_millis.must_equal 1_421_676_237_413
45
- end
46
-
47
- it 'returns the purchased_at' do
48
- expected = DateTime.new(2015, 1, 19, 14, 03, 57)
49
- subject.purchased_at.must_equal expected
50
- end
51
- end
52
-
53
- describe 'valid and consumed product' do
54
- let(:attributes) do
55
- {
56
- 'kind' => 'androidpublisher#productPurchase',
57
- 'purchaseTimeMillis' => '1421676237413',
58
- 'purchaseState' => 0,
59
- 'consumptionState' => 1,
60
- 'developerPayload' => 'payload that gets stored and returned'
61
- }
62
- end
63
-
64
- it 'is valid?' do
65
- subject.valid?.must_be_true
66
- end
67
-
68
- it 'is consumed?' do
69
- subject.consumed?.must_be_true
70
- end
71
- end
72
-
73
- describe 'non-valid product' do
74
- let(:attributes) do
75
- {
76
- 'kind' => 'androidpublisher#productPurchase',
77
- 'purchaseTimeMillis' => '1421676237413',
78
- 'purchaseState' => 1,
79
- 'consumptionState' => 0,
80
- 'developerPayload' => 'payload that gets stored and returned'
81
- }
82
- end
83
-
84
- it 'is valid?' do
85
- subject.valid?.must_be_false
86
- end
87
- end
88
- end
@@ -1,138 +0,0 @@
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