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 +5 -0
- data/Changelog.md +7 -0
- data/README.md +103 -6
- data/Rakefile +2 -2
- data/fake_braintree.gemspec +6 -1
- data/lib/fake_braintree.rb +49 -10
- data/lib/fake_braintree/customer.rb +81 -25
- data/lib/fake_braintree/helpers.rb +4 -0
- data/lib/fake_braintree/redirect.rb +37 -0
- data/lib/fake_braintree/sinatra_app.rb +48 -15
- data/lib/fake_braintree/subscription.rb +62 -11
- data/lib/fake_braintree/version.rb +1 -1
- data/spec/fake_braintree/credit_card_spec.rb +8 -15
- data/spec/fake_braintree/customer_spec.rb +56 -48
- data/spec/fake_braintree/subscription_spec.rb +67 -41
- data/spec/fake_braintree/transaction_spec.rb +44 -0
- data/spec/fake_braintree/transparent_redirect_spec.rb +76 -0
- data/spec/fake_braintree_spec.rb +79 -26
- data/spec/spec_helper.rb +26 -11
- data/spec/support/braintree_helpers.rb +8 -4
- data/spec/support/customer_helpers.rb +12 -0
- data/spec/support/subscription_helpers.rb +6 -0
- metadata +89 -18
data/Changelog.md
CHANGED
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
|
-
|
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
|
-
|
7
|
-
|
36
|
+
Just require the library and you're good to go:
|
37
|
+
|
38
|
+
require 'fake_braintree'
|
8
39
|
|
9
|
-
|
40
|
+
`FakeBraintree.clear!` will clear all data, which you almost certainly want to
|
41
|
+
do before each test.
|
10
42
|
|
11
|
-
|
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
data/fake_braintree.gemspec
CHANGED
@@ -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 '
|
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
|
data/lib/fake_braintree.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
require 'braintree'
|
3
|
-
require '
|
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
|
-
@
|
21
|
+
@transactions = {}
|
22
|
+
@redirects = {}
|
19
23
|
|
20
24
|
@decline_all_cards = false
|
21
25
|
@verify_all_cards = false
|
22
|
-
attr_accessor :customers, :subscriptions, :failures, :
|
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
|
-
|
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
|
-
|
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.
|
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(
|
6
|
-
@
|
7
|
-
|
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
|
11
|
-
|
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
|
15
|
-
|
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 = @
|
35
|
+
hash = @customer_hash.dup
|
20
36
|
hash["id"] ||= create_id
|
21
|
-
|
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"]
|
26
|
-
hash["credit_card"]["token"]
|
27
|
-
|
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
|
42
|
-
|
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
|
-
@
|
47
|
-
FakeBraintree.failure?(@
|
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?(@
|
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
|
-
@
|
58
|
-
@
|
59
|
-
@
|
60
|
-
@
|
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
|
-
|
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
|
@@ -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
|