We are starting to develop a new website using Rails 3, RSpec 2, and OmniAuth 2. We wanted to follow TDD using RSpec to write the authentication, but we actually don't know where to start. We don't want to test the gem, as it had already been tested, but we want to test the flow of the application and it has routed correctly according to the outcome of the callback.
The best so far is we categorized the problem in two stages:
1- Before the callback: Faking the services like facebook and twitter to return the calls
2- After the callback: getting the results and creating the user and related service
Please guide us and shed us some light :)
Did you checkout the Wiki?
** Edit **
Perhaps something like (untested):
before do
OmniAuth.config.mock_auth[:twitter] = {
'uid' => '123545'
}
end
it "sets a session variable to the OmniAuth auth hash" do
controller.session[:auth_hash]['uid'].should == '123545'
end
I have experiencing problem for writhing RSpec for OmniauthCallbacksController, do some research on this and it working for me. I am here adding my codes, if anyone found necessary. The tests are for happy path :)
require 'spec_helper'
describe OmniauthCallbacksController do
describe "#linkedin" do
let(:current_user) { Fabricate(:user) }
before(:each) do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:linkedin] = OmniAuth::AuthHash.new({provider: :linkedin, uid: '12345', credentials: {token: 'linkedin-token', secret: 'linkedin-secret'}})
request.env["devise.mapping"] = Devise.mappings[:user]
#controller.stub!(:env).and_return({"omniauth.auth" => OmniAuth.config.mock_auth[:linkedin]})
User.stub(:from_auth).and_return(current_user)
end
describe "#linkedin" do
context "with a new linkedin user" do
before { get :linkedin }
it "should authenticate user" do
warden.authenticated?(:user).should == true
end
it "should set current_user" do
subject.current_user.should_not be_nil
end
it "should redirect to root_path" do
response.should redirect_to(root_path)
end
end
end
end
end
Related
I've spent far too long messing with this before asking for help. I can't seem to get RSpec and Sorcery to play together nicely. I've read through the docs on Integration testing with Sorcery and can post the login action properly, but my tests still doesn't think the user is logged in.
# spec/controllers/user_controller_spec
describe 'user access' do
let (:user) { create(:user) }
before :each do
login_user(user[:email], user[:password])
end
it "should log in the user" do
controller.should be_logged_in
end
end
And my login_user method
# spec/support/sorcery_login
module Sorcery
module TestHelpers
module Rails
def login_user email, password
page.driver.post(sessions_path, { email: email , password: password, remember_me: false })
end
end
end
end
The sessions controller handles the pages properly when I use them on the generated pages just fine. I tried outputting the results of the login_user method and it appears to properly post the data. How do I persist this logged in user through the tests? Does a before :each block not work for this? I'm just not sure where it could be running wrong and I'm pretty new to testing/RSpec so I may be missing something obvious. I'd appreciate any help.
Here's the output of the failed tests:
1) UsersController user access should log in the user
Failure/Error: controller.should be_logged_in
expected logged_in? to return true, got false
I just went through this yesterday. Here's what I did, if it helps.
Sorcery provides a test helper login_user that relies on a #controller object being available. This works great in controller specs, but doesn't work in integration tests. So the workaround in integration tests is to write another method (like the one you have above) to simulate actually logging in via an HTTP request (essentially simulating submitting a form).
So my first thought is that you should try renaming your method to login_user_post or something else that doesn't collide with the built-in test helper.
Another potential gotcha is that it looks to me like the Sorcery helper assumes that your user's password is 'secret'.
Here's a link to the built-in helper so you can see what I'm talking about:
https://github.com/NoamB/sorcery/blob/master/lib/sorcery/test_helpers/rails.rb
Good luck - I really like this gem except for this part. It is really only fully explained by patching together SO posts. Here's the code I use:
Integration Helper
module Sorcery
module TestHelpers
module Rails
def login_user_post(user, password)
page.driver.post(sessions_url, { username: user, password: password})
end
def logout_user_get
page.driver.get(logout_url)
end
end
end
end
Integration Spec (where user needs to be logged in to do stuff)
before(:each) do
#user = create(:user)
login_user_post(#user.username, 'secret')
end
Controller Spec (where the regular login_user helper works fine)
before(:each) do
#user = create(:user)
login_user
end
Note that login_user doesn't need any arguments if you have an #user object with the password 'secret'.
Did you try adding to spec/spec_helpers.
RSpec.configure do |config|
# ...
config.include Sorcery::TestHelpers::Rails::Controller
end
Nota that you need to include Sorcery::TestHelpers::Rails::Controller, not just Sorcery::TestHelpers::Rails.
Then you will be able to login_user from any controller specs like:
describe CategoriesController do
before do
#user = FactoryGirl::create(:user)
end
describe "GET 'index'" do
it "returns http success" do
login_user
get 'index'
expect(response).to be_success
end
end
end
The way you pass a password is probably wrong. It may be encrypted at this point. In provided example I will try to do this at first:
describe 'user access' do
let (:user) { create(:user, password: 'secret') }
before :each do
login_user(user[:email], 'secret')
end
it "should log in the user" do
controller.should be_logged_in
end
end
This seems to be very poorly documented. The above solutions did not work for me. Here's how I got it to work:
Check your sessions_url. Make sure it is correct. Also, check what params are necessary to log in. It may be email, username, etc.
module Sorcery
module TestHelpers
module Rails
def login_user_post(email, password)
page.driver.post(sessions_url, { email:email, password: password })
end
end
end
end
RSpec config:
config.include Sorcery::TestHelpers::Rails
Spec helper:
def app
Capybara.app
end
spec/controllers/protected_resource_spec.rb:
describe UsersController do
before do
# Create user
# Login
response = login_user_post( user.email, :admin_password )
expect( response.headers[ 'location' ]).to eq 'http://test.host/'
# I test for login success here. Failure redirects to /sign_in.
#cookie = response.headers[ 'Set-Cookie' ]
end
specify 'Gets protected resource' do
get protected_resource, {}, { cookie:#cookie }
expect( last_response.status ).to eq 200
end
Following the Railscast on Devise and OmniAuth I have implemented an OmniauthCallbacksController < Devise::OmniauthCallbacksController which contains a single method to handle an OmniAuth callback:
def all
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
alias_method :facebook, :all
routes.rb:
devise_for :users, controllers: {omniauth_callbacks: "omniauth_callbacks", :sessions => "sessions" }
I would like to customise this, so I'm trying to test it using RSpec. The question is how do I test this method and the redirects?
If in the spec I put user_omniauth_callback_path(:facebook) it doesn't complain about the route not existing, but doesn't seem to actually call the method.
According to this answer "controller tests use the four HTTP verbs (GET, POST, PUT, DELETE), regardless of whether your controller is RESTful." I tried get user_... etc. but here it does complain that the route doesn't exist. And indeed if I do rake routes it shows there is no HTTP verb for this route:
user_omniauth_callback [BLANK] /users/auth/:action/callback(.:format) omniauth_callbacks#(?-mix:facebook)
Can you see what I'm missing?
EDIT
So following this question one way of calling the method is:
controller.send(:all)
However I then run into the same error that the questioner ran into:
ActionController::RackDelegation#content_type delegated to #_response.content_type, but #_response is nil
You will need to do three things to get this accomplished.
enter OmniAuth test environment
create an OmniAuth test mock
stub out your from_omniauth method to return a user
Here is a possible solution, entered in the spec itself
(spec/feature/login_spec.rb for example) . . .
let(:current_user) { FactoryGirl.create(:user) }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new({
provider: :facebook,
uid:'12345',
info: {
name: "Joe"
}
})
User.stub(:from_omniauth).and_return(current_user)
end
I adapted this from a google authentication, so facebook may require more fields, but those are the only ones required by omniauth docs. You should be able to find the correct fields by looking at your database schema and finding fields that match the documentation.
In my case, the minimum was enough to pass the request phase and move onto the stubbed out method returning my user.
This example also uses FactoryGirl.
It may not be perfect, but I hope it helps. Good luck!
-Dan
If you hit this and you are running rspec 3.4 this example should work for you:
describe Users::OmniauthCallbacksController, type: :controller do
let(:current_user) { FactoryGirl.create(:user) }
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:your_oauth_provider_here] = OmniAuth::AuthHash.new(
provider: :your_oauth_provider_here,
uid: rand(5**10),
credentials: { token: ENV['CLIENT_ID'], secret: ENV['CLIENT_SECRET'] }
)
request.env['devise.mapping'] = Devise.mappings[:user]
allow(#controller).to receive(:env) { { 'omniauth.auth' => OmniAuth.config.mock_auth[:your_oauth_provider_here] } }
allow(User).to receive(:from_omniauth) { current_user }
end
describe '#your_oauth_provider_here' do
context 'new user' do
before { get :your_oauth_provider_here }
it 'authenticate user' do
expect(warden.authenticated?(:user)).to be_truthy
end
it 'set current_user' do
expect(current_user).not_to be_nil
end
it 'redirect to root_path' do
expect(response).to redirect_to(root_path)
end
end
end
end
I am experiencing problem for writhing RSpec for OmniauthCallbacksController, do some research on this and it working for me. Here is my codes, if anyone found necessary. Tests are for happy path and it should work for news version of RSpec eg. 3.x
require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do
describe "#linkedin" do
let(:current_user) { Fabricate(:user) }
before(:each) do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:linkedin] = OmniAuth::AuthHash.new({provider: :linkedin, uid: '12345', credentials: {token: 'linkedin-token', secret: 'linkedin-secret'}})
request.env["devise.mapping"] = Devise.mappings[:user]
#controller.stub!(:env).and_return({"omniauth.auth" => OmniAuth.config.mock_auth[:linkedin]})
User.stub(:from_auth).and_return(current_user)
end
describe "#linkedin" do
context "with a new linkedin user" do
before { get :linkedin }
it "authenticate user" do
expect(warden.authenticated?(:user)).to be_truthy
end
it "set current_user" do
expect(subject.current_user).not_to be_nil
end
it "redirect to root_path" do
expect(response).to redirect_to(root_path)
end
end
end
end
end
I'm using rack/test and rspec on Rails 3 to authenticate a users api key through devise. Any request I make returns a status of 302 and a response of: "You are being redirected.". Can't seem to figure out how to authenticate.
require 'spec_helper'
describe Api::V1::CollectController do
def app
Rails.application
end
context 'user' do
subject do
FactoryGirl.create :user
end
it 'allow valid api credentials' do
post '/api/collect', {}, { 'Authentication' => subject.authentication_token }
p last_response.body
end
end
end
I've found a working solution. But I'm not satisfied with why the original approach didn't work. I eliminated rack/test and changed the way I interface with the controller. The following worked. I'm mostly just curious now...
require 'spec_helper'
describe Api::V1::CollectController do
context 'user' do
subject do
FactoryGirl.create :user
end
it 'allow valid api credentials' do
post :create, {}, { 'Authentication' => subject.authentication_token }
p response.body
end
end
end
I have been playing with Rails for a couple of years now and have produced a couple of passable apps that are in production. I've always avoided doing any testing though and I have decided to rectify that. I'm trying to write some tests for an app that I wrote for work that is already up and running but undergoing constant revision. I'm concerned that any changes will break things so I want to get some tests up and running. I've read the RSpec book, watched a few screencasts but am struggling to get started (it strikes me as the sort of thing you only understand once you've actually done it).
I'm trying to write what should be a simple test of my ReportsController. The problem with my app is that pretty much the entire thing sits behind an authentication layer. Nothing works if you're not logged in so I have to simulate a login before I can even send forth a simple get request (although I guess I should write some tests to make sure that nothing works without a login - I'll get to that later).
I've set up a testing environment with RSpec, Capybara, FactoryGirl and Guard (wasn't sure which tools to use so used Railscasts' suggestions). The way I've gone about writing my test so far is to create a user in FactoryGirl like so;
FactoryGirl.define do
sequence(:email) {|n| "user#{n}#example.com"}
sequence(:login) {|n| "user#{n}"}
factory :user do
email {FactoryGirl.generate :email}
login {FactoryGirl.generate :login}
password "abc"
admin false
first_name "Bob"
last_name "Bobson"
end
end
and then write my test like so;
require 'spec_helper'
describe ReportsController do
describe "GET 'index'" do
it "should be successful" do
user = Factory(:user)
visit login_path
fill_in "login", :with => user.login
fill_in "password", :with => user.password
click_button "Log in"
get 'index'
response.should be_success
end
end
end
This fails like so;
1) ReportsController GET 'index' should be successful
Failure/Error: response.should be_success
expected success? to return true, got false
# ./spec/controllers/reports_controller_spec.rb:13:in `block (3 levels) in <top (required)>'
Interestingly if I change my test to response.should be_redirect, the test passes which suggests to me that everything is working up until that point but the login is not being recognised.
So my question is what do I have to do to make this login work. Do I need to create a user in the database that matches the FactoryGirl credentials? If so, what is the point of FactoryGirl here (and should I even be using it)? How do I go about creating this fake user in the testing environment? My authentication system is a very simple self-made one (based on Railscasts episode 250). This logging in behaviour will presumably have to replicated for almost all of my tests so how do I go about doing it once in my code and having it apply everywhere?
I realise this is a big question so I thank you for having a look.
The answer depends on your authentication implementation. Normally, when a user logs in, you'll set a session variable to remember that user, something like session[:user_id]. Your controllers will check for a login in a before_filter and redirect if no such session variable exists. I assume you're already doing something like this.
To get this working in your tests, you have to manually insert the user information into the session. Here's part of what we use at work:
# spec/support/spec_test_helper.rb
module SpecTestHelper
def login_admin
login(:admin)
end
def login(user)
user = User.where(:login => user.to_s).first if user.is_a?(Symbol)
request.session[:user] = user.id
end
def current_user
User.find(request.session[:user])
end
end
# spec/spec_helper.rb
RSpec.configure do |config|
config.include SpecTestHelper, :type => :controller
end
Now in any of our controller examples, we can call login(some_user) to simulate logging in as that user.
I should also mention that it looks like you're doing integration testing in this controller test. As a rule, your controller tests should only be simulating requests to individual controller actions, like:
it 'should be successful' do
get :index
response.should be_success
end
This specifically tests a single controller action, which is what you want in a set of controller tests. Then you can use Capybara/Cucumber for end-to-end integration testing of forms, views, and controllers.
Add helper file in spec/support/controller_helpers.rb and copy content below
module ControllerHelpers
def sign_in(user)
if user.nil?
allow(request.env['warden']).to receive(:authenticate!).and_throw(:warden, {:scope => :user})
allow(controller).to receive(:current_user).and_return(nil)
else
allow(request.env['warden']).to receive(:authenticate!).and_return(user)
allow(controller).to receive(:current_user).and_return(user)
end
end
end
Now add following lines in spec/rails_helper.rb or spec/spec_helper.rb
file
require 'support/controller_helpers'
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
config.include ControllerHelpers, :type => :controller
end
Now in your controller spec file.
describe "GET #index" do
before :each do
#user=create(:user)
sign_in #user
end
...
end
Devise Official Link
The easiest way to login with a user on feature tests is to use the Warden's helper #login_as
login_as some_user
As I couldn't make #Brandan's answer work, but based on it and on this post, I've came to this solution:
# spec/support/rails_helper.rb
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } # Add this at top of file
...
include ControllerMacros # Add at bottom of file
And
# spec/support/controller_macros.rb
module ControllerMacros
def login_as_admin
admin = FactoryGirl.create(:user_admin)
login_as(admin)
end
def login_as(user)
request.session[:user_id] = user.id
end
end
Then on your tests you can use:
it "works" do
login_as(FactoryGirl.create(:user))
expect(request.session[:user_id]).not_to be_nil
end
For those who don't use Devise:
spec/rails_helper.rb:
require_relative "support/login_helpers"
RSpec.configure do |config|
config.include LoginHelpers
end
spec/support/login_helpers.rb:
module LoginHelpers
def login_as(user)
post "/session", params: { session: { email: user.email, password: "password" } }
end
end
and in the specs:
login_as(user)
I'm writing some RSpec tests for my Rails 3 application and trying to switch from Webrat to Capybara. So far so good but the application uses HTTP basic auth to authorize my admin user, any idea how I can test that with Capybara?
Here is my current Webrat step:
it 'should authenticate for admin' do
basic_auth('user', 'secret')
visit '/admin'
response.status.should eql 200
response.status.should_not eql 401
end
How do I do this with Capybara? Thanks!
I got it to work using page.driver.basic_authorize(name, password) instead
Update:
At the moment, after a Capybara upgrade, I'm using this pile of workarounds:
if page.driver.respond_to?(:basic_auth)
page.driver.basic_auth(name, password)
elsif page.driver.respond_to?(:basic_authorize)
page.driver.basic_authorize(name, password)
elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize)
page.driver.browser.basic_authorize(name, password)
else
raise "I don't know how to log in!"
end
The default Capybara driver, rack-test, has a basic_authorize method (with alias authorize) for Basic HTTP Auth, and digest_authorize for Digest HTTP Auth, here you can find them: https://github.com/brynary/rack-test/blob/master/lib/rack/test.rb
So you can do:
page.driver.browser.authorize 'login', 'password'
Or you can write a simple helper for Basic HTTP Auth:
def basic_auth(user, password)
encoded_login = ["#{user}:#{password}"].pack("m*")
page.driver.header 'Authorization', "Basic #{encoded_login}"
end
None of the page.driver.* solutions worked for me. I'm using Poltergeist, not Selenium, so that might have something to do with it. Here's what did work:
RSpec.shared_context "When authenticated" do
before do
username = 'yourusername'
password = 'yourpassword'
visit "http://#{username}:#{password}##{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}/"
end
end
Then, in your spec:
feature "Your feature", js: true do
include_context "When authenticated"
# Your test code here...
end
This has changed in recent versions of cucumber-rails (I am using 1.0.2).
cucumber-rails uses the Rack/Test driver by default, so if you have not changed that, the following instructions will work.
Create features/step_definitions/authorize.rb:
Given /^I am logged in as "([^\"]*)" with "([^\"]*)"$/ do |username, password|
authorize username, password
end
Now you can use this in your features:
Given I am logged in as "admin" with "password"
I had to do this horrible hack to get it work worth headless and with javascript
Given /^I am logged in$/ do
if page.driver.respond_to?(:basic_authorize)
page.driver.basic_authorize('admin', 'password')
else
# FIXME for this to work you need to add pref("network.http.phishy-userpass-length", 255); to /Applications/Firefox.app/Contents/MacOS/defaults/pref/firefox.js
page.driver.visit('/')
page.driver.visit("http://admin:password##{page.driver.current_url.gsub(/^http\:\/\//, '')}")
end
end
Man, none of these solutions worked for me.
Pistos' solution came close and worked for feature specs with js: true but failed when headless.
This below solution works for me for both headless and js: true specs.
spec/support/when_authenticated.rb
RSpec.shared_context 'When authenticated' do
background do
authenticate
end
def authenticate
if page.driver.browser.respond_to?(:authorize)
# When headless
page.driver.browser.authorize(username, password)
else
# When javascript test
visit "http://#{username}:#{password}##{host}:#{port}/"
end
end
def username
# Your value here. Replace with string or config location
Rails.application.secrets.http_auth_username
end
def password
# Your value here. Replace with string or config location
Rails.application.secrets.http_auth_password
end
def host
Capybara.current_session.server.host
end
def port
Capybara.current_session.server.port
end
end
Then, in your spec:
feature 'User does something' do
include_context 'When authenticated'
# test examples
end