Testing ApplicationController Filters, Rails - ruby-on-rails

I'm trying to use rspec to test a filter that I have in my ApplicationController.
In spec/controllers/application_controller_spec.rb I have:
require 'spec_helper'
describe ApplicationController do
it 'removes the flash after xhr requests' do
controller.stub!(:ajaxaction).and_return(flash[:notice]='FLASHNOTICE')
controller.stub!(:regularaction).and_return()
xhr :get, :ajaxaction
flash[:notice].should == 'FLASHNOTICE'
get :regularaction
flash[:notice].should be_nil
end
end
My intent was for the test to mock an ajax action that sets the flash, and then verify on the next request that the flash was cleared.
I'm getting a routing error:
Failure/Error: xhr :get, :ajaxaction
ActionController::RoutingError:
No route matches {:controller=>"application", :action=>"ajaxaction"}
However, I expect that there a multiple things wrong with how I'm trying to test this.
For reference the filter is called in ApplicationController as:
after_filter :no_xhr_flashes
def no_xhr_flashes
flash.discard if request.xhr?
end
How can I create mock methods on ApplicationController to test an application wide filter?

To test an application controller using RSpec you need to use the RSpec anonymous controller approach.
You basically set up a controller action in the application_controller_spec.rb file which the tests can then use.
For your example above it might look something like.
require 'spec_helper'
describe ApplicationController do
describe "#no_xhr_flashes" do
controller do
after_filter :no_xhr_flashes
def ajaxaction
render :nothing => true
end
end
it 'removes the flash after xhr requests' do
controller.stub!(:ajaxaction).and_return(flash[:notice]='FLASHNOTICE')
controller.stub!(:regularaction).and_return()
xhr :get, :ajaxaction
flash[:notice].should == 'FLASHNOTICE'
get :regularaction
flash[:notice].should be_nil
end
end
end

Related

Rails testing: ensure authorization (Pundit) is enforced in all controllers and actions

I'm writing RSpec tests for a Rails 4.2 application which uses Pundit for authorization.
I'd like to test whether authorization is enforced in all actions of all controllers, to avoid unintentionally providing public access to sensitive data in case a developer forgets to call policy_scope (on #index actions) and authorize (on all other actions).
One possible solution is to mock these methods in all controller unit tests. Something like expect(controller).to receive(:authorize).and_return(true) and expect(controller).to receive(:policy_scope).and_call_original. However, that would lead to a lot of code repetition. This line could be placed within a custom matcher or a helper method in spec/support but calling it in every spec of every controller also seems repetitive. Any ideas on how to achieve this in a DRY way?
In case you are wondering, Pundit's policy classes are tested separately, as shown in this post.
Pundit already provides a mechanism to guarantee a developer can't forget to authorize during the execution of a controller action:
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
end
This instructs Pundit to raise if the auth wasn't performed. As long as all your controllers are tested, this will cause the spec to fail.
https://github.com/elabs/pundit#ensuring-policies-and-scopes-are-used
I feel like you could use something like this up in spec_helper. Note that I'm assuming a naming convention where you have the word "index" in the index level answers, so that your spec might look like this:
describe MyNewFeaturesController, :type => :controller do
describe "index" do
# all of the index tests under here have policy_scope applied
end
# and these other tests have authorize applied
describe 'show' do
end
describe 'destroy' do
end
end
and here is the overall configuration:
RSpec.configure do |config|
config.before(:each, :type => :controller) do |spec|
# if the spec description has "index" in the name, then use policy-level authorization
if spec.metadata[:full_description] =~ /\bindex\b/
expect(controller).to receive(:policy_scope).and_call_original
else
expect(controller).to receive(:authorize).and_call_original
end
end
end
Here is an example using shared_examples, the before :suite hook, and metaprogramming that might get at what you need.
RSpec.configure do |config|
config.before(:suite, :type => :controller) do |spec|
it_should_behave_like("authorized_controller")
end
end
and over in spec_helper
shared_examples_for "authorized_controller" do
# expects controller to define index_params, create_params, etc
describe "uses pundit" do
HTTP_VERB = {
:create => :post, :update=>:put, :destroy=>:delete
}
%i{ new create show edit index update destroy}.each do |action|
if controller.responds_to action
it "for #{action}" do
expect(controller).to receive(:policy_scope) if :action == :index
expect(controller).to receive(:authorize) unless :action == :index
send (HTTP_VERB[action]||:get), action
end
end
end
end
end
I'm posting the code for my latest attempt.
Please note that:
You should probably not use this code as it feels overly complex and hacky.
It does not work if authorize or policy_scope is called after an exception happens. Exceptions will occur if a tested action calls Active Record methods such as find, update and destroy without providing them valid parameters. The following code creates fake parameters with empty values. An empty ID is invalid and will result in a ActiveRecord::RecordNotFound exception. Will update the code once I find a solution for this.
spec/controllers/all_controllers_spec.rb
# Test all descendants of this base controller controller
BASE_CONTROLLER = ApplicationController
# To exclude specific actions:
# "TasksController" => [:create, :new, :index]
# "API::V1::PostsController" => [:index]
#
# To exclude entire controllers:
# "TasksController" => nil
# "API::V1::PostsController" => nil
EXCLUDED = {
'TasksController' => nil
}
def expected_auth_method(action)
action == 'index' ? :policy_scope : :authorize
end
def create_fake_params(route)
# Params with non-nil values are required to "No route matches..." error
route.parts.map { |param| [param, ''] }.to_h
end
def extract_action(route)
route.defaults[:action]
end
def extract_http_method(route)
route.constraints[:request_method].to_s.delete("^A-Z")
end
def skip_controller?(controller)
EXCLUDED.key?(controller.name) && EXCLUDED[controller.name].nil?
end
def skip_action?(controller, action)
EXCLUDED.key?(controller.name) &&
EXCLUDED[controller.name].include?(action.to_sym)
end
def testable_controllers
Rails.application.eager_load!
BASE_CONTROLLER.descendants.reject {|controller| skip_controller?(controller)}
end
def testable_routes(controller)
Rails.application.routes.set.select do |route|
route.defaults[:controller] == controller.controller_path &&
!skip_action?(controller, extract_action(route))
end
end
# Do NOT name the loop variable "controller" or it will override the
# "controller" object available within RSpec controller specs.
testable_controllers.each do |tested_controller|
RSpec.describe tested_controller, :focus, type: :controller do
# login_user is implemented in spec/support/controller_macros.rb
login_user
testable_routes(tested_controller).each do |route|
action = extract_action(route)
http_method = extract_http_method(route)
describe "#{http_method} ##{action}" do
it 'enforces authorization' do
expect(controller).to receive(expected_auth_method(action)).and_return(true)
begin
process(action, http_method, create_fake_params(route))
rescue ActiveRecord::RecordNotFound
end
end
end
end
end
end

How to test the AdminController when it has no actions?

My AdminController looks like:
class AdminController < ApplicationController
before_action :check_admin
private
def check_admin
redirect_to 'home/error' unless current_user.admin?
end
end
In my rspec test, how can I test this if there are no route or views?
require 'rails_helper'
RSpec.describe AdminController, type: :controller do
context "with no render_views" do
it "redirects for non-admin users" do
#???expect do
end
end
end
I am assuming that you are using a before_action in your AdminController, even though this controller does not have any actions, so that any controllers that inherit from it will automatically by "admin only".
If so, there are two ways to approach testing this.
1) Don't write a test for check_admin.
Instead, write tests for any controller actions that you define later! For example, if you have the following controller in your application tomorrow:
UsersController < AdminController
def index
#users = User.all
end
end
then you can write the following specs for that controller.
describe UsersController
it 'redirects for non-admins' do
# insert the test you feel like writing here!
end
it 'renders the right template for admin users' do
magical_login_method
get :index
expect(response).to render_template(:index)
end
end
and so on!
2) Call the private method directly
This approach makes me feel a bit icky. Although this defeats the philosophy of public vs private methods, you can call a private method in ruby by using the .send method.
describe AdminController
it 'redirects for non-admins' do
# make an instance of your controller
controller = AdminController.new
# expect the controller to call `redirect_to`
expect(controller).to receive(:redirect_to).with('home/error')
# call the private `check_admin` method
controller.send(:check_admin)
end
end
Some, perhaps many, would argue that this sort of testing is highly intrusive, and may even limit the flexibility of your codebase in the future. I'd recommend approach 1, not because it's lazy, but because it tests things once there's something to test!

Rails: how to write an anonymous spec controller to test authorisation

I have a controller, AdminController, which sets the various authorisation levels for the rest of the CMS. Because there are no controller actions, just methods, I began to research ways to test these against controllers.
The conclusion I came to was that they needed to be tested independently of the controllers they are used in (I want to to steer clear of integration testing if possible, like capybara etc).
I found some articles like this one to help me along.
So far I have written this spec which is failing with the errors below. I am not sure about it to be honest and wanted to here what SO community had to say on what I am trying to achieve.
describe AdminController do
controller do
before_filter :authorize_fixture_uploader!
def index
render text: 'Hello World'
end
end
let(:admin){FactoryGirl.create(:admin)}
describe "authentication" do
before do
sign_in admin
allow(controller).to receive(:current_admin).and_return(admin)
end
describe "authorize_fixture_uploader! helper" do
context "signed in" do
before do
allow(:admin).to receive(:authorize_fixture_uploader!).and_return(false)
get :index
end
it "redirects do admin_home_path" do
expect(response).to redirect_to admin_home_path
end
end
end
end
end
and here is the controller
class AdminController < ApplicationController
before_filter :authenticate_admin!
def authorize_fixture_uploader!
unless current_admin.fixture_uploader?
return redirect_to(admin_home_path)
end
end
end
This test is giving me the error
1) AdminController authentication authorize_fixture_uploader! helper signed in redirects do admin_home_path
Failure/Error: allow(:admin).to receive(:authorize_fixture_uploader?).and_return(false)
TypeError:
can't define singleton
I am worried its because my whole approach to this is wrong. Help would most certainly be appreciated.
Updated thanks to #blelump's answer.
I had a type which was causing the first issue. But Now I am getting error
undefined method `authorize_fixture_uploader?' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_1::Nested_1::Nested_1:0x007f9357857108>
The logic behind this i throwing me a bit. How am I to test these methods independent of the controllers they are used?
You have a typo:
allow(:admin).to receive(:authorize_fixture_uploader!).and_return(false)
Now you're trying to add authorize_fixture_uploader! to Symbol. Just start with controller variable:
allow(controller).to receive(:authorize_fixture_uploader!).and_return(false)
Aside from the poor attention detail highlight by blelump above, the real flaw in my approach was the lack of routes. I found a very useful article from pivotal labs http://pivotallabs.com/adding-routes-for-tests-specs-with-rails-3/ which saved the day.
Read the article, but it essentially boils down to this.
require 'spec_helper'
class InheritsFromAdminController < AdminController
def show
render :text => "foo"
end
end
describe InheritsFromAdminController do
before do
Rails.application.routes.draw do
# add the route that you need in order to test
match '/foo' => "inherits_from_admin#show"
# re-drawing routes means that you lose any routes you defined in routes.rb
# so you have to add those back here if your controller references them
match '/login' => "sessions/new", :as => login
end
end
after do
# be sure to reload routes after the tests run, otherwise all your
# other controller specs will fail
Rails.application.reload_routes!
end
it "requires logged-in users" do
get :show
response.should redirect_to("/login")
end
end

Test controllers without going through routing

I'm trying to test my controller's action chain in isolation. Specifically, I want to ensure my desired behavior is applied to all my controller's actions. For example, test that all my actions require authentication:
context "when not authenticated" do
# single case
describe "GET index" do
it "responds with 401" do
get :index
response.code.should be(401)
end
end
# all of them...
described_class.action_methods.each do |action|
['get', 'put', 'post', 'delete', 'patch'].each do |verb|
describe "#{verb.upcase} #{action}" do
it "responds with 401" do
send verb, action
response.code.should == "401"
end
end
end
end
end
I expected this to work but it doesn't. I get some ActionController::RoutingErrors. This is because some of my routes require params and in some cases I'm not supplying them (like when I call post :create). I get that. But what I don't understand is: why should it matter!?
For these tests routing is a separate concern. I care about my action chains, not my requests (that's what I have routing specs and request specs for). I shouldn't need to concern myself with my route constraints at this level.
So my question: Is there a way to test just the action chain without simulating a request?
EDIT: some research
It looks like routes are being exercised in TestCase#process. Is this necessary?
One work around is to loosen the routing engine's constraints. This doesn't bypass routing, but it does make it easier to work with for testing.
Add something like the following to your specs:
before(:all) do
Rails.application.routes.draw { match ':controller(/:action)' }
end
after(:all) do
Rails.application.reload_routes!
end
While not strictly an answer to the question, it might be a good enough work around.
I'd argue that routing is not a separate concern for controller specs. One reason why is that values are added to the params hash based on what values are passed into the url, and the code in your controller may depend on those values.
Anyway, I'm assuming that you have some kind of authorization method defined in your ApplicationController. Testing each controller individually seems a little redundant. Here's how I'd do it:
require "spec_helper"
describe ApplicationController do
describe "require_current_user" do
ACTIONS_AND_VERBS = [
[:index, :get],
[:show, :get],
[:new, :get],
[:create, :post],
[:edit, :get],
[:update, :put],
[:destroy, :delete],
]
controller do
ACTIONS_AND_VERBS.each do |action, _|
define_method(action) do
end
end
end
ACTIONS_AND_VERBS.each do |action, verb|
describe "#{verb.to_s.upcase} '#{action}'" do
it "should be successful" do
send(verb, action, id: -1)
response.code.should eq("401")
end
end
end
end
end
And in my ApplicationController I'd have something like...
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :require_current_user
def require_current_user
head :unauthorized
end
end
EDIT: If I understand correctly, what we're really testing is that your require_current_user, or whatever equivalent authorization process you want to occur, is working as expected. In that case, we can test just one action, and trust that before_filter works properly.
require "spec_helper"
describe ApplicationController do
describe "require_current_user" do
controller do
def index
end
end
it 'should head unauthorized for unauthorized users' do
get :index
response.code.should eq("401")
end
end
end

Stubbing ApplicationController methods with RSpec

I am trying to stub authentication for a controller in rspec. When I stub the authorize method the test always passed no matter what the value I supply.
Controller:
class FoosController < ApplicationController
before_filter :authorize
...
end
ApplicationController:
class ApplicationController < ActionController::Base
protect_from_forgery
helper_method :current_user
protected
def authorize
return true if current_user
flash[:error] = 'Please login'
redirect_to signin_path
false
end
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
Specs:
# this passes (expected)
it "..." do
controller.stubs(:current_user).returns(User.new)
get :index
response.should be_success
end
# this fails (expected)
it "..." do
controller.stubs(:current_user).returns(nil)
get :index
response.should be_success
end
# this passes (expected)
it "..." do
controller.stubs(:authorize).returns(true)
get :index
response.should be_success
end
# Problem: this passes (unexpected)
it "..." do
controller.stubs(:authorize).returns(false)
get :index
response.should be_success
end
It seems like as soon as I stub :authorize, no matter what value is set, it always passes the before_filter. I thought it might be the protected/helper_method designations, but playing with those didn't change anything.
Why does stubbing :authorize with false cause the before_filter to pass?
I think you need to check WHAT is being rendered.
Looking at your code, if the call-chain does indeed stop when authorize returns false,
then what is going to happen?
There is no redirect, or render call.
So it will be an empty response?
An empty response would still be a 200.
However, depending what version of Rails you're using, its possible in Rails 3.1 that a before_filter returning false no longer stops the chain.
Really, a before_filter that wants to stop the chain should do one of the following
redirect somewhere
render something
raise something
I'll answer your last question Why does stubbing :authorize with false cause the before_filter to pass?
You're stubbing the method authorize, which literally stops all the code inside of it to be called but returns what you are explicitly returning with the stub.
It is working properly, because when you stub current_user to false then the authorize is called completely. I think your lasts tests are not actually testing anything for you but thats just my opinion.

Resources