Minitest - test controller concerns - ruby-on-rails

I try to test my controller concerns using minitest-rails and combining this techniques:
http://ridingtheclutch.com/post/55701769414/testing-controller-concerns-in-rails.
Anonymous controller in Minitest w/ Rails
And i get "no route matches error":
ActionController::UrlGenerationError: No route matches {:action=>"index", :controller=>"fake"}
require "test_helper"
require "warden_mock"
class FakeController < ApplicationController
attr_accessor :request
def initialize(method_name=nil, &method_body)
include MyConcern # method redirect_to_404 placed here
#request = OpenStruct.new # mockup request
#request.env = {}
#request.env['warden'] = WardenMock.new # mockup warden
if method_name and block_given? # dynamically define action for concern methods testing
self.class.send(:define_method, method_name, method_body)
test_routes = Proc.new do
resources :fake
end
Rails.application.routes.eval_block(test_routes)
end
end
end
describe FakeController do # just very simple test
context "just redirect_to_404" do
it "it must redirect to /404" do
#controller = FakeController.new(:index) { redirect_to_404 }
get :index
assert_redirected_to '/404'
end
end
end
I have rails 4.1.5 and minitest 5.4.0

Probably too late for the OP, but I've done it in this way:
require 'test_helper'
class SolrSearchParamsFakeController < ApplicationController
include SolrSearchParams # this is my controller's concern to test
def index
# The concern modify some of the parameters, so I'm saving them in a
# variable for future test inspection, so YMMV here.
#params = params
render nothing: true
end
end
Rails.application.routes.draw do
# Adding a route to the fake controller manually
get 'index' => 'solr_search_params_fake#index'
end
class SolrSearchParamsFakeControllerTest < ActionController::TestCase
def test_index
get :index, search: 'asdf wert'
# finally checking if the parameters were modified as I expect
params = assigns(:params)
assert_equal params[:original_search], 'asdf wert'
end
end
Update
Sorry, but actually this is messing up all my tests that involve route access in some way, as with Rails.application.routes.draw I'm rewriting all the routes for tests and leaving just that solr_search_params_fake#index route.
Not sure how to add instead of rewriting.... a fix would be adding directly to config/routes.rb my test routes with an if Rails.env.test? condition? (yeah, it's a crappy solution, but I'll leave this here in case someone find a better way to do this).

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

with_routing test helper doesn't work for integration tests

Simple example that by all accounts should work:
require 'test_helper'
class FoosController < ApplicationController
def index
render plain: 'something'
end
end
class UsersControllerTest < ActionDispatch::IntegrationTest
def test_some_routing
with_routing do |set|
set.draw do
get '/foos' => 'foos#index'
end
get '/foos'
assert_equal 200, response.status
end
end
end
Instead I'm getting: ActionController::RoutingError: No route matches [GET] "/foos"
What am I doing wrong? This is latest Rails 4.1 btw.
For those who come here seeking a solution to test their controller concerns in isolation (like I did), it may pay to use setup and teardown blocks instead of littering your code with with_routing blocks:
require 'test_helper'
class ConcernedController < ApplicationController
include Concern
def action
render plain: "response", status: :ok
end
end
class ConcernTest < ActionDispatch::IntegrationTest
setup do
#controller = ConcernedController.new
Rails.application.routes.draw do
get "/action" => "concerned#action"
post "/action" => "concerned#action"
end
end
teardown do
Rails.application.reload_routes!
end
test "concern method" do
post "/action"
# Test what the concern does
assert_response :ok
assert_equal "Response", response.body
end
end
Tested against Rails 5.0.2.
Ok, so I'm answering my own questions again. Problem is that with_routing doesn't work at all inside integration tests. Oversight or something, not sure. It only works for controller tests. So here's the work around:
class ActionDispatch::IntegrationTest
def with_routing(&block)
yield ComfortableMexicanSofa::Application.routes
ensure
ComfortableMexicanSofa::Application.routes_reloader.reload!
end
end
Basically you will redefine your application routes within that block and then reload them back from the routes.rb file.
-- edited --
We can have a block with new routes by defining a new method like:
def with_foo_route
Rails.application.routes.draw do
get 'foo' => 'foo#index'
end
yield
Rails.application.routes_reloader.reload!
end
And then execute a get requests to this new route by:
with_foo_route do
get '/foo'
assert_equal 200, status
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

How to test application controller before filter methods in Rails 3?

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.

How do I test helper methods using Shoulda and set the params and request values?

I'm using rails 2.2.2 and wondering how can I set the params values to test my helper methods.
I found some examples to let you run tests with helper methods but it doesn't work for me when I use the request or params value directly in the method.
require 'test_helper'
class ProductHelperTest < Test::Unit::TestCase
include ProductHelper
context 'ProductHelper' do
should 'build the link' do
assert_equal '', build_link
end
end
end
When using the request or params value I'll get an error that the local variable or method is undefined. How would I go about setting the value?
Error from shoulda when using the request value and it will be the same messages when using the params value.
1) Error:
test: ProductHelper should build the link. (ProductHelperTest):
NameError: undefined local variable or method `request` for #<ProductHelperTest:0x33ace6c>
/vendor/rails/actionpack/lib/action_controller/test_process.rb:471:in `method_missing`
/app/helpers/products_helper.rb:14:in `build_link`
./test/unit/product_helper_test.rb:10:in `__bind_1251902384_440357`
/vendor/gems/thoughtbot-shoulda-2.0.5/lib/shoulda/context.rb:254:in `call`
/vendor/gems/thoughtbot-shoulda-2.0.5/lib/shoulda/context.rb:254:in `test: ProductHelper should build the link. `
/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `__send__`
/vendor/rails/activesupport/lib/active_support/testing/setup_and_teardown.rb:94:in `run`
I guess you have to mock out calls to request and params using mocha or by defining mock objects in your test:
# Assuming ProductHelper implementation
module ProductHelper
def build_link
"#{request.path}?#{params[:id]}"
end
end
class ProductHelperTest < Test::Unit::TestCase
include ProductHelper
# mock by defining a method
def params
{ :controller => "test", :id => "23" }
end
# mock request.path using `mocha` # => "my_url"
def request
mock(:path => "my_url")
end
context 'ProductHelper' do
should 'build the link' do
assert_equal 'my_url?23', build_link
end
end
end
I hope this helps :)
As a note, if you are using Rails 2.3.x or anything which uses ActionView::TestCase - then all you really need to do is just have a private params method defined in your test.
e.g
require 'test_helper'
class BookmarksHelperTest < ActionView::TestCase
context "with an applied filter of my bookmarks" do
setup do
expects(:applied_filters).returns({:my_bookmarks => true})
end
should "not be able to see it when other filters are called using my_bookmarks_filter" do
assert_equal other_filters(:my_bookmarks), {}
end
end
private
def params
{}
end
end
You could even do one better by defining params as a method inside of ActionView::TestCase
This also works:
controller.params = {:filter => {:user_id => 1}
Here is an example:
require 'test_helper'
class BookmarksHelperTest < ActionView::TestCase
def test_some_helper_method
controller.params = {:filter => {:user_id => 1}
#... Rest of your test
end
end

Resources