I am trying to write a RSpec before filter to log a user in. I have a products controller. In order to view products, a user must be logged in. I added a login method to spec/support/utilities like so:
def login(user)
post login_path, email: user.email, password: "password"
end
Then I called the method in a before filter in my spec/controllers/products test:
before :each do
user = FactoryGirl.create(:user)
login(user)
end
When I run the test I get the following error:
The action '/login' could not be found for ProductsController
I have a route for /login and my user authentication is simple - just like Railscasts #250 Authentication from Scratch. What am I missing?
If this is a controller spec, then the problem is that Routes are not available when unit testing controllers. The point is to test in isolation, which means no routes
Login is calling the ProductsController, that does not have a login method. As other told you, I wouldn't call one controller when you are testing another. Therefore, I think you should login some other way. For example:
def login(user)
session[:user_id] = user.id
end
Related
I am trying to follow the new Rails testing convention with this new project I am working on. Therefore I setup unit tests, request tests and feature tests. The issue is that in the request tests, Capybara and the the session information are not supported.
In addition to having the current_user method setup by Devise, my application has another similar method called current_client. For some of my controllers, I need to check whether the current_user is logged in. This works with Devise authenticate_user! called in a before_action. But for some controllers, I also need to check if a client was selected first (for example if you want to add transactions, they need to be tied with the current client being worked on).
So I added another before_action method that checks if a client was also selected. This works well in feature tests with Capybara where I can mimic user loggin in and the user selecting a client to process. However in the request tests, It doesn`t work.
I first test trying to access the endpoint without a user being logged in and the response is not successful (as it should) and it redirects to the sign in page. But then I need to run my tests with a user logged in AND a client selected. Devise helpers provide a sign_in(user) method. However I can't get my current_user method to work and I can't seem to be able to set it up properly. So those tests are failing and redirecting to another page asking the user to select a client.
I have already tried many suggestions I saw. Like trying to stub the current_client method, trying to provide session info to the GET call. I even tried to check the Devise source code to find out how they can simulate the current_user method in Rspec and couldn't really find where the magic happens.
Here is the code
The current_client method looks like this
def current_client
current_client ||= Client.find(session[:client_id]) if session[:client_id]
rescue ActiveRecord::RecordNotFound
current_client = nil
end
This is how it is set once a user selects a client to process
def set_current_client(client)
session[:client_id] = client.id
end
This is the code in my test file
I first create 2 users so that I can test that user 1 cannot access user 2 transactions down the line. (I'm using FactoryBot)
I then create 2 clients (one for each user)
RSpec.describe "Rooms", :type => :request do
let!(:user) {create(:user)}
let!(:user2) {create(:user2)}
let!(:client) {create(:client, user: user)}
let!(:client) {create(:client, user: user2)}
The following works
describe 'User Not Signed In' do
describe 'GET #index' do
subject { get transact_path}
it "returns an unsuccessful response" do
subject
expect(response).to_not be_successful
end
it "redirects to sign in page" do
subject
expect(response).to redirect_to (new_user_session_path)
end
end
The following doesn't. The sign_in(user) method works in the before block and the redirect is not made to the sign in page. However, the response is not successful because the current_client is not set and I have tried to set it in so many ways in the before block without success.
describe 'User Signed In' do
before do
sign_in(user)
end
describe 'GET #index' do
it "returns a successful response" do
get transact_path
expect(response).to be_successful
end
end
end
I understand that the best practices moved away from controller tests because what view rendered or what instance variable assigned doesn't have anything to do with controllers in theory. But in my case, I'm simply trying to test my endpoints and right now I can't because I cannot setup my current_client.
I found a way around it by using a suggested solution by DHH himself.
Instead of trying to stub the current_client variable or try to jury rig something in the gut of ActionDispatch::Cookies, you simply need to do a POST or GET call to whatever controller is responsible for setting my current_client variable.
So for me the solution was to put the following code in a before do block
before do
sign_in(user)
get select_client_path, params: {id: client.id}
end
In many books and tutorials, I see people implementing the update method in users controller as either find the user data by params[:id] or session[:id]. However, I feel it possible to simply modify the session or the address bar to hack into someone else's account and modify his/her information. So how to I make sure the user can only update his/her own information instead of changing others.
The answer is application specific. You'll need to first "find" the record - where the details of how to find that record depends on the application.
Yes, as you indicate, you should not just blindly instantiate the record and update it. Your business logic needs to intelligently find the pertinent record, before you expose it to be updated.
Treat the User model the same as any other model and add your authentication to it and require the user to be logged in (e.g. current_user for some auth methods) within the uddate method.
if current_user
update_stuff and redirect
else
:notice => 'Not authd' and redirect
end
I once had this problem when working on applications that required some things to be private. What I did was use Devise and Cancan along with great rails practices. I used Devise for authentication, cancan for authorization.
In some case you can pass the user id through your form using a hidden_field. I recommend passing the user_id in your controller create action and also when using devise you have access to the current_user method which would only create the model based on the current users session and id.
Secure the users information using cancan. For example if I could only show a users photos like so without user2 seeing the photos.
#photos = current_user.photos #only grabs current users photos
With cancan I could stop the user from manually entering a url like users/2/edit by doing somthing like this
Ability.rb
can :manage, Photo, :user_id => user.id
controller.rb
load_and_authorize_resource :photo
def index
authorize! :index, #photo
end
I have actually done quite a bit of application building/research around this topic so I will try to give a detailed answer.
In the same kind of way you can divide sections of an application or website between non-users, signed-in users and admin accounts, you can implement checks to ensure that, for example, only the correct user can change their account password. Here is a test you might use for such a method:
describe "as wrong user" do
let(:user) { FactoryGirl.create(:user) }
let(:wrong_user) { FactoryGirl.create(:user, email: "wrong#example.com") }
before { sign_in user, no_capybara: true }
describe "submitting a GET request to the Users#edit action" do
before { get edit_user_path(wrong_user) }
specify { expect(response.body).not_to match(full_title('Edit user')) }
specify { expect(response).to redirect_to(root_url) }
end
describe "submitting a PATCH request to the Users#update action" do
before { patch user_path(wrong_user) }
specify { expect(response).to redirect_to(root_url) }
end
end
end
If you are new to testing (or unsure what this specifically does) I will explain:
In the code a factory can take an option (this particular line below)
FactoryGirl.create(:user, email: "wrong#example.com")
This basically creates a user with a different email address from the default. Then all that is happening is the tests specify that the wrong user should not have access to the original user’s edit or update actions.
The next step would be to add a before filter to check the status of the current user. This might go in your users_controller.rb file.
before_action :correct_user, only: [:edit, :update]
Then secondly (still in the users_controller.tb file) create a private method to check the current user has permission to modify the data otherwise redirect them back to the root url (if you were fancy you could use a [:notice] saying "you do not have access to this part of the applications" or something similar)
private
def correct_user
#user = User.find(params[:id])
redirect_to(root_url) unless current_user?(#user)
end
The correct_user filter uses the current_user? boolean method which you define in the Sessions helper as follows:
def current_user
remember_token = User.encrypt(cookies[:remember_token])
#current_user ||= User.find_by(remember_token: remember_token)
end
def current_user?(user)
user == current_user
end
That isn't exactly the entire dogma for security when it comes to who can access what but I hope it provides you with the missing details mentioned in your question.
In my users_controller_spec.rb I put
#u = FactoryGirl.create(:user)
ApplicationController.should_receive(:current_user).at_least(:once).and_return(#u)
controller.stub :current_user => #u
and then I want to write controller specs that show me if a object is indeed inaccesible for the users that should not see it - there exits a require_ownership function in the application_controller.rb which uses the var #current_user to see who's logged in, but with the method above I cannot find to make the user "logged in".
It's working if I do UserSession.create(#u) - and this is fine for me.
I'm using RSpec to test controller behavior in rails. One of the expectations I have is the following scenario:
User attempts to access a protected (requires login) url with params /home?val=123.
That user is redirected to login page.
After login the user should be redirected back to the URL originally requested, with relevant params.
I'm using devise for my auth system (using omniauth), and I've set up the recommended macros to login the user
and all that.
I'm a bit stuck on how to test the flow of the above behavior:
describe HomeController do
context "user is logged out" do
it "redirects back to original page after signin" do
# user is not signed in - redirected to login page
get :index, { :val => '12345' }
current_user.should_not be_present
expect(response).to redirect_to(login_page_path)
# should now login the user and verify
# that request.fullpath == '/home?val=12345'
# ?...
end
end
end
Does it even belong in a controller spec? Any help or example/s will be appreciated. thanks.
Luacassus looks like he has a decent answer, but have you looked at request testing with Capybara?
There's a really decent tutorial on Rails Casts here.
That's basically what we use to test our sign-up tests. However, we're not testing omniauth so it might not be exactly what you're looking for.
I have a Rails 3.1 (RC5) app with Devise and CanCan. Both are configured well and working as expected except that when I run integration tests to ensure that AccessDenied is being redirected as desired, the redirect goes to Devise's sign in instead of the application root. I can verify in my test that the user is still logged in and can still access applicable parts of the app.
The redirect is defined in this short controller, which the other restricted controllers inherit (instead of directly inheriting ApplicationController).
class AuthorizedController < ApplicationController
before_filter :authenticate_user!
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
end
end
The restricted controllers look like this:
class Admin::UsersController < AuthorizedController
load_and_authorize_resource
def index
#users = User.all.order('name')
end
...
end
I am using the default (ActionDispatch::IntegrationTest) integration test; the only additional testing gems I have are Capybara, Machinist, and Faker (no RSpec, Cucumber, etc.).
My test looks like:
def test_user_permissions
sign_in users(:user)
get admin_users_path
assert_response :redirect
assert_redirected_to root_url
end
The test fails with:
Expected response to be a redirect to <http://www.example.com/> but was a redirect to <http://www.example.com/users/sign_in>
When I test this by logging in as a restricted user in my dev environment, I am redirected to '/' as expected, but using the same type of user in the integration tests fails.
In the integration tests, the user is not actually being logged out, although the redirect makes it look like that is happening. When I change the test to not test the redirection target and continue trying other URLs, the user is still logged in and the test passes.
Addendum & Solution:
I originally did not include the sign_in method that held the key clue. Here it is:
module ActionController
class IntegrationTest
include Capybara::DSL
def sign_in (user, password = 'Passw0rd')
sign_out
visit root_path
fill_in 'Email', :with => user.email
fill_in 'Password', :with => password
click_button 'Sign in'
signed_in? user
end
...
end
end
I was mixing Capybara access methods (visit, click_button, etc.) in sign_in and vanilla integration test access methods (get, etc.) in the test. When I used Webrat (before Capybara) this mixing worked as I expected, but evidently Capybara's session state is handled separately, so access via the Capybara methods was authenticated, but access via the vanilla integration test methods was not.
You didn't post your devise config in ApplicationController, but it looks like the devise Identity/Sign-in checks are loading up before the CanCan Authorization checks (which makes sense).
It looks like your test sign in setup isn't working correctly, because '/users/sign_in' is the default redirect for devise when there isn't a valid user session.
Because the devise identity check is failing, it's never hitting your CanCan Authorization check. Why ask what the user can do if there isn't a user yet.
The before_filters will execute from the base ApplicationController first on up the chain in the order they are defined, so subclass filters after base class filters. This is why I think you've got Devise config in your base ApplicationController causing this not to hit CanCan.
If you post your Devise config/code we may be able to help you debug it further.
EDIT / TLDR:
You are seeing a redirect to login because Devise doesn't think a valid user session exists. Your test "sign_in()" helper isn't working the way you think it is. That's why this works in dev mode with a live user session via manual login.