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
@@ -0,0 +1,196 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://www.googleapis.com/oauth2/v4/token
6
+ body:
7
+ encoding: ASCII-8BIT
8
+ string: params
9
+ headers:
10
+ User-Agent:
11
+ - Faraday v0.15.4
12
+ Content-Type:
13
+ - application/x-www-form-urlencoded
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ Accept:
17
+ - "*/*"
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Content-Type:
24
+ - application/json; charset=utf-8
25
+ Vary:
26
+ - Origin
27
+ - Referer
28
+ - X-Origin
29
+ Date:
30
+ - Thu, 10 Oct 2019 14:07:52 GMT
31
+ Server:
32
+ - scaffolding on HTTPServer2
33
+ Cache-Control:
34
+ - private
35
+ X-Xss-Protection:
36
+ - '0'
37
+ X-Frame-Options:
38
+ - SAMEORIGIN
39
+ X-Content-Type-Options:
40
+ - nosniff
41
+ Alt-Svc:
42
+ - quic=":443"; ma=2592000; v="46,43",h3-Q048=":443"; ma=2592000,h3-Q046=":443";
43
+ ma=2592000,h3-Q043=":443"; ma=2592000
44
+ Transfer-Encoding:
45
+ - chunked
46
+ body:
47
+ encoding: ASCII-8BIT
48
+ string: |-
49
+ {
50
+ "access_token": "my_token",
51
+ "expires_in": 3600,
52
+ "token_type": "Bearer"
53
+ }
54
+ http_version:
55
+ recorded_at: Thu, 10 Oct 2019 14:07:52 GMT
56
+ - request:
57
+ method: get
58
+ uri: https://www.googleapis.com/androidpublisher/v3/applications/my_package_name/purchases/subscriptions/my_subscription_id/tokens/my_token
59
+ body:
60
+ encoding: UTF-8
61
+ string: ''
62
+ headers:
63
+ User-Agent:
64
+ - unknown/0.0.0 google-api-ruby-client/0.28.7 Linux/5.0.0-31-generic (gzip)
65
+ Accept:
66
+ - "*/*"
67
+ Accept-Encoding:
68
+ - gzip,deflate
69
+ Date:
70
+ - Thu, 10 Oct 2019 14:07:52 GMT
71
+ Authorization:
72
+ - Bearer my_bearer
73
+ Content-Type:
74
+ - application/x-www-form-urlencoded
75
+ response:
76
+ status:
77
+ code: 401
78
+ message: Unauthorized
79
+ headers:
80
+ Vary:
81
+ - Origin
82
+ - X-Origin
83
+ Www-Authenticate:
84
+ - Bearer realm="https://accounts.google.com/", error=invalid_token
85
+ Content-Type:
86
+ - application/json; charset=UTF-8
87
+ Content-Encoding:
88
+ - gzip
89
+ Date:
90
+ - Thu, 10 Oct 2019 14:07:52 GMT
91
+ Expires:
92
+ - Thu, 10 Oct 2019 14:07:52 GMT
93
+ Cache-Control:
94
+ - private, max-age=0
95
+ X-Content-Type-Options:
96
+ - nosniff
97
+ X-Frame-Options:
98
+ - SAMEORIGIN
99
+ X-Xss-Protection:
100
+ - 1; mode=block
101
+ Server:
102
+ - GSE
103
+ Alt-Svc:
104
+ - quic=":443"; ma=2592000; v="46,43",h3-Q048=":443"; ma=2592000,h3-Q046=":443";
105
+ ma=2592000,h3-Q043=":443"; ma=2592000
106
+ Transfer-Encoding:
107
+ - chunked
108
+ body:
109
+ encoding: UTF-8
110
+ string: |
111
+ {
112
+ "error": {
113
+ "errors": [
114
+ {
115
+ "domain": "androidpublisher",
116
+ "reason": "permissionDenied",
117
+ "message": "The current user has insufficient permissions to perform the requested operation."
118
+ }
119
+ ],
120
+ "code": 401,
121
+ "message": "The current user has insufficient permissions to perform the requested operation."
122
+ }
123
+ }
124
+ http_version:
125
+ recorded_at: Thu, 10 Oct 2019 14:07:52 GMT
126
+ - request:
127
+ method: get
128
+ uri: https://www.googleapis.com/androidpublisher/v3/applications/my_package_name/purchases/subscriptions/my_subscription_id/tokens/my_token
129
+ body:
130
+ encoding: UTF-8
131
+ string: ''
132
+ headers:
133
+ User-Agent:
134
+ - unknown/0.0.0 google-api-ruby-client/0.28.7 Linux/5.0.0-31-generic (gzip)
135
+ Accept:
136
+ - "*/*"
137
+ Accept-Encoding:
138
+ - gzip,deflate
139
+ Date:
140
+ - Thu, 10 Oct 2019 14:07:53 GMT
141
+ Authorization:
142
+ - Bearer my_bearer
143
+ Content-Type:
144
+ - application/x-www-form-urlencoded
145
+ response:
146
+ status:
147
+ code: 401
148
+ message: Unauthorized
149
+ headers:
150
+ Vary:
151
+ - Origin
152
+ - X-Origin
153
+ Www-Authenticate:
154
+ - Bearer realm="https://accounts.google.com/", error=invalid_token
155
+ Content-Type:
156
+ - application/json; charset=UTF-8
157
+ Content-Encoding:
158
+ - gzip
159
+ Date:
160
+ - Thu, 10 Oct 2019 14:07:53 GMT
161
+ Expires:
162
+ - Thu, 10 Oct 2019 14:07:53 GMT
163
+ Cache-Control:
164
+ - private, max-age=0
165
+ X-Content-Type-Options:
166
+ - nosniff
167
+ X-Frame-Options:
168
+ - SAMEORIGIN
169
+ X-Xss-Protection:
170
+ - 1; mode=block
171
+ Server:
172
+ - GSE
173
+ Alt-Svc:
174
+ - quic=":443"; ma=2592000; v="46,43",h3-Q048=":443"; ma=2592000,h3-Q046=":443";
175
+ ma=2592000,h3-Q043=":443"; ma=2592000
176
+ Transfer-Encoding:
177
+ - chunked
178
+ body:
179
+ encoding: UTF-8
180
+ string: |
181
+ {
182
+ "error": {
183
+ "errors": [
184
+ {
185
+ "domain": "androidpublisher",
186
+ "reason": "permissionDenied",
187
+ "message": "The current user has insufficient permissions to perform the requested operation."
188
+ }
189
+ ],
190
+ "code": 401,
191
+ "message": "The current user has insufficient permissions to perform the requested operation."
192
+ }
193
+ }
194
+ http_version:
195
+ recorded_at: Thu, 10 Oct 2019 14:07:53 GMT
196
+ recorded_with: VCR 5.0.0
@@ -0,0 +1,127 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: https://www.googleapis.com/oauth2/v4/token
6
+ body:
7
+ encoding: ASCII-8BIT
8
+ string: some_grant
9
+ headers:
10
+ User-Agent:
11
+ - Faraday v0.15.4
12
+ Content-Type:
13
+ - application/x-www-form-urlencoded
14
+ Accept-Encoding:
15
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
16
+ Accept:
17
+ - "*/*"
18
+ response:
19
+ status:
20
+ code: 200
21
+ message: OK
22
+ headers:
23
+ Content-Type:
24
+ - application/json; charset=utf-8
25
+ Vary:
26
+ - Origin
27
+ - Referer
28
+ - X-Origin
29
+ Date:
30
+ - Thu, 24 Oct 2019 10:43:13 GMT
31
+ Server:
32
+ - scaffolding on HTTPServer2
33
+ Cache-Control:
34
+ - private
35
+ X-Xss-Protection:
36
+ - '0'
37
+ X-Frame-Options:
38
+ - SAMEORIGIN
39
+ X-Content-Type-Options:
40
+ - nosniff
41
+ Alt-Svc:
42
+ - quic=":443"; ma=2592000; v="46,43",h3-Q048=":443"; ma=2592000,h3-Q046=":443";
43
+ ma=2592000,h3-Q043=":443"; ma=2592000
44
+ Transfer-Encoding:
45
+ - chunked
46
+ body:
47
+ encoding: ASCII-8BIT
48
+ string: |-
49
+ {
50
+ "access_token": "some token",
51
+ "expires_in": 3600,
52
+ "token_type": "Bearer"
53
+ }
54
+ http_version:
55
+ recorded_at: Thu, 24 Oct 2019 10:43:13 GMT
56
+ - request:
57
+ method: get
58
+ uri: https://www.googleapis.com/androidpublisher/v3/applications/my_package_name/purchases/subscriptions/my_subscription_id/tokens/my_token
59
+ body:
60
+ encoding: UTF-8
61
+ string: ''
62
+ headers:
63
+ User-Agent:
64
+ - unknown/0.0.0 google-api-ruby-client/0.28.7 Linux/5.0.0-32-generic (gzip)
65
+ Accept:
66
+ - "*/*"
67
+ Accept-Encoding:
68
+ - gzip,deflate
69
+ Date:
70
+ - Thu, 24 Oct 2019 10:43:13 GMT
71
+ Authorization:
72
+ - Bearer some_token
73
+ Content-Type:
74
+ - application/x-www-form-urlencoded
75
+ response:
76
+ status:
77
+ code: 200
78
+ message: OK
79
+ headers:
80
+ Expires:
81
+ - Thu, 24 Oct 2019 10:43:13 GMT
82
+ Date:
83
+ - Thu, 24 Oct 2019 10:43:13 GMT
84
+ Cache-Control:
85
+ - private, max-age=0, must-revalidate, no-transform
86
+ Etag:
87
+ - ''
88
+ Vary:
89
+ - Origin
90
+ - X-Origin
91
+ Content-Type:
92
+ - application/json; charset=UTF-8
93
+ Content-Encoding:
94
+ - gzip
95
+ X-Content-Type-Options:
96
+ - nosniff
97
+ X-Frame-Options:
98
+ - SAMEORIGIN
99
+ X-Xss-Protection:
100
+ - 1; mode=block
101
+ Server:
102
+ - GSE
103
+ Alt-Svc:
104
+ - quic=":443"; ma=2592000; v="46,43",h3-Q048=":443"; ma=2592000,h3-Q046=":443";
105
+ ma=2592000,h3-Q043=":443"; ma=2592000
106
+ Transfer-Encoding:
107
+ - chunked
108
+ body:
109
+ encoding: UTF-8
110
+ string: |
111
+ {
112
+ "kind": "androidpublisher#subscriptionPurchase",
113
+ "startTimeMillis": "123456789",
114
+ "expiryTimeMillis": "123456789",
115
+ "autoRenewing": false,
116
+ "priceCurrencyCode": "EUR",
117
+ "priceAmountMicros": "123",
118
+ "countryCode": "DE",
119
+ "developerPayload": "",
120
+ "cancelReason": 1,
121
+ "orderId": "123",
122
+ "purchaseType": 0,
123
+ "acknowledgementState": 1
124
+ }
125
+ http_version:
126
+ recorded_at: Thu, 24 Oct 2019 10:43:13 GMT
127
+ recorded_with: VCR 5.0.0
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ describe CandyCheck::PlayStore::Acknowledger do
4
+ let(:json_key_file) { File.expand_path("../fixtures/play_store/random_dummy_key.json", __dir__) }
5
+ subject { CandyCheck::PlayStore::Acknowledger.new(authorization: authorization) }
6
+
7
+ let(:package_name) { "fake_package_name" }
8
+ let(:product_id) { "fake_product_id" }
9
+ let(:token) { "fake_token" }
10
+
11
+ let(:authorization) { CandyCheck::PlayStore.authorization(json_key_file) }
12
+
13
+ describe "#acknowledge_product_purchase" do
14
+ it "when acknowledgement succeeds" do
15
+ VCR.use_cassette("play_store/product_acknowledgements/acknowledged") do
16
+ result = subject.acknowledge_product_purchase(package_name: package_name, product_id: product_id, token: token)
17
+
18
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
19
+ result.acknowledged?.must_be_true
20
+ result.error.must_be_nil
21
+ end
22
+ end
23
+ it "when already acknowledged" do
24
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"errors\": [\n {\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"invalidPurchaseState\",\n \"location\": \"token\",\n \"locationType\": \"parameter\"\n }\n ]\n }\n}\n"
25
+
26
+ VCR.use_cassette("play_store/product_acknowledgements/already_acknowledged") do
27
+ result = subject.acknowledge_product_purchase(package_name: package_name, product_id: product_id, token: token)
28
+
29
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
30
+ result.acknowledged?.must_be_false
31
+ result.error[:body].must_equal(error_body)
32
+ result.error[:status_code].must_equal(400)
33
+ end
34
+ end
35
+ it "when it has been refunded" do
36
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The product purchase is not owned by the user.\",\n \"errors\": [\n {\n \"message\": \"The product purchase is not owned by the user.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"productNotOwnedByUser\"\n }\n ]\n }\n}\n"
37
+
38
+ VCR.use_cassette("play_store/product_acknowledgements/refunded") do
39
+ result = subject.acknowledge_product_purchase(package_name: package_name, product_id: product_id, token: token)
40
+
41
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
42
+ result.acknowledged?.must_be_false
43
+ result.error[:body].must_equal(error_body)
44
+ result.error[:status_code].must_equal(400)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ describe CandyCheck::PlayStore::ProductAcknowledgements::Acknowledgement do
4
+ subject do
5
+ CandyCheck::PlayStore::ProductAcknowledgements::Acknowledgement.new(
6
+ package_name: package_name,
7
+ product_id: product_id,
8
+ token: token,
9
+ authorization: authorization,
10
+ )
11
+ end
12
+
13
+ let(:package_name) { "fake_package_name" }
14
+ let(:product_id) { "fake_product_id" }
15
+ let(:token) { "fake_token" }
16
+ let(:json_key_file) { File.expand_path("../../fixtures/play_store/random_dummy_key.json", __dir__) }
17
+ let(:authorization) { CandyCheck::PlayStore.authorization(json_key_file) }
18
+
19
+ describe "#call!" do
20
+ it "when acknowledgement succeeds" do
21
+ VCR.use_cassette("play_store/product_acknowledgements/acknowledged") do
22
+ result = subject.call!
23
+
24
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
25
+ result.acknowledged?.must_be_true
26
+ result.error.must_be_nil
27
+ end
28
+ end
29
+ it "when already acknowledged" do
30
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"errors\": [\n {\n \"message\": \"The purchase is not in a valid state to perform the desired operation.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"invalidPurchaseState\",\n \"location\": \"token\",\n \"locationType\": \"parameter\"\n }\n ]\n }\n}\n"
31
+
32
+ VCR.use_cassette("play_store/product_acknowledgements/already_acknowledged") do
33
+ result = subject.call!
34
+
35
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
36
+ result.acknowledged?.must_be_false
37
+ result.error[:body].must_equal(error_body)
38
+ result.error[:status_code].must_equal(400)
39
+ end
40
+ end
41
+ it "when it has been refunded" do
42
+ error_body = "{\n \"error\": {\n \"code\": 400,\n \"message\": \"The product purchase is not owned by the user.\",\n \"errors\": [\n {\n \"message\": \"The product purchase is not owned by the user.\",\n \"domain\": \"androidpublisher\",\n \"reason\": \"productNotOwnedByUser\"\n }\n ]\n }\n}\n"
43
+
44
+ VCR.use_cassette("play_store/product_acknowledgements/refunded") do
45
+ result = subject.call!
46
+
47
+ result.must_be_instance_of CandyCheck::PlayStore::ProductAcknowledgements::Response
48
+ result.acknowledged?.must_be_false
49
+ result.error[:body].must_equal(error_body)
50
+ result.error[:status_code].must_equal(400)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe CandyCheck::PlayStore::ProductAcknowledgements::Response do
4
+ subject do
5
+ CandyCheck::PlayStore::ProductAcknowledgements::Response.new(result: result, error_data: error_data)
6
+ end
7
+
8
+ describe '#acknowledged?' do
9
+ context 'when result present' do
10
+ let(:result) { '' }
11
+ let(:error_data) { nil }
12
+
13
+ it 'returns true' do
14
+ result = subject.acknowledged?
15
+
16
+ result.must_be_true
17
+ end
18
+ end
19
+
20
+ context 'when result is not present' do
21
+ let(:result) { nil }
22
+ let(:error_data) { nil }
23
+
24
+ it 'returns false' do
25
+ result = subject.acknowledged?
26
+
27
+ result.must_be_false
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '#error' do
33
+ context 'when error present' do
34
+ let(:result) { nil }
35
+ let(:error_data) do
36
+ Module.new do
37
+ def status_code
38
+ 400
39
+ end
40
+ def body
41
+ 'A String describing the issue'
42
+ end
43
+ module_function :status_code, :body
44
+ end
45
+ end
46
+
47
+ it 'returns the expected data' do
48
+ result = subject.error
49
+
50
+ result[:status_code].must_equal(400)
51
+ result[:body].must_equal('A String describing the issue')
52
+ end
53
+ end
54
+
55
+ context 'when error is not present' do
56
+ let(:result) { '' }
57
+ let(:error_data) { nil }
58
+
59
+ it 'returns false' do
60
+ result = subject.error
61
+
62
+ result.must_be_nil
63
+ end
64
+ end
65
+ end
66
+ end