Ruby on Rails functional test with Devise authentication - ruby-on-rails

I'm searching for a solution for a weird problem. I have a controller, that needs authentication (with the devise gem). I added the Devise TestHelpers but i can't get it working.
require 'test_helper'
class KeysControllerTest < ActionController::TestCase
include Devise::TestHelpers
fixtures :keys
def setup
#user = User.create!(
:email => 'testuser#demomailtest.com',
:password => 'MyTestingPassword',
:password_confirmation => 'MyTestingPassword'
)
sign_in #user
#key = keys(:one)
end
test "should get index" do
get :index
assert_response :success
assert_not_nil assigns(:keys)
end
test "should get new" do
get :new
assert_response :success
end
test "should create key" do
assert_difference('Key.count') do
post :create, :key => #key.attributes
end
assert_redirected_to key_path(assigns(:key))
end
test "should destroy key" do
assert_difference('Key.count', -1) do
delete :destroy, :id => #key.to_param
end
assert_redirected_to keys_path
end
end
And i get the following output in my "rake test" window:
29) Failure:
test_should_create_key(KeysControllerTest) [/test/functional/keys_controller_test.rb:29]:
"Key.count" didn't change by 1.
<3> expected but was
<2>.
30) Failure:
test_should_destroy_key(KeysControllerTest) [/test/functional/keys_controller_test.rb:37]:
"Key.count" didn't change by -1.
<1> expected but was
<2>.
31) Failure:
test_should_get_index(KeysControllerTest) [/test/functional/keys_controller_test.rb:19]:
Expected response to be a <:success>, but was <302>
32) Failure:
test_should_get_new(KeysControllerTest) [/test/functional/keys_controller_test.rb:25]:
Expected response to be a <:success>, but was <302>
Can someone tell my, why devise doesn't authenticate? I'm using the exact same procedure for an AdminController and it works perfect.

Are you using Devise with confirmable? In this case, create is not enough and you need to confirm the user with #user.confirm!
Second, why do you create the user in the functional test? Declare your users in the fixture like this (confirmed_at if you require confirmation only):
test/fixtures/users.yml:
user1:
id: 1
email: user1#test.eu
encrypted_password: abcdef1
password_salt: efvfvffdv
confirmed_at: <%= Time.now %>
and sign them in in your functional tests with:
sign_in users(:user1)
Edit: I just saw, that in my app the Devise-Testhelpers are declared in test/test-helpers.rb and I don't know if this makes a difference, maybe you want to try:
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActionController::TestCase
include Devise::TestHelpers
end
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
fixtures :all
# Add more helper methods to be used by all tests here...
end

This took me some time to figure out but it turns out the answer is really simple.
The point is that, in your fixtures file (users.yml), you need to make sure the user is 'confirmed' (assuming that you specified "confirmable" in your User model). So, for instance, put this in your users.yml:
user_one:
confirmed_at: 2015/01/01
That's all, no need to specify other fields (email, encrypted password, etc).
Now in your controller test (e.g. in 'setup' or in 'before') you simply code:
sign_in users(:user_one)
And then it should just work!

I had a similar issue (but using FactoryGirl, rather than Fixtures) and was able to resolve it by simply using FactoryGirl.create(:user) rather than FactoryGirl.build(:user).
Evidently, Devise requires the user to have been persisted to the Database, for everything to work properly.

Related

setting up minitest with Devise with refactoring test_helper

Rails 7 application using devise for authentication.
fixture (model tests pass for all classes):
one:
email: 'me#mail.co'
encrypted_password: <%= User.new.send(:password_digest, '12345678')
test_helper which attempts to re-factor the login and action process into short one-liners (test_access) in the actual controller tests
require 'simplecov'
SimpleCov.start 'rails'
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require 'webmock/minitest'
require 'barby/outputter/png_outputter'
require 'barby/barcode/ean_13'
class ActiveSupport::TestCase
fixtures :all
include Devise::Test::IntegrationHelpers
include Warden::Test::Helpers
parallelize(workers: :number_of_processors)
def log_in(user)
if integration_test?
login_as(user, :scope => :user)
else
sign_in(user)
end
end
class Minitest::Test
def setup
#one = users(:one)
end
end
private
def test_access(user, action)
get '/users/sign_in'
puts user.inspect
sign_in user
# puts user_signed_in?
# puts current_user.manager?
puts user.manager?
post user_session_url
follow_redirect!
puts action
get action
assert_response :success
end
and the controller_test (presently on an intermediate refactoring level, as the method should extend to an array of users)
require "test_helper"
class LokationsControllerTest < ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
setup do
#users_allowed = [ #one ]
#actions = [dummy_lokations_url]
end
test "should get dummy" do
#actions.each do |action|
test_access(#one, action)
assert_response :success
end
end
the problem is there seems to be no way of verifying whether the user is signed in or not, notwithstanding the inclusion of devise integration helpers. The output in the console points to proper values, but the result is the opposite of the method that should be applied
#<User id: 760812109, email: [...]>
true
http://www.example.com/lokations/dummy
[...]
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being redirected.</body></html>
because the before_action method applied on the action specifies
def index_manager
if user_signed_in? && current_user.manager?
else
redirect_to root_path and return
end
end
Where is the above mistaken ? (I have a nagging doubt that something may be missing as this pattern has been successfully followed in the past)
Sorry but this is just not a good/acceptable refactor.
Do not set the password digest - ever. That should only be known by the underlying system that encrypts the password. Your code and tests should not even know that it exists. Your code should only ever set the password attribute with a cleartext.
These are integration and not controller tests. Controller tests are subclasses of ActionController::TestCase used in legacy applications. They should not be used. While this might seem like nickpicking you're only confusing yourself and others by conflating them.
Avoid monkeypatching a bunch of junk into ActiveSupport::TestCase or even worse Minitest::Test. If you MUST monkeypatch then write your methods in a module and include it so that the stack trace points there. But do it at the correct level in the class heirarchy. For example separate between the helpers for integration and system tests and include them into ActionDispatch::IntegrationTest and ActionDispatch::SystemTest instead of polluting all your tests.
If you need to add a lot of additional behavior don't reopen the class. Instead create your own test class which inherits from ActionDispatch::IntegrationTest and have your tests subclass it.
If you have code that you want to reuse in multiple tests like the test setup use modules that can be included where they are actually needed.
test_access doesn't belong in the shared subclass way up the tree. This belongs in an actual integration test which covers your authentication system flows or that a specific resource is authorized correctly.
Devise is an authentication system (who is the user?) - but what you're doing here is authorization (who can do what?). Devise doesn't provide any kind of authorization besides preventing access for users that are not authenticated. Neither should it (see SRP).
The index_manager method isn't a good way to implement authorization. If you want to reinvent the wheel make sure you raise an exception and use rescue_from so that the callback chain is halted and so that you're not repeating yourself. You may want to look into existing systems like CanCanCan and Pundit.
What you actually want here is something more like:
class AuthorizationError < StandardError
end
class ApplicationController
before_action :authenticate_user! # use an opt-out secure by default setup.
rescue_from AuthorizationError, with: :deny_access
private
def authorize_manager!
unless current_user.manager?
raise AuthorizationError.new("You need to be a manager to access this resource")
end
end
def deny_access(exception)
redirect_to root_path, error: exception.message
end
end
module AuthorizationIntegrationHelpers
def assert_denies_access
follow_redirect!
assert_current_path root_path
# ...
end
end
class FoosFlowTest < ActionDispatch::IntegrationTest
include AuthorizationIntegrationHelpers
include Warden::Test::Helpers
test "should not be accessable to non-managers" do
login_as(users(:one))
get '/foos'
assert_denies_access
end
end

Rails 6 Devise test helpers without RSpec

Devise test helpers with Rails 6 without Rspec doesn't seem to work. Here is the code, any idea why it might be getting errors?
Controller:
class VehiclesController < ApplicationController
before_action :authenticate_user!, only: [:new]
def index
#vehicles = Vehicle.all
end
def new
#vehicle = Vehicle.new
end
end
test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative "../config/environment"
require "rails/test_help"
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
include Devise::Test::IntegrationHelpers
end
user fixture:
valid_user:
first_name: "Toseef"
last_name: "zafar"
email: "exampleuser#gmail.com"
encrypted_password: <%= Devise::Encryptor.digest(User, '12345678') %>
controller test:
require "test_helper"
class VehiclesControllerTest < ActionDispatch::IntegrationTest
test "should be able to get to new form page" do
sign_in users(:valid_user)
get new_vehicles_url
assert_response :success
end
end
and this is the error I get:
Failure:
VehiclesControllerTest#test_should_be_able_to_get_to_new_form_page [/test/controllers/vehicles_controller_test.rb:12]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/users/sign_in>
Response body: <html><body>You are being redirected.</body></html>
rails test test/controllers/vehicles_controller_test.rb:9
Also, I don't know why it would point to http://www.example.com
The devise user model has got confirmable hence when we do sign_in users(:valid_user) devise creates a user but because the user is not confirmed (i.e. no confirmation email link clicking is involved) when we go to a secured URL it takes us back to login because user hasn't confirmed through clicking on the link from the email.
The solution to this is to set confirmed_at value to Time.now before sign_in
e.g.
#user = users(:valid_user)
#user.confirmed_at = Time.now
sign_in #user
after doing that the tests passed! :)

How do I log out a user in a Rails integration/controller unit test?

I have the following test to ensure sign in is required. Most of the tests require a signed in user, so I have the user sign in on setup. However for this test, I need them signed out.
require 'test_helper'
class DealsControllerTest < ActionDispatch::IntegrationTest
include Warden::Test::Helpers
setup do
#deal = deals(:one)
#user = users(:one)
# https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara
#user.confirmed_at = Time.now
#user.save
login_as(#user, :scope => :user)
end
teardown do
Warden.test_reset!
end
test "require sign in for deal list" do
logout #user
get deals_url
assert_redirected_to new_user_session_path
end
I get the error
Failure:
DealsControllerTest#test_require_sign_in_for_deal_list [C:/Users/Chloe/workspace/fortuneempire/test/controllers/deals_controller_test.rb:35]:
Expected response to be a <3XX: redirect>, but was a <200: OK>
It says it will work, but it's just not working.
https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara
If for some reason you need to log out a logged in test user, you can use Warden's logout helper.
logout(:user)
Rails 5.0.2
Devise 4.2.1
logout by itself without any parameters worked. Maybe logout :user would work too. I wasn't using FactoryGirl so was confusing it for a FactoryGirl symbol.

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)

rail3/rspec/devise: rspec controller test fails unless I add a dummy=subject.current_user.inspect

I'm trying to get RSpec working for a simple scaffolded app, starting with the rspec scaffold tests.
Per the devise wiki, I have added the various devise config entries, a factory for a user and an admin, and the first things I do in my spec controller is login_admin.
Weirdest thing, though... all my specs fail UNLESS I add the following statement right after the it ... do line:
dummy=subject.current_user.inspect
(With the line, as shown below, the specs pass. Without that line, all tests fail with the assigns being nil instead of the expected value. I only happened to discover that when I was putting some puts statements to see if the current_user was being set correctly.)
So what it acts like is that dummy statement somehow 'forces' the current_user to be loaded or refreshed or recognized.
Can anyone explain what's going on, and what I should be doing differently so I don't need the dummy statement?
#specs/controllers/brokers_controller_spec.rb
describe BrokersController do
login_admin
def valid_attributes
{:name => "Bill", :email => "rspec_broker#example.com", :company => "Example Inc", :community_id => 1}
end
def valid_session
{}
end
describe "GET index" do
it "assigns all brokers as #brokers" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :index, {}, valid_session
assigns(:brokers).should eq([broker])
end
end
describe "GET show" do
it "assigns the requested broker as #broker" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :show, {:id => broker.to_param}, valid_session
assigns(:broker).should eq(broker)
end
end
and per the devise wiki here is how I login a :user or :admin
#spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in Factory.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in user
end
end
end
What a struggle! Thank you Robin, I've been googling on this for hours and finally saw your post; now my controller tests are working :)
To add to your answer, I figured out how to get the devise session into the valid_session hash, which allows the controller tests to run properly as generated by rails.
def valid_session
{"warden.user.user.key" => session["warden.user.user.key"]}
end
In your tests, there is the following code:
def valid_session
{}
end
...
get :index, {}, valid_session
Because of this 'session' variable, the "log_in" that you did is essentially not being used during the 'get'.
The way that I solved it was to remove all of the "valid_session" arguments to the get, post, put, delete calls in that controller's spec. The example above becomes:
get :index, {}
I suspect that there's a way to add the devise's session to the "valid_session" hash, but I don't know what it is.
Thanks for this solution.
If you are using a different Devise model, the session id also changes.
For a model Administrator use the following:
def valid_session
{'warden.user.administrator.key' => session['warden.user.administrator.key']}
end

Resources