fake_braintree 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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