Testing with Rspec, NiftyAuthentication - ruby-on-rails

I'm using Ryan Bates's nifty:authentication, and starting testing with Rspec. Fought with this for weeks, and still don't understand what's happening.
My controller simply calls
before_filter :login_required, :except => [:login]
Which is defined in lib/controller_authentication
def self.included(controller)
controller.send :helper_method, :current_account, :logged_in?, :redirect_to_target_or_default
end
def current_account
#current_account ||= Account.find(session[:account_id]) if session[:account_id]
end
def logged_in?
current_account
end
def login_required
unless logged_in?
store_target_location
redirect_to login_url, :alert => "You must first answer me these riddles three log in or sign up before accessing this page."
end
end
def redirect_to_target_or_default(default, *args)
redirect_to(session[:return_to] || default, *args)
session[:return_to] = nil
end
private
def store_target_location
session[:return_to] = request.url
end
end
The app works as intended, but the testing fails every time. No matter what I try, I get the redirect_to login_url, :alert => "You must first ...log in" page.
Things I've tried:
controller.stub!( :login_required )
ControllerAuthentication.stub!(:current_account).and_return(Account.where(:username => 'ej0c').first)
#ControllerAuthentication.stub!(:logged_in?).and_return(Account.where(:username => 'ej0c').first)
ControllerAuthentication.stub!(:login_required).and_return(true)
MyDigisController.stub!( :login_required ).and_return(true)
Which I think means I'm missing the whole theory of the thing. How can I make my login work?
I tried as Punit suggests below:
[pre]
require 'spec_helper'
describe "View event details" do
it "Should show a table of events" do
#account = Account.where(:username => 'ej0c').first
puts #account.inspect
controller.stub!(:current_account).and_return(#account)
controller.stub!(:logged_in?).and_return(true)
session[:account_id] = #account.id
visit '/my_digis/66/custom_events'
page.should have_content('Events')
end
end
#account.inspect displayed nicely, but I also got
An expectation of :current_account was set on nil. Called from C:/Users/Ed/webapps/whendidji3/spec/con
.rb:8:in `block (2 levels) in <top (required)>'. Use allow_message_expectations_on_nil to disable warn
An expectation of :logged_in? was set on nil. Called from C:/Users/Ed/webapps/whendidji3/spec/controll
:in `block (2 levels) in <top (required)>'. Use allow_message_expectations_on_nil to disable warnings.
Thanks for any detailed explanations, as I've searched high an low to understand what's goin on.

You are using a vanilla spec rather than a controller spec, which means that the variable 'controller' is not being set.
To use a controller spec, you need to pass the controller class name to your describe block rather than a string.
describe MyController do
see http://rspec.info/rails/writing/controllers.html
Once you have that going, you should be able to use your original thought of stubbing login_required

You need to stub the "current_account" method. You could do it like this -
#account = create_account #function to create an account for your specs
controller.stub!(:current_account).and_return(#account)
controller.stub!(:logged_in?).and_return(true)
You should probably create a method out of the above lines to stub authentication wherever required.

OK, the answer was that I needed a request spec.
I'd begun with a request spec, but it wasn't in the requests folder, and the questions I asked got it morphed into a half-request/half-controller spec, none of which works.
The strange thing about rspec is that it will complain if a capybara method is badly formed,...but it won't mention that the thing just plain doesn't work where you put it!
My working spec, located in specs/requests/my_digis_spec.rb is
require 'spec_helper'
describe "MyDigis" do
before :each do
#account = Account.where(:username => 'me').first
visit login_path
fill_in 'Username or Email Address', :with => #account.email
fill_in 'Password', :with => 'password'
click_button('Log in')
end
it "Shows list of digis" do
visit my_digis_path
page.should have_content('Your Custom Event groups')
end
it "Lets you modify didji list" do
visit my_digis_path
click_button('Modify events')
page.should have_content('Events for my_digi')
end
end
As easy as advertised, just took me 5 weeks to get the login part. Thanks.

Related

Devise - possible to use controller test to test user NOT signed in?

I have a controller that depends on the user being authenticated. So it looks like this
class PlansController < ApplicationController
before_action :authenticate_user!
def create
puts "here"
if user_signed_in?
puts "true"
else
puts "false"
end
end
end
My controller tests are working just fine when teh user IS signed in, i.e., when I'm writing something like this:
require 'rails_helper'
require 'devise'
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
end
describe "create action" do
before do
#user = User.create(...)
sign_in :user, #user
end
it "should puts here and then true" do
post :create
# => here
# => true
end
end
But I'd also like to test what happens in the else statement. Not sure how to do this, it fundamentally doesn't even put the here. Is it possible to test this? Or should I just leave and let Devise be?
describe "create action" do
before do
#user = User.create(...)
# do not sign in user (note I have also tried to do a sign_in and then sign_out, same result)
end
it "should puts here and then true" do
post :create
# => nothing is put, not even the first here!
# => no real "error" either, just a test failure
end
end
The before_action :authenticate_user! will immediately redirect you to the default sign-in page, if the user isn't signed in, skipping the create action altogether.
The if user_signed_in? statement is moot in this case, because the user will always be signed in when that code has the chance to run.
If plans can be created with or without an authenticated user, remove the before_action line.

Rails/Rspec - testing a redirect in the controller

So I am currently writing a test for a controller in an existing controller that just didn't have one before. What I want to test is a redirect that happens when someone is not allowed to edit something vs someone that is allowed to edit it.
the controller action being edit
def edit
if !#scorecard.reviewed? || admin?
#company = #scorecard.company
#custom_css_include = "confirmation_page"
else
redirect_to :back
end
end
So if a scorecard has been reviewed then only an admin can edit that score.
The routes for that controller..
# scorecards
resources :scorecards do
member do
get 'report'
end
resources :inaccuracy_reports, :only => [:new, :create]
end
and finally the test
require 'spec_helper'
describe ScorecardsController do
describe "GET edit" do
before(:each) do
#agency = Factory(:agency)
#va = Factory(:va_user, :agency => #agency)
#admin = Factory(:admin)
#company = Factory(:company)
#scorecard = Factory(:scorecard, :level => 1, :company => #company, :agency => #agency, :reviewed => true)
request.env["HTTP_REFERER"] = "/scorecard"
end
context "as a admin" do
before(:each) do
controller.stub(:current_user).and_return #admin
end
it "allows you to edit a reviewed scorecard" do
get 'edit', :id => #scorecard.id
response.status.should be(200)
end
end
context "as a va_user" do
before(:each) do
controller.stub(:current_user).and_return #va
end
it "does not allow you to edit a reviewed scorecard" do
get 'edit', :id => #scorecard.id
response.should redirect_to :back
end
end
end
end
so a va when trying to edit a reviewed score will be redirected back, where an admin won't.
but when running this through rspec I get
ScorecardsController
GET edit
as a admin
allows you to edit a reviewed scorecard
as a va_user
does not allow you to edit a reviewed scorecard (FAILED - 1)
Failures:
1) ScorecardsController GET edit as a va_user does not allow you to edit a reviewed scorecard
Failure/Error: response.should redirect_to :back
Expected response to be a redirect to </scorecard> but was a redirect to <http://test.host/>
# ./spec/controllers/scorecards_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.48517 seconds
2 examples, 1 failure
so I don't know if its working or not since I set the request.env["HTTP_REFERER"] = "/scorecard" as the place that should be the :back as it where. or am I missing the idea all together looking at httpstatus there are the 300 responses that I could use but I wouldn't know where to start?
any help would be awesome
EDIT
I could test it by doing it like this
...
response.status.should be(302)
but I got the idea from this question and it sounds like this could be powerful as it specifies the url redirected to.
Anyone have a working test like this?
To make the test more readable you can do this:
(rspec ~> 3.0)
expect(response).to redirect_to(action_path)
This line has problem
response.should redirect_to :back
The logic is not correct. You should expect #edit to redirect to :back path you set before, which is /scorecard. But you set :back here. In the context of Rspec, :back should be empty at each example.
To revise, just set it as
response.should redirect_to '/scorecard'
For testing if redirects happened, with no matching route
(just to test redirection, i used this when route is too long :D ).
You can simply do like:
expect(response.status).to eq(302) #redirected
In my case, it was not returning a response. If you end up in this situation, you can do:
expect(page.current_path).to eql('expected/path')

RSpec stub not working for integration test

Inside a controllers test, I want to test that when logged in, the controller renders the request fine, else if not logged in, it redirects to the login_path.
The first test passes fine as expected, no user is logged in, so the request is redirected to the login_path. However I've tried a myriad of stub/stub_chain's but still can't get the test to fake a user being logged in and render the page okay.
I would appreciate some direction on getting this to work as expected.
The following classes and tests are the bare bones to keep the question terse.
ApplicationController
class ApplicationController < ActionController::Base
include SessionsHelper
private
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
end
SessionsHelper
module SessionsHelper
def logged_in?
redirect_to login_path, :notice => "Please log in before continuing..." unless current_user
end
end
AppsController
class AppsController < ApplicationController
before_filter :logged_in?
def index
#title = "apps"
end
end
apps_controller_spec.rb
require 'spec_helper'
describe AppsController do
before do
#user = FactoryGirl.create(:user)
end
describe "Visit apps_path" do
it "should redirect to login path if not logged in" do
visit apps_path
current_path.should eq(login_path)
end
it "should get okay if logged in" do
#stubs here, I've tried many variations but can't get any to work
#stubbing the controller/ApplicationController/helper
ApplicationController.stub(:current_user).and_return(#user)
visit apps_path
current_path.should eq(apps_path)
end
end
end
This is not working because you are stubbing the method current_user on the ApplicationController class, and not an instance of that class.
I would suggest stubbing it (correctly) on an instance of that class, but your test appears to be an integration test rather than a controller test.
What I would do instead then is as Art Shayderov mentioned is to emulate the sign-in action for a user before attempting to visit a place that requires an authenticated user.
visit sign_in_path
fill_in "Username", :with => "some_guy"
fill_in "Password", :with => "password"
click_button "Sign in"
page.should have_content("You have signed in successfully.")
In my applications, I've moved this into a helper method for my tests. This is placed into a file at spec/support/authentication_helpers.rb and looks like this:
module AuthenticationHelpers
def sign_in_as!(user)
visit sign_in_path
fill_in "Username", :with => user.username
fill_in "Password", :with => "password"
click_button "Sign in"
page.should have_content("You have signed in successfully.")
end
end
RSpec.configure do |c|
c.include AuthenticationHelpers, :type => :request
end
Then in my request specs, I simply call the method to sign in as that particular user:
sign_in_as(user)
Now if you want to sign in using a standard controller test, Devise already has helpers for this. I generally include these in the same file (spec/support/authentication_helpers.rb):
RSpec.configure do |c|
c.include Devise::TestHelpers, :type => :controller
end
Then you can sign in using the helpers like this:
before do
sign_in(:user, user)
end
it "performs an action" do
get :index
end
I would look at http://ruby.railstutorial.org/chapters/sign-in-sign-out#sec:a_working_sign_in_method.
The author describes how to write a sign_in method and use it in your rspec tests.
It doesn't look like controller test. It looks more like rspec-rails request spec which simulates browser. So stabbing controller won't work, you have to either simulate sign in (something like this)
visit sign_in
fill_in 'username', :with => 'username'
...
or manually add user_id to session.
If on the other hand you want to test controller in isolation your test should look like that:
get 'index'
response.should be_success

How do I simulate a login with RSpec?

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)

Testing Sessions in Rails 3 with Rspec & Capybara

I'm trying to write integration tests with rspec, factory_girl & capybara. I also have cucumber installed, but I'm not using it (to my knowledge).
I basically want to prepopulate the db with my user, then go to my home page and try to log in. It should redirect to user_path(#user).
However, sessions don't seem to be persisted in my /rspec/requests/ integration tests.
My spec: /rspec/requests/users_spec.rb
require 'spec_helper'
describe "User flow" do
before(:each) do
#user = Factory(:user)
end
it "should login user" do
visit("/index")
fill_in :email, :with => #user.email
fill_in :password, :with => #user.password
click_button "Login"
assert current_path == user_path(#user)
end
end
Returns:
Failures:
1) User flow should login user
Failure/Error: assert current_path == user_path(#user)
<false> is not true.
# (eval):2:in `send'
# (eval):2:in `assert'
# ./spec/requests/users_spec.rb:16
Instead, it redirects to my please_login_path - which should happen if the login fails for any reason (or if session[:user_id] is not set).
If I try to put session.inspect, it fails as a nil object.
If I try to do this in the controller tests (/rspec/controllers/sessions_spec.rb), I can access the session with no problem, and I can call session[:user_id]
If you are using Devise, you'll need to include Warden::Test::Helpers (right after the require of spec_helper is a good place) as outlined in the warden wiki.
The call to session is returning nil because capybara doesn't provide access to it when running as an integration test.
I have the same problems and although filling out a form might be an option for some, I had to roll my own authentication ruby because I was using a third party auth system (Janrain to be exact).... in my tests I ended up using something like this:
Here is what I have in my spec/support/test_helpers_and_stuff.rb
module AuthTestHelper
class SessionBackdoorController < ::ApplicationController
def create
sign_in User.find(params[:user_id])
head :ok
end
end
begin
_routes = Rails.application.routes
_routes.disable_clear_and_finalize = true
_routes.clear!
Rails.application.routes_reloader.paths.each{ |path| load(path) }
_routes.draw do
# here you can add any route you want
match "/test_login_backdoor", to: "session_backdoor#create"
end
ActiveSupport.on_load(:action_controller) { _routes.finalize! }
ensure
_routes.disable_clear_and_finalize = false
end
def request_signin_as(user)
visit "/test_login_backdoor?user_id=#{user.id}"
end
def signin_as(user)
session[:session_user] = user.id
end
end
Then in my request spec, with capybara and selenium, I did the following:
describe "Giveaway Promotion" do
context "Story: A fan participates in a giveaway", js: :selenium do
context "as a signed in user" do
before :each do
#user = Factory(:user)
request_signin_as #user
end
it "should be able to participate as an already signed in user" do
visit giveaway_path
....
end
end
end
end
BTW, I came up with solutions after trying the proposed solutions to this post and this post and neither of them worked for me. (but they certainly inspired my solution)
Good luck!
You've probably moved on from this, but I was just struggling with the same question. Turns out it was a matter of syntax. I was using symbols for :email and :password and I should've been using strings instead ("email" and "password").
In other words, try changing this:
fill_in :email, :with => #user.email
fill_in :password, :with => #user.password
to this:
fill_in "email", :with => #user.email
fill_in "password", :with => #user.password

Resources