braintree 2.15.0 → 2.16.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.
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"
@@ -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
-
@@ -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
 
@@ -51,5 +51,13 @@ module Braintree
51
51
  def transaction
52
52
  TransactionGateway.new(self)
53
53
  end
54
+
55
+ def webhook_notification
56
+ WebhookNotificationGateway.new(self)
57
+ end
58
+
59
+ def webhook_testing
60
+ WebhookTestingGateway.new(self)
61
+ end
54
62
  end
55
63
  end
@@ -7,7 +7,7 @@ module Braintree
7
7
 
8
8
  def all
9
9
  response = @config.http.get "/plans"
10
- attributes_collection = response[:plans]
10
+ attributes_collection = response[:plans] || []
11
11
  attributes_collection.map do |attributes|
12
12
  Plan._new(@gateway, attributes)
13
13
  end
@@ -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)
@@ -1,7 +1,7 @@
1
1
  module Braintree
2
2
  module Version
3
3
  Major = 2
4
- Minor = 15
4
+ Minor = 16
5
5
  Tiny = 0
6
6
 
7
7
  String = "#{Major}.#{Minor}.#{Tiny}"
@@ -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,7 @@
1
+ module Braintree
2
+ class WebhookTesting # :nodoc:
3
+ def self.sample_notification(kind, id)
4
+ Configuration.gateway.webhook_testing.sample_notification(kind, id)
5
+ end
6
+ end
7
+ 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({ :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 })
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: 51
4
+ hash: 79
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
- - 15
8
+ - 16
9
9
  - 0
10
- version: 2.15.0
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-05 00:00:00 -05:00
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