braintree 2.15.0 → 2.16.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/braintree.rb +5 -0
- data/lib/braintree/digest.rb +14 -1
- data/lib/braintree/exceptions.rb +3 -0
- data/lib/braintree/gateway.rb +8 -0
- data/lib/braintree/plan_gateway.rb +1 -1
- data/lib/braintree/transaction.rb +5 -0
- data/lib/braintree/version.rb +1 -1
- data/lib/braintree/webhook_notification.rb +38 -0
- data/lib/braintree/webhook_notification_gateway.rb +36 -0
- data/lib/braintree/webhook_testing.rb +7 -0
- data/lib/braintree/webhook_testing_gateway.rb +41 -0
- data/spec/integration/braintree/plan_spec.rb +8 -2
- data/spec/integration/braintree/transaction_spec.rb +21 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/unit/braintree/webhook_notification_spec.rb +56 -0
- metadata +9 -4
data/lib/braintree.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'base64'
|
1
2
|
require "bigdecimal"
|
2
3
|
require "cgi"
|
3
4
|
require "date"
|
@@ -66,6 +67,10 @@ require "braintree/util"
|
|
66
67
|
require "braintree/validation_error"
|
67
68
|
require "braintree/validation_error_collection"
|
68
69
|
require "braintree/version"
|
70
|
+
require "braintree/webhook_notification"
|
71
|
+
require "braintree/webhook_notification_gateway"
|
72
|
+
require "braintree/webhook_testing"
|
73
|
+
require "braintree/webhook_testing_gateway"
|
69
74
|
require "braintree/xml"
|
70
75
|
require "braintree/xml/generator"
|
71
76
|
require "braintree/xml/libxml"
|
data/lib/braintree/digest.rb
CHANGED
@@ -4,6 +4,20 @@ module Braintree
|
|
4
4
|
_hmac_sha1(private_key, string)
|
5
5
|
end
|
6
6
|
|
7
|
+
def self.secure_compare(left, right)
|
8
|
+
return false unless left && right
|
9
|
+
|
10
|
+
left_bytes = left.unpack("C*")
|
11
|
+
right_bytes = right.unpack("C*")
|
12
|
+
return false if left_bytes.size != right_bytes.size
|
13
|
+
|
14
|
+
result = 0
|
15
|
+
left_bytes.zip(right_bytes).each do |left_byte, right_byte|
|
16
|
+
result |= left_byte ^ right_byte
|
17
|
+
end
|
18
|
+
result == 0
|
19
|
+
end
|
20
|
+
|
7
21
|
def self._hmac_sha1(key, message)
|
8
22
|
key_digest = ::Digest::SHA1.digest(key)
|
9
23
|
sha1 = OpenSSL::Digest::Digest.new("sha1")
|
@@ -11,4 +25,3 @@ module Braintree
|
|
11
25
|
end
|
12
26
|
end
|
13
27
|
end
|
14
|
-
|
data/lib/braintree/exceptions.rb
CHANGED
@@ -21,6 +21,9 @@ module Braintree # :nodoc:
|
|
21
21
|
# See http://www.braintreepayments.com/docs/ruby/general/exceptions
|
22
22
|
class ForgedQueryString < BraintreeError; end
|
23
23
|
|
24
|
+
# See http://www.braintreepayments.com/docs/ruby/general/exceptions
|
25
|
+
class InvalidSignature < BraintreeError; end
|
26
|
+
|
24
27
|
# See http://www.braintreepayments.com/docs/ruby/general/exceptions
|
25
28
|
class NotFoundError < BraintreeError; end
|
26
29
|
|
data/lib/braintree/gateway.rb
CHANGED
@@ -126,6 +126,11 @@ module Braintree
|
|
126
126
|
Configuration.gateway.transaction.refund(id, amount)
|
127
127
|
end
|
128
128
|
|
129
|
+
# See http://www.braintreepayments.com/docs/ruby/transactions/refund
|
130
|
+
def self.refund!(id, amount = nil)
|
131
|
+
return_object_or_raise(:transaction) { refund(id, amount) }
|
132
|
+
end
|
133
|
+
|
129
134
|
# See http://www.braintreepayments.com/docs/ruby/transactions/create
|
130
135
|
def self.sale(attributes)
|
131
136
|
Configuration.gateway.transaction.sale(attributes)
|
data/lib/braintree/version.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Braintree
|
2
|
+
class WebhookNotification
|
3
|
+
include BaseModule
|
4
|
+
|
5
|
+
module Kind
|
6
|
+
SubscriptionCanceled = "subscription_canceled"
|
7
|
+
SubscriptionChargedSuccessfully = "subscription_charged_successfully"
|
8
|
+
SubscriptionChargedUnsuccessfully = "subscription_charged_unsuccessfully"
|
9
|
+
SubscriptionExpired = "subscription_expired"
|
10
|
+
SubscriptionTrialEnded = "subscription_trial_ended"
|
11
|
+
SubscriptionWentActive = "subscription_went_active"
|
12
|
+
SubscriptionWentPastDue = "subscription_went_past_due"
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :subscription, :kind, :timestamp
|
16
|
+
|
17
|
+
def self.parse(signature, payload)
|
18
|
+
Configuration.gateway.webhook_notification.parse(signature, payload)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.verify(challenge)
|
22
|
+
Configuration.gateway.webhook_notification.verify(challenge)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(gateway, attributes) # :nodoc:
|
26
|
+
@gateway = gateway
|
27
|
+
set_instance_variables_from_hash(attributes)
|
28
|
+
@subscription = Subscription._new(gateway, @subject[:subscription]) if @subject.has_key?(:subscription)
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
protected :new
|
33
|
+
def _new(*args) # :nodoc:
|
34
|
+
self.new *args
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Braintree
|
2
|
+
class WebhookNotificationGateway # :nodoc:
|
3
|
+
def initialize(gateway)
|
4
|
+
@gateway = gateway
|
5
|
+
@config = gateway.config
|
6
|
+
end
|
7
|
+
|
8
|
+
def parse(signature_string, payload)
|
9
|
+
_verify_signature(signature_string, payload)
|
10
|
+
attributes = Xml.hash_from_xml(Base64.decode64(payload))
|
11
|
+
WebhookNotification._new(@gateway, attributes[:notification])
|
12
|
+
end
|
13
|
+
|
14
|
+
def verify(challenge)
|
15
|
+
digest = Braintree::Digest.hexdigest(@config.private_key, challenge)
|
16
|
+
"#{@config.public_key}|#{digest}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def _matching_signature_pair(signature_string)
|
20
|
+
signature_pairs = signature_string.split("&")
|
21
|
+
valid_pairs = signature_pairs.select { |pair| pair.include?("|") }.map { |pair| pair.split("|") }
|
22
|
+
|
23
|
+
valid_pairs.detect do |public_key, signature|
|
24
|
+
public_key == @config.public_key
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def _verify_signature(signature, payload)
|
29
|
+
public_key, signature = _matching_signature_pair(signature)
|
30
|
+
payload_signature = Braintree::Digest.hexdigest(@config.private_key, payload)
|
31
|
+
|
32
|
+
raise InvalidSignature if public_key.nil?
|
33
|
+
raise InvalidSignature unless Braintree::Digest.secure_compare(signature, payload_signature)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Braintree
|
2
|
+
class WebhookTestingGateway # :nodoc:
|
3
|
+
def initialize(gateway)
|
4
|
+
@gateway = gateway
|
5
|
+
@config = gateway.config
|
6
|
+
end
|
7
|
+
|
8
|
+
def sample_notification(kind, id)
|
9
|
+
payload = Base64.encode64(_sample_xml(kind, id))
|
10
|
+
signature_string = "#{Braintree::Configuration.public_key}|#{Braintree::Digest.hexdigest(Braintree::Configuration.private_key, payload)}"
|
11
|
+
|
12
|
+
return signature_string, payload
|
13
|
+
end
|
14
|
+
|
15
|
+
def _sample_xml(kind, id)
|
16
|
+
<<-XML
|
17
|
+
<notification>
|
18
|
+
<timestamp type="datetime">#{Time.now.utc.iso8601}</timestamp>
|
19
|
+
<kind>#{kind}</kind>
|
20
|
+
<subject>
|
21
|
+
#{_subscription_sample_xml(id)}
|
22
|
+
</subject>
|
23
|
+
</notification>
|
24
|
+
XML
|
25
|
+
end
|
26
|
+
|
27
|
+
def _subscription_sample_xml(id)
|
28
|
+
<<-XML
|
29
|
+
<subscription>
|
30
|
+
<id>#{id}</id>
|
31
|
+
<transactions type="array">
|
32
|
+
</transactions>
|
33
|
+
<add_ons type="array">
|
34
|
+
</add_ons>
|
35
|
+
<discounts type="array">
|
36
|
+
</discounts>
|
37
|
+
</subscription>
|
38
|
+
XML
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -22,8 +22,8 @@ describe Braintree::Plan do
|
|
22
22
|
|
23
23
|
add_on_name = "ruby_add_on"
|
24
24
|
discount_name = "ruby_discount"
|
25
|
-
create_modification_for_tests(
|
26
|
-
create_modification_for_tests(
|
25
|
+
create_modification_for_tests(:kind => "add_on", :plan_id => plan_token, :amount => "1.00", :name => add_on_name)
|
26
|
+
create_modification_for_tests(:kind => "discount", :plan_id => plan_token, :amount => "1.00", :name => discount_name)
|
27
27
|
|
28
28
|
plans = Braintree::Plan.all
|
29
29
|
plan = plans.select { |plan| plan.id == plan_token }.first
|
@@ -44,6 +44,12 @@ describe Braintree::Plan do
|
|
44
44
|
plan.add_ons.first.name.should == add_on_name
|
45
45
|
plan.discounts.first.name.should == discount_name
|
46
46
|
end
|
47
|
+
|
48
|
+
it "returns an empty array if there are no plans" do
|
49
|
+
gateway = Braintree::Gateway.new(SpecHelper::TestMerchantConfig)
|
50
|
+
plans = gateway.plan.all
|
51
|
+
plans.should == []
|
52
|
+
end
|
47
53
|
end
|
48
54
|
|
49
55
|
def create_plan_for_tests(attributes)
|
@@ -934,6 +934,27 @@ describe Braintree::Transaction do
|
|
934
934
|
end
|
935
935
|
end
|
936
936
|
|
937
|
+
describe "self.refund!" do
|
938
|
+
it "returns the refund if valid refund" do
|
939
|
+
transaction = create_transaction_to_refund
|
940
|
+
|
941
|
+
refund_transaction = Braintree::Transaction.refund!(transaction.id)
|
942
|
+
|
943
|
+
refund_transaction.refunded_transaction_id.should == transaction.id
|
944
|
+
refund_transaction.type.should == "credit"
|
945
|
+
transaction.amount.should == refund_transaction.amount
|
946
|
+
end
|
947
|
+
|
948
|
+
it "raises a ValidationsFailed if invalid" do
|
949
|
+
transaction = create_transaction_to_refund
|
950
|
+
invalid_refund_amount = transaction.amount + 1
|
951
|
+
invalid_refund_amount.should be > transaction.amount
|
952
|
+
|
953
|
+
expect do
|
954
|
+
Braintree::Transaction.refund!(transaction.id,invalid_refund_amount)
|
955
|
+
end.to raise_error(Braintree::ValidationsFailed)
|
956
|
+
end
|
957
|
+
end
|
937
958
|
describe "self.sale" do
|
938
959
|
it "returns a successful result with type=sale if successful" do
|
939
960
|
result = Braintree::Transaction.sale(
|
data/spec/spec_helper.rb
CHANGED
@@ -75,6 +75,14 @@ unless defined?(SPEC_HELPER_LOADED)
|
|
75
75
|
Discount11 = "discount_11"
|
76
76
|
Discount15 = "discount_15"
|
77
77
|
|
78
|
+
TestMerchantConfig = Braintree::Configuration.new(
|
79
|
+
:logger => Logger.new("/dev/null"),
|
80
|
+
:environment => :development,
|
81
|
+
:merchant_id => "test_merchant_id",
|
82
|
+
:public_key => "test_public_key",
|
83
|
+
:private_key => "test_private_key"
|
84
|
+
)
|
85
|
+
|
78
86
|
def self.make_past_due(subscription, number_of_days_past_due = 1)
|
79
87
|
Braintree::Configuration.instantiate.http.put(
|
80
88
|
"/subscriptions/#{subscription.id}/make_past_due?days_past_due=#{number_of_days_past_due}"
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
|
2
|
+
|
3
|
+
describe Braintree::WebhookNotification do
|
4
|
+
describe "self.sample_notification" do
|
5
|
+
it "builds a sample notification and signature given an identifier and kind" do
|
6
|
+
signature, payload = Braintree::WebhookTesting.sample_notification(
|
7
|
+
Braintree::WebhookNotification::Kind::SubscriptionWentPastDue,
|
8
|
+
"my_id"
|
9
|
+
)
|
10
|
+
|
11
|
+
notification = Braintree::WebhookNotification.parse(signature, payload)
|
12
|
+
|
13
|
+
notification.kind.should == Braintree::WebhookNotification::Kind::SubscriptionWentPastDue
|
14
|
+
notification.subscription.id.should == "my_id"
|
15
|
+
notification.timestamp.should be_close(Time.now.utc, 10)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "includes a valid signature" do
|
19
|
+
signature, payload = Braintree::WebhookTesting.sample_notification(Braintree::WebhookNotification::Kind::SubscriptionWentPastDue, "my_id")
|
20
|
+
expected_signature = Braintree::Digest.hexdigest(Braintree::Configuration.private_key, payload)
|
21
|
+
|
22
|
+
signature.should == "#{Braintree::Configuration.public_key}|#{expected_signature}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "parse" do
|
27
|
+
it "raises InvalidSignature error the signature is completely invalid" do
|
28
|
+
signature, payload = Braintree::WebhookTesting.sample_notification(
|
29
|
+
Braintree::WebhookNotification::Kind::SubscriptionWentPastDue,
|
30
|
+
"my_id"
|
31
|
+
)
|
32
|
+
|
33
|
+
expect do
|
34
|
+
notification = Braintree::WebhookNotification.parse("not a valid signature", payload)
|
35
|
+
end.to raise_error(Braintree::InvalidSignature)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "raises InvalidSignature error the payload has been changed" do
|
39
|
+
signature, payload = Braintree::WebhookTesting.sample_notification(
|
40
|
+
Braintree::WebhookNotification::Kind::SubscriptionWentPastDue,
|
41
|
+
"my_id"
|
42
|
+
)
|
43
|
+
|
44
|
+
expect do
|
45
|
+
notification = Braintree::WebhookNotification.parse(signature, payload + "bad stuff")
|
46
|
+
end.to raise_error(Braintree::InvalidSignature)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "self.verify" do
|
51
|
+
it "creates a verification string" do
|
52
|
+
response = Braintree::WebhookNotification.verify("verification_token")
|
53
|
+
response.should == "integration_public_key|c9f15b74b0d98635cd182c51e2703cffa83388c3"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: braintree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 79
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
-
-
|
8
|
+
- 16
|
9
9
|
- 0
|
10
|
-
version: 2.
|
10
|
+
version: 2.16.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Braintree
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-04-
|
18
|
+
date: 2012-04-19 00:00:00 -05:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -96,6 +96,10 @@ files:
|
|
96
96
|
- lib/braintree/validation_error.rb
|
97
97
|
- lib/braintree/validation_error_collection.rb
|
98
98
|
- lib/braintree/version.rb
|
99
|
+
- lib/braintree/webhook_notification.rb
|
100
|
+
- lib/braintree/webhook_notification_gateway.rb
|
101
|
+
- lib/braintree/webhook_testing.rb
|
102
|
+
- lib/braintree/webhook_testing_gateway.rb
|
99
103
|
- lib/braintree/xml/generator.rb
|
100
104
|
- lib/braintree/xml/libxml.rb
|
101
105
|
- lib/braintree/xml/parser.rb
|
@@ -145,6 +149,7 @@ files:
|
|
145
149
|
- spec/unit/braintree/util_spec.rb
|
146
150
|
- spec/unit/braintree/validation_error_collection_spec.rb
|
147
151
|
- spec/unit/braintree/validation_error_spec.rb
|
152
|
+
- spec/unit/braintree/webhook_notification_spec.rb
|
148
153
|
- spec/unit/braintree/xml/libxml_spec.rb
|
149
154
|
- spec/unit/braintree/xml/parser_spec.rb
|
150
155
|
- spec/unit/braintree/xml/rexml_spec.rb
|