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
Related
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! :)
Summary
I am building a Rails app which includes a user registration process. A username and password are necessary to create a user object in the database; the username must be unique. I am looking for the right way to test that the uniqueness validation prompts a particular action of a controller method, namely UsersController#create.
Context
The user model includes the relevant validation:
# app/models/user.rb
#
# username :string not null
# ...
class User < ApplicationRecord
validates :username, presence: true
# ... more validations, class methods, and instance methods
end
Moreover, the spec file for the User model tests this validation using shoulda-matchers:
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
it { should validate_uniqueness_of(:username)}
# ... more model tests
end
The method UsersController#create is defined as follows:
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
render :show
else
flash[:errors] = #user.errors.full_messages
redirect_to new_user_url
end
end
# ... more controller methods
end
Since the User spec for username uniqueness passes, I know that a POST request which contains a username already in the database will cause UsersController#create to enter the else portion of the conditional, and I want a test to verify this situation.
Currently, I test how UsersController#create handles the uniqueness validation on username in the following manner:
# spec/controllers/users_controller_spec.rb
require 'rails_helper'
RSpec.describe UsersController, type: :controller do
describe 'POST #create' do
context "username exists in db" do
before(:all) do
User.create!(username: 'jarmo', password: 'good_password')
end
before(:each) do
post :create, params: { user: { username: 'jarmo', password: 'better_password' }}
end
after(:all) do
User.last.destroy
end
it "redirects to new_user_url" do
expect(response).to redirect_to new_user_url
end
it "sets flash errors" do
should set_flash[:errors]
end
end
# ... more controller tests
end
Issue
My primary concern is the before and after hooks. Without User.last.destroy, this test will fail when run in the future: The new record can't be created, and thus the creation of a second record with the same username doesn't occur.
Question
Should I be testing this particular model validation in the controller spec? If so, are these hooks the right/best way to accomplish this goal?
I'll steer away from an opinion on the 'should I...' part, but there are a couple of aspects worth considering. First, although controller tests have not been formally deprecated, they have generally been discouraged by both the Rails and Rspec teams for a while now. From the RSpec 3.5 release notes:
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
Whether or not the scenario warrants a corresponding request spec is a judgement call, but if you want to unit test the validation at the model level, check out the shoulda matchers gem, which assists with model validation testing).
In terms of your question about hooks, before(:all) hooks run outside a database transaction, so even if you have use_transactional_fixtures set to true in your RSpec configuration, they won't be automatically rolled back. So, a matching after(:all) like you have is the way to go. Alternatives include:
Creating the user inside a before(:each) hook, which does run in a transaction and is rolled back. That's at the potential cost of some test performance.
Use a tool like the Database Cleaner gem, which gives you fine-grained control over the strategies for cleaning your test databases.
If you want to cover the controller together with the user feedback aspect of this I would suggest a feature spec:
RSpec.feature "User creation" do
context "with duplicate emails" do
let!(:user) { User.create!(username: 'jarmo', password: 'good_password') }
it "does not allow duplicate emails" do
visit new_user_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'p4ssw0rd'
fill_in 'Password Confirmation', with: 'p4ssw0rd'
expect do
click_button 'Sign up'
end.to_not change(User, :count)
expect(page).to have_content 'Email has already been taken'
end
end
end
Instead of poking inside the controller this drives the full stack from the user story and tests that the view actually has an output for the validation errors as well - it thus provides value where a controller spec provides very little value.
Use let/let! to setup givens for a particular example as it has the advantage that you can reference them in the example through the helper method it generates. before(:all) should generally be avoided apart from stuff like stubbing out API's. Each example should have its own setup/teardown.
But you also need to deal with the fact that the controller itself is broken. It should read:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
render :new, status: :unprocessable_entity
end
end
end
When a record is invalid you should NOT redirect back. Render the form again as you're displaying the result of performing a POST request. Redirecting back will make for a horrible user experience since all the fields will be blanked out.
When creating a resource is successful you should redirect the user to the newly created resource so that the browser URL actually points to the new resource. If you don't reloading the page will load the index instead.
This also removes the need to stuff the error messages in the session. If you want to give useful feedback through the flash you would do it like so:
class UsersController < ApplicationController
def create
#user = User.new(user_params)
if #user.save
redirect_to #user
else
flash.now[:error] = "Signup failed."
render :new, status: :unprocessable_entity
end
end
end
And you can test it with:
expect(page).to have_content "Signup failed."
I want the simplest example to test the validity of attributes of devise's actions for new registrations (email, password, password confirmation), sign_in and forgot_password. The internet is full of RSpec ones but I want to go native with what Rails 4.2 gives me and there is absolutely nothing.
I am stuck to the default implementation:
require 'test_helper'
class RegistrationsControllerTest < ActionController::TestCase
def setup
#controller = RegistrationsController.new
#request = ActionController::TestRequest.new
#response = ActionController::TestResponse.new
#request.env["devise.mapping"] = Devise.mappings[:user]
#user = Registrations.new(username: "John", email: "myemail#email.com")
end
end
I know this isn't too much but I am making my first steps in TDD, so please don't shoot !
How do I check for the validity of a user's attributes, for example a nil email or a password of 100 characters ?
You can check with rails in-build minitest
I have assumed you have overriding devise's registration_controller.
In tdd for controller you can write test cases for action.
Check modified code :-
require 'test_helper'
class RegistrationsControllerTest < ActionController::TestCase
include Devise::TestHelpers # for including devise's actions
def setup # this set up default settings for controller
#controller = RegistrationsController.new
#request = ActionController::TestRequest.new
#response = ActionController::TestResponse.new
#request.env["devise.mapping"] = Devise.mappings[:user]
#user = Registrations.new(username: "John", email: "myemail#email.com")
end
setup do # this used for setting global variable used in test file
#user= users(:one) # users is the fixture file of test in which you can set default data for test environment.
end
test "should create user" do # then you test cases for controller
sign_in users(:one)
post :create, users:{email:'test#test.com, password:'XXXX'...}# you can pass arguments for create method. Please check it once, i am not sure about names
assert_response :success
end
end
Rails minitest provides some assertion.
I am trying to test a CanCan ability in my app that also uses Authlogic. I have verified the correct behavior works when using the actual site, but I want to write a functional test that will alert me if this behavior breaks in the future. My ability file is simple, and looks as follows:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
can :read, User
can :manage, User, :id => user.id
cannot :create, User
can :destroy, UserSession
if user.role? :guest
can :create, UserSession
cannot :destroy UserSession
end
end
end
My test for the UserSessionsController is also simple, and looks like this:
test "should redirect new for member" do
default_user = login :default_user
assert default_user.role? :member
assert_raise(CanCan::AccessDenied) { get :new }
assert_redirected_to root_path
end
Just for reference, my test_helper.rb looks like this:
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'authlogic/test_case'
class ActiveSupport::TestCase
fixtures :all
setup :activate_authlogic
def login(user_login)
UserSession.create users(user_login)
users(user_login)
end
end
When I run my code, my test fails, however:
test_should_redirect_new_for_member FAIL
CanCan::AccessDenied expected but nothing was raised.
Assertion at test/functional/user_sessions_controller_test.rb:13:in `block in <class:UserSessionsControllerTest>'
If I comment out the assert_raise, the redirect assertion also fails. Does anyone see anything wrong with my code that is causing this test to fail?
The problem was that I was rescuing the AccessDenied in my ApplicationController, so the exception was never being raised.
You need to block new action too.
if !(user.role? :member)
can :new, User
end
May be User having 'member' role have access to new action(display form for user) and restricted access to create action.
And one more thing, we don't need to use
cannot [:any_action], [Model]
We can do everything by can itself.
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.