85

When writing a request spec, how do you set sessions and/or stub controller methods? I'm trying to stub out authentication in my integration tests - rspec/requests

Here's an example of a test

require File.dirname(__FILE__) + '/../spec_helper'
require File.dirname(__FILE__) + '/authentication_helpers'


describe "Messages" do
  include AuthenticationHelpers

  describe "GET admin/messages" do
    before(:each) do
      @current_user = Factory :super_admin
      login(@current_user)
    end

    it "displays received messages" do
      sender = Factory :jonas
      direct_message = Message.new(:sender_id => sender.id, :subject => "Message system.", :content => "content", :receiver_ids => [@current_user.id])
      direct_message.save
      get admin_messages_path
      response.body.should include(direct_message.subject) 
    end
  end
end

The helper:

module AuthenticationHelpers
  def login(user)
    session[:user_id] = user.id # session is nil
    #controller.stub!(:current_user).and_return(user) # controller is nil
  end
end

And the ApplicationController that handles authentication:

class ApplicationController < ActionController::Base
  protect_from_forgery

  helper_method :current_user
  helper_method :logged_in?

  protected

  def current_user  
    @current_user ||= User.find(session[:user_id]) if session[:user_id]  
  end

  def logged_in?
    !current_user.nil?
  end
end

Why is it not possible to access these resources?

1) Messages GET admin/messages displays received messages
     Failure/Error: login(@current_user)
     NoMethodError:
       undefined method `session' for nil:NilClass
     # ./spec/requests/authentication_helpers.rb:3:in `login'
     # ./spec/requests/message_spec.rb:15:in `block (3 levels) in <top (required)>'
Jonas Bylov
  • 1,394
  • 1
  • 13
  • 26

5 Answers5

103

A request spec is a thin wrapper around ActionDispatch::IntegrationTest, which doesn't work like controller specs (which wrap ActionController::TestCase). Even though there is a session method available, I don't think it is supported (i.e. it's probably there because a module that gets included for other utilities also includes that method).

I'd recommend logging in by posting to whatever action you use to authenticate users. If you make the password 'password' (for example) for all the User factories, then you can do something like this:

def login(user)
  post login_path, :login => user.login, :password => 'password'
end
David Chelimsky
  • 8,572
  • 1
  • 35
  • 30
  • 1
    Thanks David. It works great, but it does seem to be a little overkill making all those requests? – Jonas Bylov Apr 27 '11 at 11:52
  • 19
    If I thought it was overkill, I wouldn't have recommended it :) – David Chelimsky Apr 27 '11 at 12:54
  • 6
    It's also the simplest way to do it reliably. `ActionDispatch::IntegrationTest` is designed to simulate one or more users interacting via browsers, without having to use real browsers. There are potentially more than one user (i.e. session) and more than one controller within a single example, and the session/controller objects are those used in the last request. You don't have access to them before a request. – David Chelimsky Apr 27 '11 at 13:02
  • 17
    I have to use `page.driver.post` with Capybara – Ian Yang Aug 24 '11 at 11:17
  • @IanYang `page.driver.post` may be an antipattern, according to Jonas Nicklas in [Capybara and testing APIs](https://www.varvet.com/blog/capybara-and-testing-apis/) ) – Epigene Dec 04 '17 at 09:28
  • I think, this one is good solution https://makandracards.com/makandra/37161-rspec-devise-how-to-sign-in-users-in-request-specs – Ismail Akbudak Sep 06 '18 at 13:57
62

Note for Devise users...

BTW, @David Chelimsky's answer may need a little tweaking if you're using Devise. What I'm doing in my integration / requests testing (thanks to this StackOverflow post):

# file: spec/requests_helper.rb

# Rails 6
def login(user)
  post user_session_path, params: {
    user: {
      email: user.email, password: user.password
    }
  }
  follow_redirect!
end

# Rails 5 or older
def login(user)
  post_via_redirect user_session_path, 'user[email]' => user.email, 'user[password]' => user.password
end
Joonas
  • 167
  • 1
  • 7
fearless_fool
  • 29,889
  • 20
  • 114
  • 193
  • 2
    when i then use 'login user1' in an rspec model spec, I get undefined local variable or method 'user_session_path' for # – jpw Dec 20 '12 at 19:36
  • 1
    This assumes you have `devise_for :users` in `config/routes.rb` file. If you've specified something different, you'll have to tweak your code accordingly. – fearless_fool Feb 23 '13 at 17:10
  • This worked for me bu I had to modify it slightly. I changed `'user[email]' => user.email` to `'user[username]' => user.username` since my app uses username as a login instead of email. – webdevguy May 24 '16 at 14:25
  • You saved me, thank you! – Daniel Duque Jan 31 '21 at 23:49
3

FWIW, in porting my Test::Unit tests to RSpec, I wanted to be able to login with multiple (devise) sessions in my request specs. It took some digging, but got this to work for me. Using Rails 3.2.13 and RSpec 2.13.0.

# file: spec/support/devise.rb
module RequestHelpers
  def login(user)
    ActionController::IntegrationTest.new(self).open_session do |sess|
      u = users(user)

      sess.post '/users/sign_in', {
        user: {
          email: u.email,
          password: 'password'
        }
      }

      sess.flash[:alert].should be_nil
      sess.flash[:notice].should == 'Signed in successfully.'
      sess.response.code.should == '302'
    end
  end
end

include RequestHelpers

And...

# spec/request/user_flows.rb
require 'spec_helper'

describe 'User flows' do
  fixtures :users

  it 'lets a user do stuff to another user' do
    karl = login :karl
    karl.get '/users'
    karl.response.code.should eq '200'

    karl.xhr :put, "/users/#{users(:bob).id}", id: users(:bob).id,
      "#{users(:bob).id}-is-funny" => 'true'

    karl.response.code.should eq '200'
    User.find(users(:bob).id).should be_funny

    bob = login :bob
    expect { bob.get '/users' }.to_not raise_exception

    bob.response.code.should eq '200'
  end
end

Edit: fixed typo

turboladen
  • 659
  • 1
  • 9
  • 14
-1

You could pretty easily stub the session as well.

controller.session.stub(:[]).with(:user_id).and_return(<whatever user ID>)

All ruby special operators are indeed methods. Calling 1+1 is the same as 1.+(1), which means + is just a method. Similarly, session[:user_id] is the same as calling method [] on session, as session.[](:user_id)

Subhas
  • 13,845
  • 1
  • 27
  • 36
-3

I found this very helpful for Devise : https://github.com/plataformatec/devise/wiki/How-To:-Test-controllers-with-Rails-3-and-4-(and-RSpec)

Shimaa Marzouk
  • 391
  • 3
  • 9