Rails Pundit Policy Spec test failing with NoMethodError - ruby-on-rails

I have just started using Pundit for authorization in my current project along with the pundit-matchers gem.
So far it seems to generally be working for me but I have a problem in my tests.
I have generally tried to follow the examples in the pundit-matchers readme and the Thunderbolt labs blog (http://thunderboltlabs.com/blog/2013/03/27/testing-pundit-policies-with-rspec/).
This is my policy file;
#app/policies/procedure_policy.rb
class ProcedurePolicy
attr_reader :user, :procedure
def initialize(user, procedure)
#user = user
#procedure = procedure
end
def index?
user.admin?
end
end
And this is my policy_spec file
require 'rails_helper'
describe ProcedurePolicy do
subject {described_class.new(user, procedure)}
let(:procedure) {FactoryGirl.create(:procedure)}
context "for a guest" do
let(:user) {nil}
it {is_expected.not_to permit_action(:index)}
end
context "for a non-admin user" do
let(:user) {FactoryGirl.create(:user)}
it {is_expected.not_to permit_action(:index)}
end
context "for an admin user" do
let(:user) {FactoryGirl.create(:admin_user)}
it {is_expected.to permit_action(:index)}
end
end
2 of my 3 tests pass; The "for a non-admin user" and "for an admin user" ones. The "for a guest" test fails with
NoMethodError:
undefined method `admin?' for nil:NilClass
Now I understand why. I'm passing nil to the #index? method of my ProcedurePolicy class which will not have an #admin? method. But all of the example specs I have found online do exactly this. What am I not seeing.
Apologies if I'm missing something really obvious. I've been away from coding for a couple of years.

Pundit calls the current_user method to set user, and typically that is provided by an authentication system like Devise or a custom solution. This means that in most scenarios, the expectation is that you always have a layer of authentication before you hit the Pundit logic, so you never have a user set to nil when it gets there.
If you want Pundit authorizations to work directly without authentication, you have to handle that in your policy definitions, ex:
class ProcedurePolicy
def index?
user.present? && user.admin?
end
end
http://www.rubydoc.info/gems/pundit#Policies

The "being a visitor" context is for testing authorisation attempts when there is no user currently authenticated in the system. Since no user is logged in, no user record/object exists - the absence of a user object is nil. This is why both the Thunderbolt Labs and pundit-matchers examples use nil to represent a visitor. The nil object does not have an admin? method, causing the error.
To correctly handle nil/guest users in your policy, check that the user object is present before checking if the user is an admin, either by checking the user object directly or using the present? method in Rails, e.g.
def index?
user.present? && user.admin?
end
or just:
def index?
user && user.admin?
end
You'll find that authentication systems such as Devise return nil from the current_user method when no user is signed in.

Related

Using Session, Cookies or Current_something in Rspec request tests

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

Rails 5 - integration tests NoMethodError: undefined method `signed' for #<Rack::Test::CookieJar:0x00000006796390>

I've been looking for solution for quite a long time but I can't really understand what the actual problem is. I'm following Ruby On Rails Tutorial by Michael Hartl and trying to make some integration tests for login. After a few modifications the code in test file looks like this:
test 'login without remembering' do
login_as #user, remember: '1'
delete logout_path
login_as #user, remember: '0'
assert_not session[:user_id].nil?
assert_nil cookies[:remember_token]
assert_nil cookies[:user_id]
end
test 'login with remembering' do
login_as #user, remember: '1'
assert logged_in?
assert_not_empty cookies[:remember_token]
assert_not_empty cookies[:user_id]
assert session[:user_id].nil?
assert_equal assigns(:user).remember_token, cookies[:remember_token]
end
logged_in? method is avaible by include SessionsHelper in ActionDispatch::IntegrationTest. It's core part is to find an user:
if (user_id = session[:user_id]) # User is not remembered
#current_user = User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id]) # User is remembered
user = User.find_by(id: user_id)
remember_token = cookies[:remember_token]
(#current_user = user) if user && user.authenticated?(remember_token)
end
However, because test cookie jar is Rack::Test::CookieJar this simply doesn't work and raises NoMethodError: undefined method 'signed' for #<Rack::Test::CookieJar:0x00000006796390>
This came in useful and helped me solve the problem. After some time thinking I did that:
class ActionDispatch::IntegrationTest
# Get ActionDispatch::Cookies::CookieJar because Rack::Test::CookieJar
# doesn't support signed cookies
def cookies
cookies = ActionDispatch::Request.new(Rails.application.env_config).cookie_jar
end
include SessionsHelper
def login_as(user, password: 'Password', remember: '0')
post login_path, params: { session: { email: user.email,
password: password,
remember: remember } }
# Set cookies manually
return if remember == '0'
user = assigns(:user)
return if user.remember_token.nil?
cookies[:remember_token] = user.remember_token
cookies.signed[:user_id] = user.id
end
end
However, I still don't understand why it solves my problem and whether it's a good solution. If I don't assign cookie_jar from requested object to cookies variable in cookies method, assert session[:user_id].nil? fails even though I log in with remembering (for me it means using cookies instead of session). I'm trying to wrap my head around that but it's beyond my capabilities.
EDIT:
It's getting ever weirder. It looks like I get different errors after running tests several times. Some of them just turn up from time to time.
Well, none of those modifications solved my problem to be honest, some of my tests started to behave inconsistently.
I've decided to thumb through Ruby On Rails Tutorial and discovered that instead of trying to replace the default cookie jar in integration test I can simply check cookies in helper test. It turns out that in tests for helpers the cookie jar is not Rack::Test::CookieJar but ActionDispatch::Cookies::CookieJar which is fine and works with signed method. For integration tests I simply rely on modified logged_in? method which tests only session[:user_id] and looks like this:
class ActiveSupport::TestCase
def logged_in?
!session[:user_id].nil?
end
end
To ensure that cookies works, I created a suitable test for SessionsHelper which can be found in rails tutorial.
What is also important is that I underestimated session. I've decided to store user's id there and treat it as a main indicator that the user is logged in, thereby using cookies only for remembering users. In this way I don't have to worry about testing cookies in integration tests which seems to be faulty.
Big thanks to OnlySteveH who helped me out by asking some useful questions.

Rails 4: making the first user an admin?

I'm using FB and G+ for authentication. How do I make the first user that is registered an admin? I've looked at a few answers like this one, but I get errors (like "undefined method `update_attribute' for nil:NilClass" for the previous linked answer) and they are mostly old and for devise gem.
users_controller
def update
#user = User.find(params[:id])
if #user.update(user_params)
redirect_to #user
else
render 'edit'
end
end
private
def user_params
params.require(:user).permit(:admin, :banned)
end
This should be done in the db/seeds.rb file which you can execute with rake db:seed. The linking with Facebook and G+ will have to be done after the fact. I would not depend on any solution that automatically promotes the first user to admin status. That should only ever done explicitly.
Example seeds.rb:
# This example is using Devise, however it can easily be adapted to whatever
# setup you require as there is nothing "devise" specific here.
user = User.find_or_create_by(email: "Admin Email")
user.password = "Admin Password"
user.admin = true
user.save
Another tool I use is by registering a user normally and then manually promoting them to Admin status via a tool like rails console.
EDIT
If you still wish to pursue your method (which I consider unsafe) then you can use ActiveRecord hooks like so:
class User < ActiveRecord::Base
# relations and stuff
before_save :check_to_make_admin
private
def check_to_make_admin
# Only the first user should be made admin
if User.count == 0
self.admin = true
end
end
end
It's important to note that I strongly discourage this approach and recommned using the seed method which was created for purposes like this.

Rails - Securely updating user information

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.

How to stub ApplicationController method in request spec

I am needing to stub the response of a current_user method in an Rspec/capybara request spec. The method is defined in ApplicationController and is using helper_method. The method should simply return a user id. Within the test, I'd like this method to return the same user id each time.
Alternatively, I could fix my problem by setting session[:user_id] in the spec (which is what current_user returns)... but that doesn't seem to work either.
Are either of these possible?
Edit:
Here is what I've got (it is not working. It just runs the normal current_user method).
require 'spec_helper'
describe "Login" do
before(:each) do
ApplicationController.stub(:current_user).and_return(User.first)
end
it "logs in" do
visit '/'
page.should have_content("Hey there user!")
end
end
Also not working:
require 'spec_helper'
describe "Login" do
before(:each) do
#mock_controller = mock("ApplicationController")
#mock_controller.stub(:current_user).and_return(User.first)
end
it "logs in" do
visit '/'
page.should have_content("Hey there user!")
end
end
skalee seems to have provided the correct answer in the comment.
If the method you're trying to stub is an instance method (most likely) and not a class method then you need use:
ApplicationController.any_instance.stub(:current_user)
Here are a couple of examples of the basic form.
controller.stub(:action_name).and_raise([some error])
controller.stub(:action_name).and_return([some value])
In your particular case, I believe the proper form would be:
controller.stub(:current_user).and_return([your user object/id])
Here's a full working example from a project I work on:
describe PortalsController do
it "if an ActionController::InvalidAuthenticityToken is raised the user should be redirected to login" do
controller.stub(:index).and_raise(ActionController::InvalidAuthenticityToken)
get :index
flash[:notice].should eql("Your session has expired.")
response.should redirect_to(portals_path)
end
end
To explain my full example, basically what this does is verify that, when an ActionController::InvalidAuthenticityToken error is raised anywhere in the app, that a flash message appears, and the user is redirected to the portals_controller#index action. You can use these forms to stub out and return specific values, test an instance of a given error being raised, etc. There are several .stub(:action_name).and_[do_something_interesting]() methods available to you.
Update (after you added your code): per my comment, change your code so it reads:
require 'spec_helper'
describe "Login" do
before(:each) do
#mock_controller = mock("ApplicationController")
#mock_controller.stub(:current_user).and_return(User.first)
end
it "logs in" do
visit '/'
page.should have_content("Hey there user!")
end
end
This works for me and gives me a #current_user variable to use in tests.
I have a helper that looks like this:
def bypass_authentication
current_user = FactoryGirl.create(:user)
ApplicationController.send(:alias_method, :old_current_user, :current_user)
ApplicationController.send(:define_method, :current_user) do
current_user
end
#current_user = current_user
end
def restore_authentication
ApplicationController.send(:alias_method, :current_user, :old_current_user)
end
And then in my request specs, I call:
before(:each){bypass_authentication}
after(:each){restore_authentication}
For anyone else who happens to need to stub an application controller method that sets an ivar (and was stymied by endless wanking about why you shouldn't do that) here's a way that works, with the flavour of Rspec circa October 2013.
before(:each) do
campaign = Campaign.create!
ApplicationController.any_instance.stub(:load_campaign_singleton)
controller.instance_eval{#campaign = campaign}
#campaign = campaign
end
it stubs the method to do nothing, and sets the ivar on rspec's controller instance, and makes it available to the test as #campaign.
For Rspec 3+ the new api is:
For a controller test, nice and short:
allow(controller).to receive(:current_user).and_return(#user)
Or for all instances of ApplicationController:
allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(#user)
None of the provided responses worked for me. As in #matt-fordam's original post, I have a request spec, not a controller spec. The test just renders the view without launching a controller.
I resolved this by stubbing the method on the view as described in this other SO post
view.stub(:current_user).and_return(etc)

Resources