Wednesday, February 16, 2011

Mocking Out Omniauth for Cucumber Testing

A few months ago some friends and I got together for Cleveland Startup Weekend and started building our "next big thing" DreamKumo

We had a couple challenges. First we knew from the get go that we wanted the site to be 100% facebook integrated. That meant implementing Facebook Connect and the Facebook Graph API. We also are a group of people who throughly believe in ATDD, TDD and agile development principals (and practices) in general. This led us to building up cucumber features for every area of the site. 

We hit a snag though when we tried to cuke our login process. We're asking our users to login through Facebook connect, which means when they click the login button they are redirected to facebook.com, and then sent back to us. Implementing the requirement was pretty simple using the omniauth gem with a little help from the folks at Rails Rumble. Cuking it is another story. Since facebook connect actually redirects the user we can't mock out the request using fakeweb and we don't want to cuke what happens on Facebook.com since 1) we can't control it, 2) it isn't consistent (sometimes you get a screen to accept the permissions that your app is requesting, sometimes you get the facebook login screen and sometimes you get redirected straight through) and 3) we don't want to setup a dummy facebook account for our cukes to use. 

So we needed to prevent the site from redirecting to facebook, and dummy up the site cookie so it looked like we were authenticated. We also needed the failure case to test what would happen if our user was sent to facebook and they decided they didn't want to grant us permission to their account. 

Enter monkey patching. 

Ruby has this wonderfully powerful ability to override the native functionality of a method or class at runtime. It's part of it being a dynamic language and it let's you do some really cool things, like manipulate the functionality of any class, including library classes like those in omniauth, at any time. So we dug through omniauth's code to find the point where it did the redirect to facebook and mocked out a response. We then had it immediatley redirect back to the callback url, or the url in our application that responds when facebook redirects back to us. So the result is we never hit facebook and omniauth sends along what we need to determine if the authentication was successful or not. 

Ok enough talk - here's the code. 

These steps are in a generic_context_steps.rb file in our step_definitions

 

Given /^I have valid facebook credentials$/ do
  module OmniAuth
    module Strategies
      class OAuth2
        def request_phase
          redirect callback_url
        end
        def callback_phase
          @env['rack.auth'] = {"provider"=>"facebook", "uid"=>"123", "user_info"=>{"first_name"=>"John", "last_name"=>"Doe", "nickname"=>"JohnDoe", "email"=>"john.doe@test.com"}, "credentials"=>{"token"=>"abc123"}}
          call_app!
        end
      end
    end
  end
end
Given /^I have invalid facebook credentials$/ do
  module OmniAuth
    module Strategies
      class OAuth2
        def request_phase
          redirect callback_url
        end
        def callback_phase
          fail!(:invalid_credentials, nil)
        end
      end
    end
  end
end
We simply call those in any feature where we want to authenticate the user and it continues on with the test as intended. 
There is a slight ATDD tradeoff. You aren't actually testing everything exactly as your user would see it since you're skipping the Facebook part. But In this case we borrow a bit from our TDD book and justify this by saying we only test our own code, and we didn't write facebook.com.
Hope this helps. 

 

2 comments:

  1. Originally from Michael Bleigh -

    You might want to check out Integration Testing in OmniAuth on the wiki, since it basically does the same thing just a little more "officially". It's available in the latest 0.2.0beta gem.

    ReplyDelete
  2. Thanks Michael, that's really good to see. I've actually been sitting on this post for a while so i don't quite think you guys had that in there yet when we put in this work around, but now that there's an official way we'll upgrade when we upgrade the gem.

    ReplyDelete