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 +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 [](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
|