Skip to content
Browse files

2.16.0

  • Loading branch information...
1 parent d0838f2 commit a5f18a53a1296f5269933ff25d2ce7049a1471f9 @braintreeps braintreeps committed Apr 19, 2012
View
6 CHANGELOG.rdoc
@@ -1,3 +1,9 @@
+== 2.16.0
+
+* Adds webhook gateways for parsing, verifying, and testing incoming
+notifications
+* Adds Transaction.refund!(id, amount = nil)
+
== 2.15.0
* Adds unique_number_identifier attribute to CreditCard
View
5 lib/braintree.rb
@@ -1,3 +1,4 @@
+require 'base64'
require "bigdecimal"
require "cgi"
require "date"
@@ -66,6 +67,10 @@
require "braintree/validation_error"
require "braintree/validation_error_collection"
require "braintree/version"
+require "braintree/webhook_notification"
+require "braintree/webhook_notification_gateway"
+require "braintree/webhook_testing"
+require "braintree/webhook_testing_gateway"
require "braintree/xml"
require "braintree/xml/generator"
require "braintree/xml/libxml"
View
15 lib/braintree/digest.rb
@@ -4,11 +4,24 @@ def self.hexdigest(private_key, string)
_hmac_sha1(private_key, string)
end
+ def self.secure_compare(left, right)
+ return false unless left && right
+
+ left_bytes = left.unpack("C*")
+ right_bytes = right.unpack("C*")
+ return false if left_bytes.size != right_bytes.size
+
+ result = 0
+ left_bytes.zip(right_bytes).each do |left_byte, right_byte|
+ result |= left_byte ^ right_byte
+ end
+ result == 0
+ end
+
def self._hmac_sha1(key, message)
key_digest = ::Digest::SHA1.digest(key)
sha1 = OpenSSL::Digest::Digest.new("sha1")
OpenSSL::HMAC.hexdigest(sha1, key_digest, message.to_s)
end
end
end
-
View
3 lib/braintree/exceptions.rb
@@ -22,6 +22,9 @@ class DownForMaintenanceError < BraintreeError; end
class ForgedQueryString < BraintreeError; end
# See http://www.braintreepayments.com/docs/ruby/general/exceptions
+ class InvalidSignature < BraintreeError; end
+
+ # See http://www.braintreepayments.com/docs/ruby/general/exceptions
class NotFoundError < BraintreeError; end
# See http://www.braintreepayments.com/docs/ruby/general/exceptions
View
8 lib/braintree/gateway.rb
@@ -51,5 +51,13 @@ def transparent_redirect
def transaction
TransactionGateway.new(self)
end
+
+ def webhook_notification
+ WebhookNotificationGateway.new(self)
+ end
+
+ def webhook_testing
+ WebhookTestingGateway.new(self)
+ end
end
end
View
2 lib/braintree/plan_gateway.rb
@@ -7,7 +7,7 @@ def initialize(gateway)
def all
response = @config.http.get "/plans"
- attributes_collection = response[:plans]
+ attributes_collection = response[:plans] || []
attributes_collection.map do |attributes|
Plan._new(@gateway, attributes)
end
View
5 lib/braintree/transaction.rb
@@ -126,6 +126,11 @@ def self.refund(id, amount = nil)
Configuration.gateway.transaction.refund(id, amount)
end
+ # See http://www.braintreepayments.com/docs/ruby/transactions/refund
+ def self.refund!(id, amount = nil)
+ return_object_or_raise(:transaction) { refund(id, amount) }
+ end
+
# See http://www.braintreepayments.com/docs/ruby/transactions/create
def self.sale(attributes)
Configuration.gateway.transaction.sale(attributes)
View
2 lib/braintree/version.rb
@@ -1,7 +1,7 @@
module Braintree
module Version
Major = 2
- Minor = 15
+ Minor = 16
Tiny = 0
String = "#{Major}.#{Minor}.#{Tiny}"
View
38 lib/braintree/webhook_notification.rb
@@ -0,0 +1,38 @@
+module Braintree
+ class WebhookNotification
+ include BaseModule
+
+ module Kind
+ SubscriptionCanceled = "subscription_canceled"
+ SubscriptionChargedSuccessfully = "subscription_charged_successfully"
+ SubscriptionChargedUnsuccessfully = "subscription_charged_unsuccessfully"
+ SubscriptionExpired = "subscription_expired"
+ SubscriptionTrialEnded = "subscription_trial_ended"
+ SubscriptionWentActive = "subscription_went_active"
+ SubscriptionWentPastDue = "subscription_went_past_due"
+ end
+
+ attr_reader :subscription, :kind, :timestamp
+
+ def self.parse(signature, payload)
+ Configuration.gateway.webhook_notification.parse(signature, payload)
+ end
+
+ def self.verify(challenge)
+ Configuration.gateway.webhook_notification.verify(challenge)
+ end
+
+ def initialize(gateway, attributes) # :nodoc:
+ @gateway = gateway
+ set_instance_variables_from_hash(attributes)
+ @subscription = Subscription._new(gateway, @subject[:subscription]) if @subject.has_key?(:subscription)
+ end
+
+ class << self
+ protected :new
+ def _new(*args) # :nodoc:
+ self.new *args
+ end
+ end
+ end
+end
View
36 lib/braintree/webhook_notification_gateway.rb
@@ -0,0 +1,36 @@
+module Braintree
+ class WebhookNotificationGateway # :nodoc:
+ def initialize(gateway)
+ @gateway = gateway
+ @config = gateway.config
+ end
+
+ def parse(signature_string, payload)
+ _verify_signature(signature_string, payload)
+ attributes = Xml.hash_from_xml(Base64.decode64(payload))
+ WebhookNotification._new(@gateway, attributes[:notification])
+ end
+
+ def verify(challenge)
+ digest = Braintree::Digest.hexdigest(@config.private_key, challenge)
+ "#{@config.public_key}|#{digest}"
+ end
+
+ def _matching_signature_pair(signature_string)
+ signature_pairs = signature_string.split("&")
+ valid_pairs = signature_pairs.select { |pair| pair.include?("|") }.map { |pair| pair.split("|") }
+
+ valid_pairs.detect do |public_key, signature|
+ public_key == @config.public_key
+ end
+ end
+
+ def _verify_signature(signature, payload)
+ public_key, signature = _matching_signature_pair(signature)
+ payload_signature = Braintree::Digest.hexdigest(@config.private_key, payload)
+
+ raise InvalidSignature if public_key.nil?
+ raise InvalidSignature unless Braintree::Digest.secure_compare(signature, payload_signature)
+ end
+ end
+end
View
7 lib/braintree/webhook_testing.rb
@@ -0,0 +1,7 @@
+module Braintree
+ class WebhookTesting # :nodoc:
+ def self.sample_notification(kind, id)
+ Configuration.gateway.webhook_testing.sample_notification(kind, id)
+ end
+ end
+end
View
41 lib/braintree/webhook_testing_gateway.rb
@@ -0,0 +1,41 @@
+module Braintree
+ class WebhookTestingGateway # :nodoc:
+ def initialize(gateway)
+ @gateway = gateway
+ @config = gateway.config
+ end
+
+ def sample_notification(kind, id)
+ payload = Base64.encode64(_sample_xml(kind, id))
+ signature_string = "#{Braintree::Configuration.public_key}|#{Braintree::Digest.hexdigest(Braintree::Configuration.private_key, payload)}"
+
+ return signature_string, payload
+ end
+
+ def _sample_xml(kind, id)
+ <<-XML
+ <notification>
+ <timestamp type="datetime">#{Time.now.utc.iso8601}</timestamp>
+ <kind>#{kind}</kind>
+ <subject>
+ #{_subscription_sample_xml(id)}
+ </subject>
+ </notification>
+ XML
+ end
+
+ def _subscription_sample_xml(id)
+ <<-XML
+ <subscription>
+ <id>#{id}</id>
+ <transactions type="array">
+ </transactions>
+ <add_ons type="array">
+ </add_ons>
+ <discounts type="array">
+ </discounts>
+ </subscription>
+ XML
+ end
+ end
+end
View
10 spec/integration/braintree/plan_spec.rb
@@ -22,8 +22,8 @@
add_on_name = "ruby_add_on"
discount_name = "ruby_discount"
- create_modification_for_tests({ :kind => "add_on", :plan_id => plan_token, :amount => "1.00", :name => add_on_name })
- create_modification_for_tests({ :kind => "discount", :plan_id => plan_token, :amount => "1.00", :name => discount_name })
+ create_modification_for_tests(:kind => "add_on", :plan_id => plan_token, :amount => "1.00", :name => add_on_name)
+ create_modification_for_tests(:kind => "discount", :plan_id => plan_token, :amount => "1.00", :name => discount_name)
plans = Braintree::Plan.all
plan = plans.select { |plan| plan.id == plan_token }.first
@@ -44,6 +44,12 @@
plan.add_ons.first.name.should == add_on_name
plan.discounts.first.name.should == discount_name
end
+
+ it "returns an empty array if there are no plans" do
+ gateway = Braintree::Gateway.new(SpecHelper::TestMerchantConfig)
+ plans = gateway.plan.all
+ plans.should == []
+ end
end
def create_plan_for_tests(attributes)
View
21 spec/integration/braintree/transaction_spec.rb
@@ -934,6 +934,27 @@
end
end
+ describe "self.refund!" do
+ it "returns the refund if valid refund" do
+ transaction = create_transaction_to_refund
+
+ refund_transaction = Braintree::Transaction.refund!(transaction.id)
+
+ refund_transaction.refunded_transaction_id.should == transaction.id
+ refund_transaction.type.should == "credit"
+ transaction.amount.should == refund_transaction.amount
+ end
+
+ it "raises a ValidationsFailed if invalid" do
+ transaction = create_transaction_to_refund
+ invalid_refund_amount = transaction.amount + 1
+ invalid_refund_amount.should be > transaction.amount
+
+ expect do
+ Braintree::Transaction.refund!(transaction.id,invalid_refund_amount)
+ end.to raise_error(Braintree::ValidationsFailed)
+ end
+ end
describe "self.sale" do
it "returns a successful result with type=sale if successful" do
result = Braintree::Transaction.sale(
View
8 spec/spec_helper.rb
@@ -75,6 +75,14 @@ module SpecHelper
Discount11 = "discount_11"
Discount15 = "discount_15"
+ TestMerchantConfig = Braintree::Configuration.new(
+ :logger => Logger.new("/dev/null"),
+ :environment => :development,
+ :merchant_id => "test_merchant_id",
+ :public_key => "test_public_key",
+ :private_key => "test_private_key"
+ )
+
def self.make_past_due(subscription, number_of_days_past_due = 1)
Braintree::Configuration.instantiate.http.put(
"/subscriptions/#{subscription.id}/make_past_due?days_past_due=#{number_of_days_past_due}"
View
56 spec/unit/braintree/webhook_notification_spec.rb
@@ -0,0 +1,56 @@
+require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
+
+describe Braintree::WebhookNotification do
+ describe "self.sample_notification" do
+ it "builds a sample notification and signature given an identifier and kind" do
+ signature, payload = Braintree::WebhookTesting.sample_notification(
+ Braintree::WebhookNotification::Kind::SubscriptionWentPastDue,
+ "my_id"
+ )
+
+ notification = Braintree::WebhookNotification.parse(signature, payload)
+
+ notification.kind.should == Braintree::WebhookNotification::Kind::SubscriptionWentPastDue
+ notification.subscription.id.should == "my_id"
+ notification.timestamp.should be_close(Time.now.utc, 10)
+ end
+
+ it "includes a valid signature" do
+ signature, payload = Braintree::WebhookTesting.sample_notification(Braintree::WebhookNotification::Kind::SubscriptionWentPastDue, "my_id")
+ expected_signature = Braintree::Digest.hexdigest(Braintree::Configuration.private_key, payload)
+
+ signature.should == "#{Braintree::Configuration.public_key}|#{expected_signature}"
+ end
+ end
+
+ describe "parse" do
+ it "raises InvalidSignature error the signature is completely invalid" do
+ signature, payload = Braintree::WebhookTesting.sample_notification(
+ Braintree::WebhookNotification::Kind::SubscriptionWentPastDue,
+ "my_id"
+ )
+
+ expect do
+ notification = Braintree::WebhookNotification.parse("not a valid signature", payload)
+ end.to raise_error(Braintree::InvalidSignature)
+ end
+
+ it "raises InvalidSignature error the payload has been changed" do
+ signature, payload = Braintree::WebhookTesting.sample_notification(
+ Braintree::WebhookNotification::Kind::SubscriptionWentPastDue,
+ "my_id"
+ )
+
+ expect do
+ notification = Braintree::WebhookNotification.parse(signature, payload + "bad stuff")
+ end.to raise_error(Braintree::InvalidSignature)
+ end
+ end
+
+ describe "self.verify" do
+ it "creates a verification string" do
+ response = Braintree::WebhookNotification.verify("verification_token")
+ response.should == "integration_public_key|c9f15b74b0d98635cd182c51e2703cffa83388c3"
+ end
+ end
+end

0 comments on commit a5f18a5

Please sign in to comment.
Something went wrong with that request. Please try again.