We've been working with Pressable to add features to their application and to make their test suite more robust.
Most of the specs that we added or changed included integrating WebMock. This works well in the majority of the of the specs but there was one test that was continually failing for us in spite of us mocking it.
The test - an integration/acceptance test - was ensuring that the sign up process worked with Chargify's Direct process.
This process is referred to as a Transparent Redirect, where your application posts directly to a the payment processor and then the payment processor redirects back to your application to verify cryptographically signed data.
This has several benefits for the application stakeholders. First, it offloads PCI Compliance to the payment processor because the application isn't storing credit card data. Second, the user experience is seamless because the user isn't shunted to another unbranded page to verify data.
Clearly the benefits of using this solution are evident but this presents a problem for integration tests for a number of reasons:
- The rack-test driver that's used by default in Capybara cannot post to outside URLs.
- Enabling the test to use a headless browser like Capybara-Webkit means that we can no longer use WebMock to intercept HTTP requests because that browser will start in a different thread.
The approach that I used was to start up a Rack server to mock/fake the responses that would come from Chargify's V2 API.
I first created a simple Rack server that I placed in the spec/support directory.
require 'rack'
module MockChargifyServer
class Server
def initialize(capybara_host, capybara_port)
@app = Rack::Builder.new do
use Rack::CommonLogger
use Rack::ShowExceptions
end
end
def call(env)
@app.call(env)
end
end
end
I'm passing in the Capybara host and port here so that I can send a redirect back to my Rails application.
Looking at the Chargify API documentation, it looks like we're going to need to fake a signups
path and a calls
path. So I created some simple routes in my Rack server:
require 'rack'
module MockChargifyServer
class Server
def initialize(capybara_host, capybara_port)
@app = Rack::Builder.new do
use Rack::CommonLogger
use Rack::ShowExceptions
map '/signups' do
end
map '/calls' do
end
end
end
def call(env)
@app.call(env)
end
end
end
The routes don't do anything yet, so we'll need to wire them up. Let's tackle the signups route first.
This is what our Rails application should be posting to and it should be redirecting back to our app with some query parameters.
I created a class to handle this:
class SignupResponse
def initialize(capybara_host, capybara_port)
@capybara_host = capybara_host
@capybara_port = capybara_port
end
def call(env)
response = Rack::Response.new
parameters = ParameterBuilder.new.create
response.redirect("http://#{@capybara_host}:#{@capybara_port.to_s}/subscription/verify?#{parameters}")
response.finish
end
end
As you can see in the call
method we are creating a rack response and setting it as a redirect. The parameters that we send over are built in a ParameterBuilder
class:
class ParameterBuilder
def initialize
@parameter_list = {
api_id: ENV['CHARGIFY_DIRECT_API_ID'],
timestamp: Time.zone.now,
nonce: SecureRandom.hex(20),
status_code: 200,
result_code: 2000,
call_id: 'chargify_id'
}
@parameters = []
end
def create
@parameter_list.each do |k,v|
@parameters << Parameter.new(k,v)
end
@parameters << Parameter.new('signature', sign_parameters)
@parameters.map(&:to_s).join("&")
end
private
def sign_parameters
key = ENV['CHARGIFY_DIRECT_API_SECRET']
data = @parameters.map(&:value).join('')
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), key, data)
end
end
class Parameter
attr_reader :value
def initialize(name, value)
@name = name
@value = value
end
def to_s
"#{@name}=#{@value}"
end
end
The code here is pretty straightforward but there are a few things of note. The parameter list has to be in this order otherwise the signature won't match (this is enforced by the gem that we are using). Additionally, we are creating a signature
that is a list of all the parameters signed with SHA1.
When our rack app redirects back to our Rails app, we have to verify the parameters to ensure that they haven't been tampered with during transit. Again, I create another class to handle this for me:
class CallResponse
def call(env)
json = File.open(File.dirname(__FILE__) + '/../fixtures/chargify_v2_subscription_call_response.json').read
response = Rack::Response.new json, 200, {"Content-Type"=>"application/json"}
response.finish
end
end
Here we are using a fixture file to return specific account data and returning it back as json.
With our classes built we can plug them into our rack app:
require 'rack'
module MockChargifyServer
class Server
def initialize(capybara_host, capybara_port)
@app = Rack::Builder.new do
use Rack::CommonLogger
use Rack::ShowExceptions
map '/signups' do
run SignupResponse.new(capybara_host, capybara_post)
end
map '/calls' do
run CallResponse.new
end
end
end
def call(env)
@app.call(env)
end
end
end
One thing to note, when our application makes a request to verify the parameters, it will make a request to calls/:call_id
. Our route /calls
catches that request as well, we just don't really care what the call id is.
So now that we have a Rack app that can stand in for Chargify we have to plug this in to our Rails app and our spec.
When our view page loads, we have a before filter that gets triggered to setup a Chargify client object. We're using the chargify2 gem for this.
We also setup a helper method (simply named chargify_client
) in our controller so that we can override that later.
In our spec we find out what our Capybara server's host and port are and then startup a new rack server in a different thread:
context 'Subscription Creation' do
before do
host, port = [Capybara.current_session.server.host, Capybara.current_session.server.port]
@server_thread = Thread.new do
Rack::Handler::Thin.run MockChargifyServer::Server.new(host, port), Port: 9292
end
SubscriptionsController.any_instance.stub(:chargify_client).and_return(stub_chargify_client)
signup_user
end
after do
@server_thread.exit
end
it 'does our expectation here', js: true
Above you can see that we are starting up Thin to run our Rack application on port 9292. Additionally, we're also stubbing out the charfigy_client
and replacing it with our stub:
def stub_chargify_client
Chargify2::Client.new({
:api_id => ENV['CHARGIFY_DIRECT_API_ID'],
:api_password => ENV['CHARGIFY_DIRECT_API_PASSWORD'],
:api_secret => ENV['CHARGIFY_DIRECT_API_SECRET'],
:base_uri => 'http://localhost:9292'
})
end
Now when our test runs, it happily thinks that Chargify is at http://localhost:9292
and will send requests to our Rack application.
Here is the full code for the MockChargifyServer.
Final Thoughts
Spinning up a Rack server to intercept calls to Chargify was the solution that worked for us. If you've had to deal with testing transparent redirects in your app, what what the approach you've used? We'd love to hear about it.
P.S. Get in touch if you need any assistance integrating your Ruby on Rails application with Chargify.