candy_check 0.4.0 → 0.6.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +35 -0
- data/.rubocop.yml +10 -0
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/Guardfile +5 -3
- data/README.md +2 -2
- data/Rakefile +5 -5
- data/candy_check.gemspec +14 -12
- data/lib/candy_check/app_store/client.rb +7 -7
- data/lib/candy_check/app_store/config.rb +7 -9
- data/lib/candy_check/app_store/receipt.rb +16 -16
- data/lib/candy_check/app_store/receipt_collection.rb +2 -2
- data/lib/candy_check/app_store/subscription_verification.rb +5 -5
- data/lib/candy_check/app_store/verification.rb +3 -3
- data/lib/candy_check/app_store/verification_failure.rb +25 -21
- data/lib/candy_check/app_store/verifier.rb +4 -5
- data/lib/candy_check/app_store.rb +8 -8
- data/lib/candy_check/cli/app.rb +3 -3
- data/lib/candy_check/cli/commands/app_store.rb +1 -1
- data/lib/candy_check/cli/commands.rb +4 -4
- data/lib/candy_check/cli/out.rb +1 -3
- data/lib/candy_check/cli.rb +3 -3
- data/lib/candy_check/play_store/product_acknowledgements/acknowledgement.rb +2 -1
- data/lib/candy_check/play_store/product_purchases/product_purchase.rb +3 -3
- data/lib/candy_check/play_store/product_purchases/product_verification.rb +1 -1
- data/lib/candy_check/play_store/subscription_purchases/subscription_purchase.rb +2 -1
- data/lib/candy_check/play_store/subscription_purchases/subscription_verification.rb +1 -0
- data/lib/candy_check/play_store/verification_failure.rb +2 -2
- data/lib/candy_check/play_store.rb +2 -1
- data/lib/candy_check/utils/attribute_reader.rb +3 -2
- data/lib/candy_check/utils/config.rb +2 -0
- data/lib/candy_check/utils.rb +2 -2
- data/lib/candy_check/version.rb +1 -1
- data/lib/candy_check.rb +4 -4
- data/spec/app_store/client_spec.rb +18 -18
- data/spec/app_store/config_spec.rb +9 -9
- data/spec/app_store/receipt_collection_spec.rb +39 -41
- data/spec/app_store/receipt_spec.rb +47 -47
- data/spec/app_store/subscription_verification_spec.rb +35 -32
- data/spec/app_store/verifcation_failure_spec.rb +7 -7
- data/spec/app_store/verification_spec.rb +23 -11
- data/spec/app_store/verifier_spec.rb +40 -48
- data/spec/cli/app_spec.rb +11 -13
- data/spec/cli/commands/app_store_spec.rb +22 -23
- data/spec/cli/commands/play_store_spec.rb +3 -1
- data/spec/cli/commands/version_spec.rb +2 -2
- data/spec/cli/out_spec.rb +9 -9
- data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/acknowledged.yml +1 -1
- data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/already_acknowledged.yml +1 -1
- data/spec/fixtures/vcr_cassettes/play_store/product_acknowledgements/refunded.yml +1 -1
- data/spec/fixtures/vcr_cassettes/play_store/product_purchases/permission_denied.yml +2 -2
- data/spec/fixtures/vcr_cassettes/play_store/product_purchases/response_with_empty_body.yml +2 -2
- data/spec/fixtures/vcr_cassettes/play_store/product_purchases/valid_but_not_consumed.yml +1 -1
- data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/permission_denied.yml +2 -2
- data/spec/fixtures/vcr_cassettes/play_store/subscription_purchases/valid_but_expired.yml +1 -1
- data/spec/play_store/acknowledger_spec.rb +4 -0
- data/spec/play_store/product_acknowledgements/acknowledgement_spec.rb +4 -0
- data/spec/play_store/product_acknowledgements/response_spec.rb +16 -15
- data/spec/play_store/product_purchases/product_purchase_spec.rb +6 -27
- data/spec/play_store/subscription_purchases/subscription_purchase_spec.rb +22 -46
- data/spec/play_store/verification_failure_spec.rb +6 -4
- data/spec/play_store/verifier_spec.rb +4 -2
- data/spec/spec_helper.rb +20 -9
- metadata +91 -63
- data/.travis.yml +0 -18
@@ -26,7 +26,7 @@ module CandyCheck
|
|
26
26
|
# The purchase state of the order. Possible values are:
|
27
27
|
# * 0: Purchased
|
28
28
|
# * 1: Cancelled
|
29
|
-
# @return [
|
29
|
+
# @return [Integer]
|
30
30
|
def purchase_state
|
31
31
|
@product_purchase.purchase_state
|
32
32
|
end
|
@@ -34,7 +34,7 @@ module CandyCheck
|
|
34
34
|
# The consumption state of the inapp product. Possible values are:
|
35
35
|
# * 0: Yet to be consumed
|
36
36
|
# * 1: Consumed
|
37
|
-
# @return [
|
37
|
+
# @return [Integer]
|
38
38
|
def consumption_state
|
39
39
|
@product_purchase.consumption_state
|
40
40
|
end
|
@@ -60,7 +60,7 @@ module CandyCheck
|
|
60
60
|
|
61
61
|
# The time the product was purchased, in milliseconds since the
|
62
62
|
# epoch (Jan 1, 1970)
|
63
|
-
# @return [
|
63
|
+
# @return [Integer]
|
64
64
|
def purchase_time_millis
|
65
65
|
@product_purchase.purchase_time_millis
|
66
66
|
end
|
@@ -5,7 +5,8 @@ module CandyCheck
|
|
5
5
|
class SubscriptionPurchase
|
6
6
|
include Utils::AttributeReader
|
7
7
|
|
8
|
-
# @return [Google::Apis::AndroidpublisherV3::SubscriptionPurchase] the raw subscription purchase
|
8
|
+
# @return [Google::Apis::AndroidpublisherV3::SubscriptionPurchase] the raw subscription purchase
|
9
|
+
# from google-api-client
|
9
10
|
attr_reader :subscription_purchase
|
10
11
|
|
11
12
|
# The payment of the subscription is pending (paymentState)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "date"
|
2
2
|
|
3
3
|
module CandyCheck
|
4
4
|
module Utils
|
@@ -23,7 +23,8 @@ module CandyCheck
|
|
23
23
|
def read_bool(field)
|
24
24
|
val = read(field).to_s
|
25
25
|
return nil unless %w(false true).include?(val)
|
26
|
-
|
26
|
+
|
27
|
+
val == "true"
|
27
28
|
end
|
28
29
|
|
29
30
|
def read_datetime_from_string(field)
|
@@ -26,6 +26,7 @@ module CandyCheck
|
|
26
26
|
# @raise [ArgumentError] if attribute is missing
|
27
27
|
def validates_presence(name)
|
28
28
|
return if send(name)
|
29
|
+
|
29
30
|
raise ArgumentError, "Configuration field #{name} is missing"
|
30
31
|
end
|
31
32
|
|
@@ -34,6 +35,7 @@ module CandyCheck
|
|
34
35
|
# @param values [Array] of possible values
|
35
36
|
def validates_inclusion(name, *values)
|
36
37
|
return if values.include?(send(name))
|
38
|
+
|
37
39
|
raise ArgumentError, "Configuration field #{name} should be "\
|
38
40
|
"one of: #{values.join(', ')}"
|
39
41
|
end
|
data/lib/candy_check/utils.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "candy_check/utils/attribute_reader"
|
2
|
+
require "candy_check/utils/config"
|
data/lib/candy_check/version.rb
CHANGED
data/lib/candy_check.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require "candy_check/version"
|
2
|
+
require "candy_check/utils"
|
3
|
+
require "candy_check/app_store"
|
4
|
+
require "candy_check/play_store"
|
5
5
|
|
6
6
|
# Module to check and verify in-app receipts
|
7
7
|
module CandyCheck
|
@@ -1,12 +1,12 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe CandyCheck::AppStore::Client do
|
4
|
-
let(:endpoint_url) {
|
4
|
+
let(:endpoint_url) { "https://some.endpoint.com/verify" }
|
5
5
|
let(:receipt_data) do
|
6
|
-
|
6
|
+
"some_very_long_receipt_information_which_is_normaly_base64_encoded"
|
7
7
|
end
|
8
8
|
let(:password) do
|
9
|
-
|
9
|
+
"some_secret_password"
|
10
10
|
end
|
11
11
|
let(:response) do
|
12
12
|
'{
|
@@ -18,40 +18,40 @@ describe CandyCheck::AppStore::Client do
|
|
18
18
|
end
|
19
19
|
let(:expected) do
|
20
20
|
{
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
}
|
21
|
+
"status" => 0,
|
22
|
+
"receipt" => {
|
23
|
+
"item_id" => "521129812",
|
24
|
+
},
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
28
|
subject { CandyCheck::AppStore::Client.new(endpoint_url) }
|
29
29
|
|
30
|
-
describe
|
31
|
-
it
|
30
|
+
describe "valid response" do
|
31
|
+
it "sends JSON and parses the JSON response without a secret" do
|
32
32
|
stub_endpoint
|
33
33
|
.with(
|
34
34
|
body: {
|
35
|
-
|
36
|
-
}
|
35
|
+
"receipt-data" => receipt_data,
|
36
|
+
},
|
37
37
|
)
|
38
38
|
.to_return(
|
39
|
-
body: response
|
39
|
+
body: response,
|
40
40
|
)
|
41
41
|
result = subject.verify(receipt_data)
|
42
42
|
_(result).must_equal expected
|
43
43
|
end
|
44
44
|
|
45
|
-
it
|
45
|
+
it "sends JSON and parses the JSON response with a secret" do
|
46
46
|
stub_endpoint
|
47
47
|
.with(
|
48
48
|
body: {
|
49
|
-
|
50
|
-
|
51
|
-
}
|
49
|
+
"receipt-data" => receipt_data,
|
50
|
+
"password" => password,
|
51
|
+
},
|
52
52
|
)
|
53
53
|
.to_return(
|
54
|
-
body: response
|
54
|
+
body: response,
|
55
55
|
)
|
56
56
|
result = subject.verify(receipt_data, password)
|
57
57
|
_(result).must_equal expected
|
@@ -1,39 +1,39 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe CandyCheck::AppStore::Config do
|
4
4
|
subject { CandyCheck::AppStore::Config.new(attributes) }
|
5
5
|
|
6
|
-
describe
|
6
|
+
describe "valid" do
|
7
7
|
let(:attributes) do
|
8
8
|
{
|
9
|
-
environment: :sandbox
|
9
|
+
environment: :sandbox,
|
10
10
|
}
|
11
11
|
end
|
12
12
|
|
13
|
-
it
|
13
|
+
it "returns environment" do
|
14
14
|
_(subject.environment).must_equal :sandbox
|
15
15
|
end
|
16
16
|
|
17
|
-
it
|
17
|
+
it "checks for production?" do
|
18
18
|
_(subject.production?).must_be_false
|
19
19
|
|
20
20
|
other = CandyCheck::AppStore::Config.new(
|
21
|
-
environment: :production
|
21
|
+
environment: :production,
|
22
22
|
)
|
23
23
|
_(other.production?).must_be_true
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
describe
|
27
|
+
describe "invalid" do
|
28
28
|
let(:attributes) do
|
29
29
|
{}
|
30
30
|
end
|
31
31
|
|
32
|
-
it
|
32
|
+
it "needs an environment" do
|
33
33
|
_(proc { subject }).must_raise ArgumentError
|
34
34
|
end
|
35
35
|
|
36
|
-
it
|
36
|
+
it "needs an included environment" do
|
37
37
|
attributes[:environment] = :invalid
|
38
38
|
_(proc { subject }).must_raise ArgumentError
|
39
39
|
end
|
@@ -1,98 +1,96 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe CandyCheck::AppStore::ReceiptCollection do
|
4
4
|
subject { CandyCheck::AppStore::ReceiptCollection.new(attributes) }
|
5
5
|
|
6
|
-
describe
|
6
|
+
describe "overdue subscription" do
|
7
7
|
let(:attributes) do
|
8
8
|
[{
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
+
"purchase_date" => "2014-04-14 12:52:40 Etc/GMT",
|
12
|
+
"is_trial_period" => "false",
|
13
13
|
}, {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
"expires_date" => "2015-04-15 12:52:40 Etc/GMT",
|
15
|
+
"expires_date_pst" => "2015-04-15 05:52:40 America/Los_Angeles",
|
16
|
+
"purchase_date" => "2015-04-14 12:52:40 Etc/GMT",
|
17
|
+
"is_trial_period" => "false",
|
18
18
|
}]
|
19
19
|
end
|
20
20
|
|
21
|
-
it
|
21
|
+
it "is expired" do
|
22
22
|
_(subject.expired?).must_be_true
|
23
23
|
end
|
24
24
|
|
25
|
-
it
|
25
|
+
it "is not a trial" do
|
26
26
|
_(subject.trial?).must_be_false
|
27
27
|
end
|
28
28
|
|
29
|
-
it
|
29
|
+
it "has positive overdue days" do
|
30
30
|
overdue = subject.overdue_days
|
31
|
-
_(overdue).must_be_instance_of
|
31
|
+
_(overdue).must_be_instance_of Integer
|
32
32
|
assert overdue > 0
|
33
33
|
end
|
34
34
|
|
35
|
-
it
|
35
|
+
it "has a last expires date" do
|
36
36
|
expected = DateTime.new(2015, 4, 15, 12, 52, 40)
|
37
37
|
_(subject.expires_at).must_equal expected
|
38
38
|
end
|
39
39
|
|
40
|
-
it
|
40
|
+
it "is expired? at same pointin time" do
|
41
41
|
Timecop.freeze(Time.utc(2015, 4, 15, 12, 52, 40)) do
|
42
42
|
_(subject.expired?).must_be_true
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
describe
|
47
|
+
describe "unordered receipts" do
|
48
48
|
let(:attributes) do
|
49
49
|
[{
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
50
|
+
"expires_date" => "2015-04-15 12:52:40 Etc/GMT",
|
51
|
+
"expires_date_pst" => "2015-04-15 05:52:40 America/Los_Angeles",
|
52
|
+
"purchase_date" => "2015-04-14 12:52:40 Etc/GMT",
|
53
|
+
"is_trial_period" => "false",
|
54
|
+
}, {
|
55
|
+
"expires_date" => "2014-04-15 12:52:40 Etc/GMT",
|
56
|
+
"expires_date_pst" => "2014-04-15 05:52:40 America/Los_Angeles",
|
57
|
+
"purchase_date" => "2014-04-14 12:52:40 Etc/GMT",
|
58
|
+
"is_trial_period" => "false",
|
59
|
+
}]
|
60
60
|
end
|
61
61
|
|
62
|
-
it
|
62
|
+
it "the expires date is the latest one in time" do
|
63
63
|
expected = DateTime.new(2015, 4, 15, 12, 52, 40)
|
64
64
|
_(subject.expires_at).must_equal expected
|
65
65
|
end
|
66
|
-
|
67
66
|
end
|
68
67
|
|
69
|
-
describe
|
68
|
+
describe "unexpired trial subscription" do
|
70
69
|
two_days_from_now = DateTime.now + 2
|
71
70
|
|
72
71
|
let(:attributes) do
|
73
72
|
[{
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
"expires_date" => "2016-04-15 12:52:40 Etc/GMT",
|
74
|
+
"purchase_date" => "2016-04-15 12:52:40 Etc/GMT",
|
75
|
+
"is_trial_period" => "true",
|
77
76
|
}, {
|
78
|
-
|
79
|
-
two_days_from_now.strftime(
|
80
|
-
|
81
|
-
|
77
|
+
"expires_date" =>
|
78
|
+
two_days_from_now.strftime("%Y-%m-%d %H:%M:%S Etc/GMT"),
|
79
|
+
"purchase_date" => "2016-04-15 12:52:40 Etc/GMT",
|
80
|
+
"is_trial_period" => "true",
|
82
81
|
}]
|
83
82
|
end
|
84
83
|
|
85
|
-
it
|
84
|
+
it "has not expired" do
|
86
85
|
_(subject.expired?).must_be_false
|
87
86
|
end
|
88
87
|
|
89
|
-
it
|
88
|
+
it "it is a trial" do
|
90
89
|
_(subject.trial?).must_be_true
|
91
90
|
end
|
92
91
|
|
93
|
-
it
|
92
|
+
it "expires in two days" do
|
94
93
|
_(subject.overdue_days).must_equal(-2)
|
95
94
|
end
|
96
95
|
end
|
97
|
-
|
98
96
|
end
|
@@ -1,105 +1,105 @@
|
|
1
|
-
require
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe CandyCheck::AppStore::Receipt do
|
4
4
|
subject { CandyCheck::AppStore::Receipt.new(attributes) }
|
5
5
|
|
6
6
|
let(:attributes) do
|
7
7
|
{
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
8
|
+
"original_purchase_date_pst" => "2015-01-08 03:40:46" \
|
9
|
+
" America/Los_Angeles",
|
10
|
+
"purchase_date_ms" => "1420803646868",
|
11
|
+
"unique_identifier" => "some_uniq_identifier_from_apple" \
|
12
|
+
"_for_this",
|
13
|
+
"original_transaction_id" => "some_original_transaction_id",
|
14
|
+
"bvrs" => "2.0",
|
15
|
+
"transaction_id" => "some_transaction_id",
|
16
|
+
"quantity" => "1",
|
17
|
+
"unique_vendor_identifier" => "00000000-1111-2222-3333-" \
|
18
|
+
"444444444444",
|
19
|
+
"item_id" => "some_item_id",
|
20
|
+
"product_id" => "some_product",
|
21
|
+
"purchase_date" => "2015-01-09 11:40:46 Etc/GMT",
|
22
|
+
"original_purchase_date" => "2015-01-08 11:40:46 Etc/GMT",
|
23
|
+
"purchase_date_pst" => "2015-01-09 03:40:46" \
|
24
|
+
" America/Los_Angeles",
|
25
|
+
"bid" => "some.test.app",
|
26
|
+
"original_purchase_date_ms" => "1420717246868",
|
27
|
+
"expires_date" => "2016-06-09 13:59:40 Etc/GMT",
|
28
|
+
"is_trial_period" => "false",
|
29
29
|
}
|
30
30
|
end
|
31
31
|
|
32
|
-
describe
|
33
|
-
it
|
32
|
+
describe "valid transaction" do
|
33
|
+
it "is valid" do
|
34
34
|
_(subject.valid?).must_be_true
|
35
35
|
end
|
36
36
|
|
37
|
-
it
|
38
|
-
_(subject.item_id).must_equal
|
37
|
+
it "returns the item's id" do
|
38
|
+
_(subject.item_id).must_equal "some_item_id"
|
39
39
|
end
|
40
40
|
|
41
|
-
it
|
42
|
-
_(subject.product_id).must_equal
|
41
|
+
it "returns the item's product_id" do
|
42
|
+
_(subject.product_id).must_equal "some_product"
|
43
43
|
end
|
44
44
|
|
45
|
-
it
|
45
|
+
it "returns the quantity" do
|
46
46
|
_(subject.quantity).must_equal 1
|
47
47
|
end
|
48
48
|
|
49
|
-
it
|
50
|
-
_(subject.app_version).must_equal
|
49
|
+
it "returns the app version" do
|
50
|
+
_(subject.app_version).must_equal "2.0"
|
51
51
|
end
|
52
52
|
|
53
|
-
it
|
54
|
-
_(subject.bundle_identifier).must_equal
|
53
|
+
it "returns the bundle identifier" do
|
54
|
+
_(subject.bundle_identifier).must_equal "some.test.app"
|
55
55
|
end
|
56
56
|
|
57
|
-
it
|
57
|
+
it "returns the purchase date" do
|
58
58
|
expected = DateTime.new(2015, 1, 9, 11, 40, 46)
|
59
59
|
_(subject.purchase_date).must_equal expected
|
60
60
|
end
|
61
61
|
|
62
|
-
it
|
62
|
+
it "returns the original purchase date" do
|
63
63
|
expected = DateTime.new(2015, 1, 8, 11, 40, 46)
|
64
64
|
_(subject.original_purchase_date).must_equal expected
|
65
65
|
end
|
66
66
|
|
67
|
-
it
|
68
|
-
_(subject.transaction_id).must_equal
|
67
|
+
it "returns the transaction id" do
|
68
|
+
_(subject.transaction_id).must_equal "some_transaction_id"
|
69
69
|
end
|
70
70
|
|
71
|
-
it
|
72
|
-
_(subject.original_transaction_id).must_equal
|
71
|
+
it "returns the original transaction id" do
|
72
|
+
_(subject.original_transaction_id).must_equal "some_original_transaction_id"
|
73
73
|
end
|
74
74
|
|
75
|
-
it
|
75
|
+
it "return nil for cancellation date" do
|
76
76
|
_(subject.cancellation_date).must_be_nil
|
77
77
|
end
|
78
78
|
|
79
|
-
it
|
79
|
+
it "returns raw attributes" do
|
80
80
|
_(subject.attributes).must_be_same_as attributes
|
81
81
|
end
|
82
82
|
|
83
|
-
it
|
83
|
+
it "returns the subscription expiration date" do
|
84
84
|
expected = DateTime.new(2016, 6, 9, 13, 59, 40)
|
85
85
|
_(subject.expires_date).must_equal expected
|
86
86
|
end
|
87
87
|
|
88
|
-
it
|
88
|
+
it "returns the trial status" do
|
89
89
|
_(subject.is_trial_period).must_be_false
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
-
describe
|
93
|
+
describe "valid transaction" do
|
94
94
|
before do
|
95
|
-
attributes[
|
95
|
+
attributes["cancellation_date"] = "2015-01-12 11:40:46 Etc/GMT"
|
96
96
|
end
|
97
97
|
|
98
|
-
it
|
98
|
+
it "isn't valid" do
|
99
99
|
_(subject.valid?).must_be_false
|
100
100
|
end
|
101
101
|
|
102
|
-
it
|
102
|
+
it "return nil for cancellation date" do
|
103
103
|
expected = DateTime.new(2015, 1, 12, 11, 40, 46)
|
104
104
|
_(subject.cancellation_date).must_equal expected
|
105
105
|
end
|