giant robots smashing into other giant robots

We are thoughtbot. We make web & mobile apps.

Tagged:

Comments (View)

Writing an API client with test, staging, and production in mind

I was recently tasked with this story:

Background:
  Given I go to the sign up page
  And I fill in "Email" with "new@example.com"
  And I fill in "Password" with "password"

Scenario: Visitor signs up and does not want email
  When I press "Sign up"
  Then email service is not notified "new@example.com" signed up

Scenario: Visitor signs up and wants email
  When I check "Please email me"
  And I press "Sign up"
  Then email service is notified "new@example.com" signed up

The email service in question was internal, accessible via an HTTP API, and did not have a Ruby client library.

So, I had to write and test my own. The usual approach would involve using a tool like Sham Rack or Artifice to stub and record the HTTP requests. I decided to try something different and see how it felt.

First, I needed an interface that I could test:

Then /^email service is notified "([^"]*)" signed up$/ do |email|
  EmailService.notifications.should include(email)
end

Then /^email service is not notified "([^"]*)" signed up$/ do |email|
  EmailService.notifications.should_not include(email)
end

I’m re-using a pattern (“store data in a simple array for easy state-based testing”) we’ve used before for Javascript integration testing Mixpanel and the AjaxRecorder (which was roundly boo’ed as a bad idea).

Cool, I have the start to an EmailService interface. I thought I wanted it invoked as part of an after_save callback on the User model, so here’s that spec:

describe User, "who opts into email" do
  subject { Factory.build(:user, :email_opt_in => true) }

  before do
    EmailService.stubs(:notify)
    subject.save
  end

  it "notifies EmailService" do
    EmailService.should have_received(:notify).with(subject.email)
  end
end

describe User, "who does not opt into email" do
  subject { Factory.build(:user, :email_opt_in => false) }

  before do
    EmailService.stubs(:notify)
    subject.save
  end

  it "does not notify EmailService" do
    EmailService.should have_received(:notify).never
  end
end

This is the stubbing and spying technique and uses RSpec, mocha, and bourne.

My thought process was that I needed an active verb, notify to invoke when the user is created, but that will store the invocation in a notifications array that the Cucumber step definition needs to check state (I don’t want to stub, spy, or mock in an integration test).

So, making the user spec pass isn’t bad:

require 'email_service/notifier'

class User < ActiveRecord::Base
  after_create do
    EmailService.notify(email) if email_opt_in?
  end
end

Now, the EmailService can be spec’ed. We already had John Nunemaker’s HTTParty as a dependency in the app, and I only had to make one HTTP POST, so I knew I would be re-using HTTParty’s interface.

describe EmailService::Notifier, "#post" do
  subject { EmailService::Notifier }

  let(:email) { "new-signup@example.com" }

  before do
    subject.stubs(:post)
    subject.new(email).post
  end

  it "POSTs to email service with API_KEY and given email" do
    subject.should have_received(:post).with(
      subject::URL, :query => { :api_key => subject::PARAMS, :email => email }
    )
  end
end

What I care about here is that during the one POST the app has to make, that the parameters are correct. This is close to hitting the live service as I’m willing to get without making an HTTP request.

I had an internal debate with myself while writing it over whether this is “stubbing the system under test” (considered bad practice). I decided “no” because despite the subject being stubbed, the system under test is actually the EmailService::Notifier#post method.

The stubbed and spied method is also mixed in from HTTParty, so I feel clean with this approach.

Making it pass:

require "httparty"

module EmailService
  class << self
    attr_accessor :notifications
  end

  def self.notify(email, live = false)
    if Rails.env.production? || live
      Notifier.new(email).post
    else
      self.notifications << email
    end
  end

  class Notifier
    include HTTParty

    URL     = 'http://emailservice.example.com'
    API_KEY = 12345

    def initialize(email)
      @url   = URL
      @email = email
    end

    def post
      self.class.post(@url, :query => { :api_key => API_KEY, :email => @email })
    end
  end
end

Summary

  • I’ve got an environment-specific conditional inside the true public interface for the service, EmailService.notify, which is used by the User model.
  • In my integration test and in the development environment, all that happens is an array is populated, which makes it easy to confirm that it was notified correctly.
  • I’ve got a unit test that makes sure the correct URL and API key are used.
  • I’ve got a way to override EmailService.notify with the live flag so I can invoke it from the Rails console on production or staging when testing or debugging.

This almost takes longer to describe than to code but I’m curious what people think about this style of writing an API client with test, staging, and production environments in mind. How do you do things differently?

Tagged:

Comments (View)

Forbidden kisses & HTTP fluency in Clearance

UPDATE: After about two years of using this approach in Clearance, we removed the 403 Forbidden feature in Clearance. We discovered that setting the 403 status code turned out to be a bad user experience in some browsers such as Chrome on Windows machines. Philosophically, we decided we value user experience over technical purity.

Clearance tries to be fluent in HTTP. That means a few things:

<ul>
<li>Know when to return which <span class="caps">HTTP</span> status codes.</li>
    <li>Know when to raise errors.</li>
</ul>


<h2>401 Unauthorized</h2>


<p>In layman&#8217;s terms:</p>


<blockquote>
    <p>&#8220;Specifically for use when authentication is possible but has failed or not yet been provided.&#8221;</p>
</blockquote>


<p>The response is <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error">401 Unauthorized</a> &#8220;out of the box&#8221; with Clearance when:</p>


<ul>
<li>A user tries to sign in with bad credentials.</li>
    <li>A user without confirmed email tries to sign in.</li>
</ul>


<p>If you protect an action with <code>before_filter :authenticate</code> in your app, Clearance will also return 401 Unauthorized when:</p>


<ul>
<li>A user who is not signed in tries to access that action.</li>
</ul>


<h2>403 Forbidden</h2>


<p>In layman&#8217;s terms:</p>


<blockquote>
    <p>&#8220;The request was a legal request, but the server is refusing to respond to it.<br />

Unlike a 401 Unauthorized response, authenticating will make no difference.”

<p><img src="http://ui.thoughtbot.com/assets/2009-2-22-forbdden_kiss.jpg" style="float:right;" />The response is <a href="http://en.wikipedia.org/wiki/HTTP_403">403 Forbidden</a> &#8220;out of the box&#8221; with Clearance when:</p>


<ul>
<li>A user tries to confirm a user with confirmed email.</li>
    <li>A user tries to confirm a user without a token.</li>
    <li>A user tries to confirm a user without the correct token for an unconfirmed user.</li>
    <li>A user tries to edit a user&#8217;s password without a token.</li>
    <li>A user tries to update a user&#8217;s password without a token.</li>
    <li>A user tries to edit a user&#8217;s password without the correct token for the user.</li>
    <li>A user tries to update a user&#8217;s password without the correct token for the user.</li>
</ul>


<p>These are legal requests by someone or something (maybe a malicious user) requesting actions in forbidden, exceptional ways. They are not available to any user, regardless of their authentication status. The server should refuse to respond to it.</p>


<h2>When to raise errors</h2>


<p>Consider a typical edit, show, or destroy action:</p>


<pre><code>def show

@user = User.find(params[:id]) end

<p>In the development and test environments, this will raise a <code>ActiveRecord::RecordNotFound</code> error if a User does not exist for the given id. In production, this will return 404 Not Found instead of 500 Internal Server Error.</p>


<p>Rails does this by rescuing the <code>ActiveRecord::RecordNotFound</code> error for <strong>public requests</strong> (for example, staging or production environments). Inside the rescue, it returns the logical status code, :not_found. For <a href="http://api.rubyonrails.org/classes/ActionController/Rescue.html#M000397"><strong>local requests</strong></a> (for example, development or test environments), the error is not rescued.</p>


<p>Rails provides similar functionality for other errors:</p>


<pre><code>'ActionController::RoutingError'             =&gt; :not_found,

‘ActionController::UnknownAction’ => :not_found, ‘ActiveRecord::RecordNotFound’ => :not_found, ‘ActiveRecord::StaleObjectError’ => :conflict, ‘ActiveRecord::RecordInvalid’ => :unprocessable_entity, ‘ActiveRecord::RecordNotSaved’ => :unprocessable_entity, ‘ActionController::MethodNotAllowed’ => :method_not_allowed, ‘ActionController::NotImplemented’ => :not_implemented, ‘ActionController::InvalidAuthenticityToken’ => :unprocessable_entity

<p>This maps errors to <span class="caps">HTTP</span> status codes.</p>


<p>Clearance creates a custom error, <code>ActionController::Forbidden</code>, and maps it to <code>:forbidden</code> to match this convention.</p>


<p>So when situations arise when 403 Forbidden is called for, Clearance simply does:</p>


<pre><code>raise ActionController::Forbidden</code></pre>


<p>The effect is exactly like <code>ActiveRecord::RecordNotFound</code>. In development and test environments, the developer has the opportunity to investigate what is going wrong. In staging and production, the app behaves like a good internet citizen by responding with the correct <span class="caps">HTTP</span> status code.</p>


<p><em>Note:</em> One could argue that Rails could provide a mapping like this for all <span class="caps">HTTP</span> status codes, or at least a few more of the most common ones. A patch for another day, perhaps.</p>


<h2>Attribution</h2>


<p>These ideas were lifted from the good work coming out of the <a href="http://merbivore.com">Merb</a> community. The implementation was driven out through conversations with Tim Pope, Joe Ferris, Mike Burns, and Jason Morrison.</p>


<p><a href="http://github.com/thoughtbot/clearance">Clearance is on github</a>.</p>

Tagged:

Comments (View)

A HTTP testing proxy

Hoptoadβ€”which is now liveβ€”is both an application and a Rails plugin that must work together. This integration simply cannot go untested in a test-happy place like thoughtbot.

Battletoads

The plugin, I’m sure you’ve seen, has a private method #send_to_hoptoad that handles the dirty HTTP stuff. It looks like a more complicated version of this:

def send_to_hoptoad(data)
  url = HoptoadNotifier.url
  Net::HTTP.start(url.host, url.port) do |http|
    headers = {
      'Content-type' => 'application/x-yaml',
      'Accept' => 'text/xml, application/xml'
    }
    response = begin
                 http.post(url.path, stringify_keys(data).to_yaml, headers)
              rescue TimeoutError => e
                 nil
               end
    case response
    when Net::HTTPSuccess then
      logger.info "Hoptoad Success"
    else
      logger.error "Hoptoad Failure"
    end
  end
end

Dead frog controlled via a network

The integration test simulates the plugin actually hitting the application. Normally to test #send_to_hoptoad you’d use Mocha to stub out Net::HTTP methods, but stubbing sweeps away too many potential issues here.

What we really want is an integration test that pits the plugin against the real application, without running a server. We want Net::HTTP#post to use ActionController::Integration::Session#post .

The gruesome internals

In the integration test for the application, first require in the needed tricks:

require 'test_helper'
require 'net/http'
require File.dirname(__FILE__) + '/../lib/hoptoad_notifier/lib/hoptoad_notifier'

(we’ve installed a copy of the plugin into test/lib)

Then, open up Net::HTTP and get rid of the bits that connect to the network. This part could be done with Mocha, but we need to open Net::HTTP later so we might as well do it this way:

class Net::HTTP < Net::Protocol
  def connect
  end
end

While you have Net::HTTP open, replace #post with a proxy. The class to proxy to is passed into the test_unit_class module variable.

class Net::HTTP < Net::Protocol
  mattr_accessor :proxy_object

  def post(path, body, headers)
    self.class.proxy_object.post path, body, headers
  end
end

Finally in the test setup block we need to initialize Net::HTTP with the appropriate instance of ActionController::Integration::Session (which is to say, self):

class PostingFromHoptoadNotifierTest < ActionController::IntegrationTest
  context "with a connection from the plugin to the application" do
    setup do
      Net::HTTP::proxy_object = self
    end

    should_eventually "deny access to people who disagree with me" do
    end
  end
end

All #should statements inside the context will proxy themselves through the integration test instead of hitting the network. Bam!

Check out the complete test file.

Look ma, no OSI layer 7!