I'm trying to test the scenario in my Rails app where a customer has allowed a subscription to go to 'unpaid' (usually because the card expired and was not updated during the two weeks while Stripe retried the charge) and is finally getting around to updating the card and reactivating the account. I believe I have the logic correct (update the card and pay each unpaid invoice) but I'd like to be able to test it, or better yet, write some RSpec tests (a feature test and possibly a controller test). The problem is, I can't figure out how to create a mock situation in which a subscription is 'unpaid'. (I suppose I could create a bunch of accounts with expired cards and wait two weeks to test, but that's not an acceptable solution. I can't even change the subscription retry settings for only the 'test' context to speed up the process.) I found stripe-ruby-mock but I can't manually set the status of a subscription.
Here's what I've tried:
plan = Stripe::Plan.create(id: 'test')
customer = Stripe::Customer.create(id: 'test_customer', card: 'tk', plan: 'test')
sub = customer.subscriptions.retrieve(customer.subscriptions.data.first.id)
sub.status = 'unpaid'
sub.save
sub = customer.subscriptions.retrieve(customer.subscriptions.data.first.id)
expect(sub.status).to eq 'unpaid'
This was the result with stripe-ruby-mock:
Failure/Error: expect(sub.status).to eq 'unpaid'
expected: "unpaid"
got: "active"
Stripe's recommended procedure is:
Setup the customer with the card 4000000000000341 ("Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.")
Give the customer a subscription but with a trial date ending today/tomorrow.
Wait
Step three is annoying, but it'll get the job done.
Taking @VoteyDisciple's suggestion, I've worked through something to have this reasonably automated in RSpec ('reasonably', given the circumstances). I'm using VCR to capture the API calls (against the test Stripe environment), which is essential, as it means the sleep
call only happens when recording the test the first time.
Using comments to indicate behaviour done via Stripe's API, as my implementation is rather caught up in my project, and that's not so useful.
VCR.use_cassette('retry') do |cassette|
# Clear out existing Stripe data: customers, coupons, plans.
# This is so the test is reliably repeatable.
Stripe::Customer.all.each &:delete
Stripe::Coupon.all.each &:delete
Stripe::Plan.all.each &:delete
# Create a plan, in my case it has the id 'test'.
Stripe::Plan.create(
id: 'test',
amount: 100_00,
currency: 'AUD',
interval: 'month',
interval_count: 1,
name: 'RSpec Test'
)
# Create a customer
customer = Stripe::Customer.create email: '[email protected]'
token = card_token cassette, '4000000000000341'
# Add the card 4000000000000341 to the customer
customer.sources.create token: 'TOKEN for 0341'
# Create a subscription with a trial ending in two seconds.
subscription = customer.subscriptions.create(
plan: 'test',
trial_end: 2.seconds.from_now.to_i
)
# Wait for Stripe to create a proper invoice. I wish this
# was unavoidable, but I don't think it is.
sleep 180 if cassette.recording?
# Grab the invoice that actually has a dollar value.
# There's an invoice for the trial, and we don't care about that.
invoice = customer.invoices.detect { |invoice| invoice.total > 0 }
# Force Stripe to attempt payment for the first time (instead
# of waiting for hours).
begin
invoice.pay
rescue Stripe::CardError
# Expecting this to fail.
end
invoice.refresh
expect(invoice.paid).to eq(false)
expect(invoice.attempted).to eq(true)
# Add a new (valid) card to the customer.
token = card_token cassette, '4242424242424242'
card = customer.sources.create token: token
# and set it as the default
customer.default_source = card.id
customer.save
# Run the code in your app that retries the payment, which
# essentially invokes invoice.pay again.
# THIS IS FOR YOU TO IMPLEMENT
# And now we can check that the invoice wass successfully paid
invoice.refresh
expect(invoice.paid).to eq(true)
end
Getting card tokens in an automated fashion is a whole new area of complexity. What I've got may not work for others, but here's the ruby method, calling out to phantomjs (you'll need to send the VCR cassette object through):
def card_token(cassette, card = '4242424242424242')
return 'tok_my_default_test_token' unless cassette.recording?
token = `phantomjs --ignore-ssl-errors=true --ssl-protocol=any ./spec/fixtures/stripe_tokens.js #{ENV['STRIPE_PUBLISH_KEY']} #{card}`.strip
raise "Unexpected token: #{token}" unless token[/^tok_/]
token
end
And the javascript file that phantomjs is running (stripe_tokens.js) contains:
var page = require('webpage').create(),
system = require('system');
var key = system.args[1],
card = system.args[2];
page.onCallback = function(data) {
console.log(data);
phantom.exit();
};
page.open('spec/fixtures/stripe_tokens.html', function(status) {
if (status == 'success') {
page.evaluate(function(key, card) {
Stripe.setPublishableKey(key);
Stripe.card.createToken(
{number: card, cvc: "123", exp_month: "12", exp_year: "2019"},
function(status, response) { window.callPhantom(response.id) }
);
}, key, card);
}
});
Finally, the HTML file involved (stripe_tokens.html) is pretty simple:
<html>
<head>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
</head>
<body></body>
</html>
Put all of that together, and, well, it might work! It does for our app :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With