Adding a social login to your Rails app is a cinch with OmniAuth, and with the rise of frameworks like Ember and Angular, that Rails app is more and more likely to be an API. Previous apps might have had a sophisticated suite of acceptance tests that drive a browser, but now you need to simulate API calls. RSpec supports this kind of interaction through request specs, but there is no browser, so interactive logins are out. Outlined below are two ways around this with OmniAuth—and why I prefer one over the other.
The Developer Strategy
OmniAuth calls its different authentication methods “strategies,” and out of the box it includes one called “developer,” which, when accessed through a browser, presents the user with a form asking for name and email. It has no external dependencies and can be accessed through a browser to speed up feedback while developing your app. You could use this strategy to authenticate your acceptance test sessions, which looks something like this:
def login
auth_params = { name: 'Test', email: 'test@example.com' }
post '/auth/developer/callback', auth_params
end
It posts that name and email address to the authentication callback URI. It works, but there are two issues that I see. First, you probably don’t need a POST route for your callback if you’re using some kind of social login (Twitter, Facebook, GitHub, etc), so this route is exclusively for testing. Second, the developer strategy provided by OmniAuth isn’t what you’d use in production, so it isn’t a true end-to-end test. It would be nice if we could use the code we wrote to handle production logins for our test. We can do that with OmniAuth’s mocks, though.
Mock Authentication
Here’s another example using Mock Authentication with Twitter as our auth source:
OmniAuth.config.test_mode = true
def login
OmniAuth.config.add_mock(:twitter, { uid: '12345' })
get '/auth/twitter'
request.env['omniauth.env'] = OmniAuth.config.mock_auth[:twitter]
get '/auth/twitter/callback'
end
When we put OmniAuth into test mode on the first line, it will use an internal hash for its authentication data rather than calling out to the integrated service. You can then feed OmniAuth data describing all your authentication scenarios. If you plan to support multiple authentication sources, you can parameterize this method to support that as well:
OmniAuth.config.test_mode = true
def login(mock_login)
strategy = mock_login[:strategy]
OmniAuth.config.add_mock(strategy.to_sym, mock_login[:data])
get "/auth/#{strategy.to_s}"
request.env['omniauth.env'] = OmniAuth.config.mock_auth[strategy.to_sym]
get "/auth/#{strategy.to_s}/callback"
end
Example...
login_data = {
strategy: 'twitter',
data: {
uid: '12345'
}
}
login(login_data)
Using this to iterate through each authentication strategy you support makes it easy to add or change scenarios just by creating new data. You can add tests for missing data, changes to data structure, or recently discovered failure modes without redefining your tests.
The Robustness Principle
I want to stress that I’m not advocating for testing the behavior of third-party APIs. Rather, I’m advocating for the robustness principle when relying on them. Be liberal in what you accept, conservative in what you return. You must be able to respond to how they change, and having good tests means higher confidence that your application stays secure.