fake_braintree 0.0.5 → 0.0.6

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/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ rvm:
2
+ - ree
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
data/Changelog.md CHANGED
@@ -1,3 +1,10 @@
1
+ # 0.0.6
2
+ * Flesh out the README
3
+ * Add support for transparent redirect
4
+ * Add basic support for adding add-ons
5
+ * Add basic support for adding discounts
6
+ * Lots of internal refactorings
7
+
1
8
  # 0.0.5
2
9
  * Add support for Braintree::Customer.find
3
10
 
data/README.md CHANGED
@@ -1,14 +1,49 @@
1
- # fake\_braintree, a Braintree fake
1
+ # fake\_braintree, a Braintree fake [![Build Status](https://secure.travis-ci.org/thoughtbot/fake_braintree.png)](http://travis-ci.org/thoughtbot/fake_braintree)
2
2
 
3
- Currently in alpha.
3
+
4
+ This library is a way to test Braintree code without hitting Braintree's servers.
5
+ It uses [Capybara::Server](https://github.com/jnicklas/capybara/blob/master/lib/capybara/server.rb)
6
+ to intercept all of the calls from Braintree's Ruby library and returns XML that the Braintree library
7
+ can parse. The whole point is not to hit the Braintree API.
8
+
9
+ Currently in alpha (i.e. it does not support every Braintree call).
10
+
11
+ ## Supported API methods
12
+
13
+ ### Customer
14
+ * `Braintree::Customer.find`
15
+ * `Braintree::Customer.create` (including adding add-ons and discounts)
16
+ * `Braintree::Customer.update`
17
+ * `Braintree::Customer.delete`
18
+
19
+ ### Subscription
20
+ * `Braintree::Subscription.find`
21
+ * `Braintree::Subscription.create`
22
+ * `Braintree::Subscription.update`
23
+
24
+ ### CreditCard
25
+ * `Braintree::CreditCard.find`
26
+ * `Braintree::CreditCard.sale`
27
+
28
+ ### Transaction
29
+ * `Braintree::Transaction.sale`
30
+
31
+ ### TransparentRedirect
32
+ * `Braintree::TransparentRedirect.url`
33
+ * `Braintree::TransparentRedirect.confirm` (only for creating customers)
4
34
 
5
35
  ## Quick start
6
- Call `FakeBraintree.activate!` to make it go. `FakeBraintree.clear!` will clear
7
- all data, which you probably want to do before each test.
36
+ Just require the library and you're good to go:
37
+
38
+ require 'fake_braintree'
8
39
 
9
- Example, in spec\_helper.rb:
40
+ `FakeBraintree.clear!` will clear all data, which you almost certainly want to
41
+ do before each test.
10
42
 
11
- FakeBraintree.activate!
43
+ Full example:
44
+
45
+ # spec_helper.rb
46
+ require 'fake_braintree'
12
47
 
13
48
  RSpec.configure do |c|
14
49
  c.before do
@@ -16,6 +51,15 @@ Example, in spec\_helper.rb:
16
51
  end
17
52
  end
18
53
 
54
+ If you're using Cucumber, add this too:
55
+
56
+ # env.rb
57
+ require 'fake_braintree'
58
+
59
+ Before do
60
+ FakeBraintree.clear!
61
+ end
62
+
19
63
  ## Verifying credit cards
20
64
 
21
65
  To verify every credit card you try to use, call:
@@ -30,3 +74,56 @@ Calling FakeBraintree.clear! _will not_ change this setting. It does very basic
30
74
  verification: it only matches the credit card number against these:
31
75
  http://www.braintreepayments.com/docs/ruby/reference/sandbox and rejects them if
32
76
  they aren't one of the listed numbers.
77
+
78
+ ## Declining credit cards
79
+
80
+ To decline every card you try, call:
81
+
82
+ FakeBraintree.decline_all_cards!
83
+
84
+ This will decline all cards until you call
85
+
86
+ FakeBraintree.clear!
87
+
88
+ This behavior is different from `FakeBraintree.verify_all_cards`, which will
89
+ stay on even when `clear!` is called.
90
+
91
+ Note that after `decline_all_cards!` is set, Braintree will still create
92
+ customers, but will not be able to charge them (so charging for e.g. a subscription
93
+ will fail). Setting `verify_all_cards!`, on the other hand, will prevent
94
+ creation of customers with bad credit cards - Braintree won't even get to trying
95
+ to charge them.
96
+
97
+ ## Generating transactions
98
+
99
+ You can generate a transaction using `FakeBraintree.generate_transaction`. This
100
+ is for use in testing, e.g.
101
+ `before { user.transaction = FakeBraintree.generate_transaction }`.
102
+
103
+ It takes the following options:
104
+
105
+ * `:subscription_id`: the ID of the subscription associated with the transaction.
106
+ * `:created_at`: when the transaction was created (defaults to `Time.now`)
107
+ * `:amount`: the amount of the transaction
108
+ * `:status`: the status of the transaction, e.g. `Braintree::Transaction::Status::Failed`
109
+
110
+ Any or all of these can be nil, and in fact are nil by default. You can also
111
+ call it with no arguments.
112
+
113
+ Full example:
114
+
115
+ transaction = FakeBraintree.generate_transaction(:amount => '20.00',
116
+ :status => Braintree::Transaction::Status::Settled,
117
+ :subscription_id => 'foobar',
118
+ :created_at => Time.now + 60)
119
+ p transaction
120
+ # {
121
+ # "status_history" =>
122
+ # [{
123
+ # "timestamp" => 2011-11-20 12:57:25 -0500,
124
+ # "amount" => "20.00",
125
+ # "status" => "settled",
126
+ # "created_at" => 2011-11-20 12:58:25 -0500
127
+ # }],
128
+ # "subscription_id" => "foobar"
129
+ # }
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
+ require "bundler/setup"
1
2
  require "bundler/gem_tasks"
2
-
3
- require 'rspec/core/rake_task'
3
+ require "rspec/core/rake_task"
4
4
 
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
 
@@ -16,12 +16,17 @@ Gem::Specification.new do |s|
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
 
19
- s.add_dependency 'sham_rack'
19
+ s.add_dependency 'capybara'
20
20
  s.add_dependency 'activesupport'
21
21
  s.add_dependency 'i18n'
22
22
  s.add_dependency 'sinatra'
23
23
  s.add_dependency 'braintree', '~> 2.5'
24
+ s.add_dependency 'mongrel', '~> 1.2.0.pre'
24
25
 
25
26
  s.add_development_dependency 'rspec', '~> 2.6.0'
26
27
  s.add_development_dependency 'mocha', '~> 0.9.12'
28
+ s.add_development_dependency 'timecop', '~> 0.3.5'
29
+ s.add_development_dependency 'spork', '~> 0.9.0.rc9'
30
+ s.add_development_dependency 'bundler', '>= 1.0.14'
31
+ s.add_development_dependency 'rake'
27
32
  end
@@ -1,10 +1,13 @@
1
1
  require 'fileutils'
2
2
  require 'braintree'
3
- require 'sham_rack'
3
+ require 'capybara'
4
+ require 'capybara/server'
5
+ require 'rack/handler/mongrel'
4
6
 
5
7
  require 'fake_braintree/helpers'
6
8
  require 'fake_braintree/customer'
7
9
  require 'fake_braintree/subscription'
10
+ require 'fake_braintree/redirect'
8
11
 
9
12
  require 'fake_braintree/sinatra_app'
10
13
  require 'fake_braintree/valid_credit_cards'
@@ -15,20 +18,18 @@ module FakeBraintree
15
18
  @customers = {}
16
19
  @subscriptions = {}
17
20
  @failures = {}
18
- @transaction = {}
21
+ @transactions = {}
22
+ @redirects = {}
19
23
 
20
24
  @decline_all_cards = false
21
25
  @verify_all_cards = false
22
- attr_accessor :customers, :subscriptions, :failures, :transaction, :decline_all_cards, :verify_all_cards
26
+ attr_accessor :customers, :subscriptions, :failures, :transactions, :decline_all_cards, :verify_all_cards, :redirects
23
27
  end
24
28
 
25
29
  def self.activate!
26
- Braintree::Configuration.environment = :production
27
- Braintree::Configuration.merchant_id = "xxx"
28
- Braintree::Configuration.public_key = "xxx"
29
- Braintree::Configuration.private_key = "xxx"
30
+ set_configuration
30
31
  clear!
31
- ShamRack.mount(FakeBraintree::SinatraApp, "www.braintreegateway.com", 443)
32
+ boot_server
32
33
  end
33
34
 
34
35
  def self.log_file_path
@@ -39,7 +40,8 @@ module FakeBraintree
39
40
  self.customers = {}
40
41
  self.subscriptions = {}
41
42
  self.failures = {}
42
- self.transaction = {}
43
+ self.transactions = {}
44
+ self.redirects = {}
43
45
  self.decline_all_cards = false
44
46
  clear_log!
45
47
  end
@@ -53,7 +55,7 @@ module FakeBraintree
53
55
  self.failures.include?(card_number)
54
56
  end
55
57
 
56
- def self.failure_response(card_number)
58
+ def self.failure_response(card_number = nil)
57
59
  failure = self.failures[card_number] || {}
58
60
  failure["errors"] ||= { "errors" => [] }
59
61
 
@@ -96,6 +98,43 @@ module FakeBraintree
96
98
  return card if card
97
99
  end
98
100
  end
101
+
102
+ def self.generate_transaction(options = {})
103
+ history_item = { 'timestamp' => Time.now,
104
+ 'amount' => options[:amount],
105
+ 'status' => options[:status] }
106
+ created_at = options[:created_at] || Time.now
107
+ {'status_history' => [history_item],
108
+ 'subscription_id' => options[:subscription_id],
109
+ 'created_at' => created_at }
110
+ end
111
+
112
+ private
113
+
114
+ def self.set_configuration
115
+ Braintree::Configuration.environment = :development
116
+ Braintree::Configuration.merchant_id = "xxx"
117
+ Braintree::Configuration.public_key = "xxx"
118
+ Braintree::Configuration.private_key = "xxx"
119
+ end
120
+
121
+ def self.boot_server
122
+ with_mongrel_runner do
123
+ server = Capybara::Server.new(FakeBraintree::SinatraApp)
124
+ server.boot
125
+ ENV['GATEWAY_PORT'] = server.port.to_s
126
+ end
127
+ end
128
+
129
+ def self.with_mongrel_runner
130
+ default_server_process = Capybara.server
131
+ Capybara.server do |app, port|
132
+ Rack::Handler::Mongrel.run(app, :Port => port)
133
+ end
134
+ yield
135
+ ensure
136
+ Capybara.server(&default_server_process)
137
+ end
99
138
  end
100
139
 
101
140
  FakeBraintree.activate!
@@ -2,31 +2,44 @@ module FakeBraintree
2
2
  class Customer
3
3
  include Helpers
4
4
 
5
- def initialize(request, merchant_id)
6
- @request_hash = Hash.from_xml(request.body).delete("customer")
7
- @merchant_id = merchant_id
5
+ def initialize(customer_hash, options)
6
+ @customer_hash = customer_hash.merge("merchant_id" => options[:merchant_id],
7
+ "id" => options[:id])
8
8
  end
9
9
 
10
- def invalid?
11
- credit_card_is_failure? || invalid_credit_card?
10
+ def create
11
+ if invalid?
12
+ failure_response
13
+ else
14
+ hash = customer_hash
15
+ FakeBraintree.customers[hash["id"]] = hash
16
+ gzipped_response(201, hash.to_xml(:root => 'customer'))
17
+ end
12
18
  end
13
19
 
14
- def failure_response
15
- gzipped_response(422, FakeBraintree.failure_response(@request_hash["credit_card"]["number"]).to_xml(:root => 'api_error_response'))
20
+ def update
21
+ if existing_customer_hash
22
+ hash = update_existing_customer!
23
+ gzipped_response(200, hash.to_xml(:root => 'customer'))
24
+ else
25
+ failure_response(404)
26
+ end
27
+ end
28
+
29
+ def delete
30
+ FakeBraintree.customers[existing_customer_id] = nil
31
+ gzipped_response(200, '')
16
32
  end
17
33
 
18
34
  def customer_hash
19
- hash = @request_hash.dup
35
+ hash = @customer_hash.dup
20
36
  hash["id"] ||= create_id
21
- hash["merchant-id"] = @merchant_id
37
+
22
38
  if hash["credit_card"] && hash["credit_card"].is_a?(Hash)
23
- hash["credit_card"].delete("__content__")
24
39
  if !hash["credit_card"].empty?
25
- hash["credit_card"]["last_4"] = hash["credit_card"].delete("number")[-4..-1]
26
- hash["credit_card"]["token"] = md5("#{hash['merchant_id']}#{hash['id']}")
27
- expiration_date = hash["credit_card"].delete("expiration_date")
28
- hash["credit_card"]["expiration_month"] = expiration_date.split('/')[0]
29
- hash["credit_card"]["expiration_year"] = expiration_date.split('/')[1]
40
+ hash["credit_card"]["last_4"] = last_four(hash)
41
+ hash["credit_card"]["token"] = credit_card_token(hash)
42
+ split_expiration_date_into_month_and_year!(hash)
30
43
 
31
44
  credit_card = hash.delete("credit_card")
32
45
  hash["credit_cards"] = [credit_card]
@@ -38,30 +51,73 @@ module FakeBraintree
38
51
 
39
52
  private
40
53
 
41
- def create_id
42
- md5("#{@merchant_id}#{Time.now.to_f}")
54
+ def invalid?
55
+ credit_card_is_failure? || invalid_credit_card?
56
+ end
57
+
58
+ def split_expiration_date_into_month_and_year!(hash)
59
+ if expiration_date = hash["credit_card"].delete("expiration_date")
60
+ hash["credit_card"]["expiration_month"] = expiration_date.split('/')[0]
61
+ hash["credit_card"]["expiration_year"] = expiration_date.split('/')[1]
62
+ end
63
+ end
64
+
65
+ def existing_customer_hash
66
+ existing_customer_id && FakeBraintree.customers[existing_customer_id]
67
+ end
68
+
69
+ def update_existing_customer!
70
+ existing_customer_hash.merge!(customer_hash)
71
+ end
72
+
73
+ def credit_card_token(hash)
74
+ md5("#{hash['merchant_id']}#{hash['id']}")
75
+ end
76
+
77
+ def last_four(hash)
78
+ hash["credit_card"].delete("number")[-4..-1]
79
+ end
80
+
81
+ def failure_response(code = 422)
82
+ gzipped_response(code, FakeBraintree.failure_response(credit_card_number).to_xml(:root => 'api_error_response'))
43
83
  end
44
84
 
45
85
  def credit_card_is_failure?
46
- @request_hash.key?('credit_card') &&
47
- FakeBraintree.failure?(@request_hash["credit_card"]["number"])
86
+ @customer_hash.key?('credit_card') &&
87
+ FakeBraintree.failure?(@customer_hash["credit_card"]["number"])
48
88
  end
49
89
 
50
90
  def invalid_credit_card?
51
- verify_credit_card?(@request_hash) && has_invalid_credit_card?(@request_hash)
91
+ verify_credit_card?(@customer_hash) && has_invalid_credit_card?(@customer_hash)
52
92
  end
53
93
 
54
94
  def verify_credit_card?(customer_hash)
55
95
  return true if FakeBraintree.verify_all_cards
56
96
 
57
- @request_hash.key?("credit_card") &&
58
- @request_hash["credit_card"].key?("options") &&
59
- @request_hash["credit_card"]["options"].is_a?(Hash) &&
60
- @request_hash["credit_card"]["options"]["verify_card"] == true
97
+ @customer_hash.key?("credit_card") &&
98
+ @customer_hash["credit_card"].is_a?(Hash) &&
99
+ @customer_hash["credit_card"].key?("options") &&
100
+ @customer_hash["credit_card"]["options"].is_a?(Hash) &&
101
+ @customer_hash["credit_card"]["options"]["verify_card"] == true
61
102
  end
62
103
 
63
104
  def has_invalid_credit_card?(customer_hash)
64
- ! FakeBraintree::VALID_CREDIT_CARDS.include?(@request_hash["credit_card"]["number"])
105
+ has_credit_card_number? &&
106
+ ! FakeBraintree::VALID_CREDIT_CARDS.include?(@customer_hash["credit_card"]["number"])
107
+ end
108
+
109
+ def has_credit_card_number?
110
+ @customer_hash.key?("credit_card") &&
111
+ @customer_hash["credit_card"].is_a?(Hash) &&
112
+ @customer_hash["credit_card"].key?("number")
113
+ end
114
+
115
+ def credit_card_number
116
+ has_credit_card_number? && @customer_hash["credit_card"]["number"]
117
+ end
118
+
119
+ def existing_customer_id
120
+ @customer_hash['id']
65
121
  end
66
122
  end
67
123
  end
@@ -15,5 +15,9 @@ module FakeBraintree
15
15
  def md5(content)
16
16
  Digest::MD5.hexdigest(content)
17
17
  end
18
+
19
+ def create_id
20
+ md5("#{@merchant_id}#{Time.now.to_f}")
21
+ end
18
22
  end
19
23
  end
@@ -0,0 +1,37 @@
1
+ module FakeBraintree
2
+ class Redirect
3
+ include Helpers
4
+
5
+ attr_reader :id
6
+
7
+ def initialize(params, merchant_id)
8
+ hash, query = *params[:tr_data].split("|", 2)
9
+ @transparent_data = Rack::Utils.parse_query(query)
10
+ @merchant_id = merchant_id
11
+ @id = create_id
12
+ @params = params
13
+ end
14
+
15
+ def url
16
+ uri.to_s
17
+ end
18
+
19
+ def confirm
20
+ Customer.new(@params["customer"], {:merchant_id => @merchant_id}).create
21
+ end
22
+
23
+ private
24
+
25
+ def uri
26
+ URI.parse(@transparent_data["redirect_url"]).merge("?#{base_query}&hash=#{hash(base_query)}")
27
+ end
28
+
29
+ def base_query
30
+ "http_status=200&id=#{@id}&kind=create_customer"
31
+ end
32
+
33
+ def hash(string)
34
+ Braintree::Digest.hexdigest(Braintree::Configuration.private_key, string)
35
+ end
36
+ end
37
+ end