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