candy_check 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +6 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +16 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +157 -0
  9. data/Rakefile +15 -0
  10. data/bin/cc_appstore +90 -0
  11. data/bin/cc_playstore +119 -0
  12. data/candy_check.gemspec +31 -0
  13. data/lib/candy_check/app_store/client.rb +57 -0
  14. data/lib/candy_check/app_store/config.rb +30 -0
  15. data/lib/candy_check/app_store/receipt.rb +83 -0
  16. data/lib/candy_check/app_store/verification.rb +49 -0
  17. data/lib/candy_check/app_store/verification_failure.rb +60 -0
  18. data/lib/candy_check/app_store/verifier.rb +69 -0
  19. data/lib/candy_check/app_store.rb +12 -0
  20. data/lib/candy_check/play_store/client.rb +102 -0
  21. data/lib/candy_check/play_store/config.rb +51 -0
  22. data/lib/candy_check/play_store/discovery_repository.rb +33 -0
  23. data/lib/candy_check/play_store/receipt.rb +81 -0
  24. data/lib/candy_check/play_store/verification.rb +46 -0
  25. data/lib/candy_check/play_store/verification_failure.rb +30 -0
  26. data/lib/candy_check/play_store/verifier.rb +52 -0
  27. data/lib/candy_check/play_store.rb +15 -0
  28. data/lib/candy_check/utils/attribute_reader.rb +30 -0
  29. data/lib/candy_check/utils/config.rb +40 -0
  30. data/lib/candy_check/utils.rb +2 -0
  31. data/lib/candy_check/version.rb +4 -0
  32. data/lib/candy_check.rb +8 -0
  33. data/spec/app_store/client_spec.rb +55 -0
  34. data/spec/app_store/config_spec.rb +41 -0
  35. data/spec/app_store/receipt_spec.rb +92 -0
  36. data/spec/app_store/verifcation_failure_spec.rb +28 -0
  37. data/spec/app_store/verification_spec.rb +66 -0
  38. data/spec/app_store/verifier_spec.rb +110 -0
  39. data/spec/candy_check_spec.rb +9 -0
  40. data/spec/fixtures/api_cache.dump +1 -0
  41. data/spec/fixtures/play_store/api_cache.dump +1 -0
  42. data/spec/fixtures/play_store/auth_failure.txt +18 -0
  43. data/spec/fixtures/play_store/auth_success.txt +20 -0
  44. data/spec/fixtures/play_store/discovery.txt +2841 -0
  45. data/spec/fixtures/play_store/dummy.p12 +0 -0
  46. data/spec/fixtures/play_store/empty.txt +17 -0
  47. data/spec/fixtures/play_store/products_failure.txt +29 -0
  48. data/spec/fixtures/play_store/products_success.txt +22 -0
  49. data/spec/play_store/client_spec.rb +104 -0
  50. data/spec/play_store/config_spec.rb +96 -0
  51. data/spec/play_store/discovery_respository_spec.rb +31 -0
  52. data/spec/play_store/receipt_spec.rb +88 -0
  53. data/spec/play_store/verification_failure_spec.rb +35 -0
  54. data/spec/play_store/verification_spec.rb +80 -0
  55. data/spec/play_store/verifier_spec.rb +95 -0
  56. data/spec/spec_helper.rb +35 -0
  57. data/spec/support/with_fixtures.rb +9 -0
  58. data/spec/support/with_temp_file.rb +23 -0
  59. metadata +270 -0
Binary file
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,22 @@
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
+ }
@@ -0,0 +1,104 @@
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 for a successful call' do
68
+ bootup!
69
+ mock_request!('products_success.txt')
70
+ result = subject.verify('the_package', 'the_id', 'the_token')
71
+ result.must_be_instance_of Hash
72
+ result['purchaseState'].must_equal 0
73
+ result['consumptionState'].must_equal 0
74
+ result['developerPayload'].must_equal \
75
+ 'payload that gets stored and returned'
76
+ result['purchaseTimeMillis'].must_equal '1421676237413'
77
+ result['kind'].must_equal 'androidpublisher#productPurchase'
78
+ end
79
+
80
+ private
81
+
82
+ def bootup!
83
+ mock_discovery!('discovery.txt')
84
+ mock_authorize!('auth_success.txt')
85
+ subject.boot!
86
+ end
87
+
88
+ def mock_discovery!(file)
89
+ stub_request(:get, 'https://www.googleapis.com/discovery/' \
90
+ 'v1/apis/androidpublisher/v2/rest')
91
+ .to_return(fixture_content('play_store', file))
92
+ end
93
+
94
+ def mock_authorize!(file)
95
+ stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
96
+ .to_return(fixture_content('play_store', file))
97
+ end
98
+
99
+ def mock_request!(file)
100
+ stub_request(:get, 'https://www.googleapis.com/androidpublisher/v2/' \
101
+ 'applications/the_package/purchases/products/the_id/tokens/the_token')
102
+ .to_return(fixture_content('play_store', file))
103
+ end
104
+ end
@@ -0,0 +1,96 @@
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,88 @@
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
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe CandyCheck::PlayStore::VerificationFailure do
4
+ subject { CandyCheck::PlayStore::VerificationFailure.new(attributes) }
5
+
6
+ describe 'denied' do
7
+ let(:attributes) do
8
+ {
9
+ 'errors' => [],
10
+ 'code' => 401,
11
+ 'message' => 'The current user has insufficient permissions'
12
+ }
13
+ end
14
+
15
+ it 'returns the code' do
16
+ subject.code.must_equal 401
17
+ end
18
+
19
+ it 'returns the message' do
20
+ subject.message.must_equal 'The current user has insufficient permissions'
21
+ end
22
+ end
23
+
24
+ describe 'empty' do
25
+ let(:attributes) { nil }
26
+
27
+ it 'returns an unknown code' do
28
+ subject.code.must_equal(-1)
29
+ end
30
+
31
+ it 'returns an unknown message' do
32
+ subject.message.must_equal 'Unknown error'
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe CandyCheck::PlayStore::Verification do
4
+ subject do
5
+ CandyCheck::PlayStore::Verification.new(client, package, product_id, token)
6
+ end
7
+ let(:client) { DummyGoogleClient.new(response) }
8
+ let(:package) { 'the_package' }
9
+ let(:product_id) { 'the_product' }
10
+ let(:token) { 'the_token' }
11
+
12
+ describe 'valid' do
13
+ let(:response) do
14
+ {
15
+ 'kind' => 'androidpublisher#productPurchase',
16
+ 'purchaseTimeMillis' => '1421676237413',
17
+ 'purchaseState' => 0,
18
+ 'consumptionState' => 0,
19
+ 'developerPayload' => 'payload that gets stored and returned'
20
+ }
21
+ end
22
+
23
+ it 'calls the client with the correct paramters' do
24
+ subject.call!
25
+ client.package.must_equal package
26
+ client.product_id.must_equal product_id
27
+ client.token.must_equal token
28
+ end
29
+
30
+ it 'returns a receipt' do
31
+ result = subject.call!
32
+ result.must_be_instance_of CandyCheck::PlayStore::Receipt
33
+ result.valid?.must_be_true
34
+ result.consumed?.must_be_false
35
+ end
36
+ end
37
+
38
+ describe 'failure' do
39
+ let(:response) do
40
+ {
41
+ 'error' => {
42
+ 'code' => 401,
43
+ 'message' => 'The current user has insufficient permissions'
44
+ }
45
+ }
46
+ end
47
+
48
+ it 'returns a verification failure' do
49
+ result = subject.call!
50
+ result.must_be_instance_of CandyCheck::PlayStore::VerificationFailure
51
+ result.code.must_equal 401
52
+ end
53
+ end
54
+
55
+ describe 'empty' do
56
+ let(:response) do
57
+ {}
58
+ end
59
+
60
+ it 'returns a verification failure' do
61
+ result = subject.call!
62
+ result.must_be_instance_of CandyCheck::PlayStore::VerificationFailure
63
+ result.code.must_equal(-1)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ class DummyGoogleClient < Struct.new(:response)
70
+ attr_reader :package, :product_id, :token
71
+
72
+ def boot!
73
+ end
74
+
75
+ def verify(package, product_id, token)
76
+ @package, @product_id, @token = package, product_id, token
77
+ response
78
+ end
79
+ end
80
+ end