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
Related
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
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!
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
I have a before_filter on my ApplicationController class and I want to write a test for it? Where should I write this test into? I do not want to go into every subclass controller test file and repeat the test about this filter.
Hence, what is the recommended way to test ApplicationController before_filters?
Note that I am using Rails 3.2.1 with minitest.
My case is slightly different than yours, but I needed to do something similar to test authentication across the site (with Devise). Here's how I did it:
# application_controller.rb
class ApplicationController < ActionController::Base
before_filter :authenticate_user!
end
# application_controller_test.rb
require 'test_helper'
class TestableController < ApplicationController
def show
render :text => 'rendered content here', :status => 200
end
end
class ApplicationControllerTest < ActionController::TestCase
tests TestableController
context "anonymous user" do
setup do
get :show
end
should redirect_to '/users/sign_in'
end
end
If there's specific controllers that need to skip the before filter I'll have a test to make sure they skip it in the specific controller's tests. This isn't quite your situation as I'm interested in the effect of the method, not just knowing it was invoked, but I thought I'd share in case you found it useful.
Improving on #bmaddy answser, you do need to setup routing for the specs to run.
Here is a rails 5 working example:
require 'test_helper'
class BaseController < ApplicationController
def index
render nothing: true
end
end
class BaseControllerTest < ActionDispatch::IntegrationTest
test 'redirects if user is not logedin' do
Rails.application.routes.draw do
get 'base' => 'base#index'
end
get '/base'
assert_equal 302, status
assert_redirected_to 'http://somewhere.com'
Rails.application.routes_reloader.reload!
end
test 'returns success if user is loggedin' do
Rails.application.routes.draw do
get 'base' => 'base#index'
end
mock_auth!
get '/base'
assert_equal 200, status
Rails.application.routes_reloader.reload!
end
end
I now believe that I have to have all my controllers tests test about the before_filter existence and that this filter works as expected. This is because, I cannot know whether a controller uses a skip_before_filter when it shouldn't.
Hence, I decided to use mock (#controller.expects(:before_filter_method)) to make sure that the filter is called. So, for example, in a index action I write in my test:
test "get index calls the before filter method" do
#controller.expects(:before_filter_method)
# fire
get :index
end
This will make sure that my controller calls before_filter_method on the particular action. I have to do this on all my actions tests.
If anyone else has a better solution, let me know.
Usually when I want something like this I just test the expected behaviour without taking into account that this particular behaviour may be implemented in a filter and not in a method per se. So for the following simple scenario :
class Controller < ApplicationController
before_filter :load_resource, :only => [:show, :edit]
def show
end
def edit
end
def index
end
#########
protected
#########
def load_resource
#resource = Model.find(params[:id])
end
end
I would simple test that #show and #edit assign the #resource thing. This works for simple scenarios pretty much ok. If the filter is applied to a lot of actions/controllers then you can extract the testing code and reuse it amongst the tests.
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