candy_check 0.0.1

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.
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