Back to Repositories

Testing Controller Resource Authorization and Loading in CanCan

This test suite examines the CanCan::ControllerResource functionality, focusing on resource loading, authorization, and parameter handling in Ruby on Rails applications. It verifies the proper loading of resources into controller instance variables and authorization checks across different controller actions and namespaces.

Test Coverage Overview

The test suite provides comprehensive coverage of CanCan’s controller resource handling functionality.

Key areas tested include:
  • Resource loading with and without params[:id]
  • Namespaced controller and model handling
  • Parent resource loading and authorization
  • Collection loading for index actions
  • Custom action handling
  • Nested resource authorization

Implementation Analysis

The testing approach uses RSpec to verify CanCan’s controller integration patterns. Tests employ mock objects and stubs to isolate controller resource behavior, with particular attention to parameter handling and instance variable management.

Key patterns include:
  • Before block setup for controller and ability mocking
  • Nested describe blocks for logical test organization
  • Expectation verification for instance variables
  • Exception handling verification for authorization failures

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • Mock object framework for controller stubbing
  • HashWithIndifferentAccess for parameter simulation
  • Custom test helpers for ability setup
  • Nested module support for namespace testing

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices for Rails authorization systems.

Notable practices include:
  • Thorough edge case coverage
  • Isolation of controller logic
  • Consistent test structure and organization
  • Clear test descriptions
  • Comprehensive authorization scenario testing

ryanb/cancan

spec/cancan/controller_resource_spec.rb

            
require "spec_helper"

describe CanCan::ControllerResource do
  before(:each) do
    @params = HashWithIndifferentAccess.new(:controller => "projects")
    @controller_class = Class.new
    @controller = @controller_class.new
    @ability = Ability.new(nil)
    stub(@controller).params { @params }
    stub(@controller).current_ability { @ability }
    stub(@controller_class).cancan_skipper { {:authorize => {}, :load => {}} }
  end

  it "should load the resource into an instance variable if params[:id] is specified" do
    project = Project.create!
    @params.merge!(:action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should not load resource into an instance variable if already set" do
    @params.merge!(:action => "show", :id => "123")
    @controller.instance_variable_set(:@project, :some_project)
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should properly load resource for namespaced controller" do
    project = Project.create!
    @params.merge!(:controller => "admin/projects", :action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should attempt to load a resource with the same namespace as the controller when using :: for namespace" do
    module MyEngine
      class Project < ::Project; end
    end

    project = MyEngine::Project.create!
    @params.merge!(:controller => "MyEngine::ProjectsController", :action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  # Rails includes namespace in params, see issue #349
  it "should create through the namespaced params" do
    module MyEngine
      class Project < ::Project; end
    end

    @params.merge!(:controller => "MyEngine::ProjectsController", :action => "create", :my_engine_project => {:name => "foobar"})
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "foobar"
  end

  it "should properly load resource for namespaced controller when using '::' for namespace" do
    project = Project.create!
    @params.merge!(:controller => "Admin::ProjectsController", :action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "has the specified nested resource_class when using / for namespace" do
    module Admin
      class Dashboard; end
    end
    @ability.can(:index, "admin/dashboard")
    @params.merge!(:controller => "admin/dashboard", :action => "index")
    resource = CanCan::ControllerResource.new(@controller, :authorize => true)
    resource.send(:resource_class).should == Admin::Dashboard
  end

  it "should build a new resource with hash if params[:id] is not specified" do
    @params.merge!(:action => "create", :project => {:name => "foobar"})
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "foobar"
  end

  it "should build a new resource for namespaced model with hash if params[:id] is not specified" do
    @params.merge!(:action => "create", 'sub_project' => {:name => "foobar"})
    resource = CanCan::ControllerResource.new(@controller, :class => ::Sub::Project)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "foobar"
  end

  it "should build a new resource for namespaced controller and namespaced model with hash if params[:id] is not specified" do
    @params.merge!(:controller => "Admin::SubProjectsController", :action => "create", 'sub_project' => {:name => "foobar"})
    resource = CanCan::ControllerResource.new(@controller, :class => Project)
    resource.load_resource
    @controller.instance_variable_get(:@sub_project).name.should == "foobar"
  end

  it "should build a new resource with attributes from current ability" do
    @params.merge!(:action => "new")
    @ability.can(:create, Project, :name => "from conditions")
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "from conditions"
  end

  it "should override initial attributes with params" do
    @params.merge!(:action => "new", :project => {:name => "from params"})
    @ability.can(:create, Project, :name => "from conditions")
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "from params"
  end

  it "should build a collection when on index action when class responds to accessible_by" do
    stub(Project).accessible_by(@ability, :index) { :found_projects }
    @params[:action] = "index"
    resource = CanCan::ControllerResource.new(@controller, :project)
    resource.load_resource
    @controller.instance_variable_get(:@project).should be_nil
    @controller.instance_variable_get(:@projects).should == :found_projects
  end

  it "should not build a collection when on index action when class does not respond to accessible_by" do
    @params[:action] = "index"
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should be_nil
    @controller.instance_variable_defined?(:@projects).should be_false
  end

  it "should not use accessible_by when defining abilities through a block" do
    stub(Project).accessible_by(@ability) { :found_projects }
    @params[:action] = "index"
    @ability.can(:read, Project) { |p| false }
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should be_nil
    @controller.instance_variable_defined?(:@projects).should be_false
  end

  it "should not authorize single resource in collection action" do
    @params[:action] = "index"
    @controller.instance_variable_set(:@project, :some_project)
    stub(@controller).authorize!(:index, Project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller)
    lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should authorize parent resource in collection action" do
    @params[:action] = "index"
    @controller.instance_variable_set(:@category, :some_category)
    stub(@controller).authorize!(:show, :some_category) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller, :category, :parent => true)
    lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should perform authorization using controller action and loaded model" do
    @params.merge!(:action => "show", :id => "123")
    @controller.instance_variable_set(:@project, :some_project)
    stub(@controller).authorize!(:show, :some_project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller)
    lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should perform authorization using controller action and non loaded model" do
    @params.merge!(:action => "show", :id => "123")
    stub(@controller).authorize!(:show, Project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller)
    lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should call load_resource and authorize_resource for load_and_authorize_resource" do
    @params.merge!(:action => "show", :id => "123")
    resource = CanCan::ControllerResource.new(@controller)
    mock(resource).load_resource
    mock(resource).authorize_resource
    resource.load_and_authorize_resource
  end

  it "should not build a single resource when on custom collection action even with id" do
    @params.merge!(:action => "sort", :id => "123")
    resource = CanCan::ControllerResource.new(@controller, :collection => [:sort, :list])
    resource.load_resource
    @controller.instance_variable_get(:@project).should be_nil
  end

  it "should load a collection resource when on custom action with no id param" do
    stub(Project).accessible_by(@ability, :sort) { :found_projects }
    @params[:action] = "sort"
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should be_nil
    @controller.instance_variable_get(:@projects).should == :found_projects
  end

  it "should build a resource when on custom new action even when params[:id] exists" do
    @params.merge!(:action => "build", :id => "123")
    stub(Project).new { :some_project }
    resource = CanCan::ControllerResource.new(@controller, :new => :build)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should not try to load resource for other action if params[:id] is undefined" do
    @params[:action] = "list"
    resource = CanCan::ControllerResource.new(@controller)
    resource.load_resource
    @controller.instance_variable_get(:@project).should be_nil
  end

  it "should be a parent resource when name is provided which doesn't match controller" do
    resource = CanCan::ControllerResource.new(@controller, :category)
    resource.should be_parent
  end

  it "should not be a parent resource when name is provided which matches controller" do
    resource = CanCan::ControllerResource.new(@controller, :project)
    resource.should_not be_parent
  end

  it "should be parent if specified in options" do
    resource = CanCan::ControllerResource.new(@controller, :project, {:parent => true})
    resource.should be_parent
  end

  it "should not be parent if specified in options" do
    resource = CanCan::ControllerResource.new(@controller, :category, {:parent => false})
    resource.should_not be_parent
  end

  it "should have the specified resource_class if 'name' is passed to load_resource" do
    class Section
    end

    resource = CanCan::ControllerResource.new(@controller, :section)
    resource.send(:resource_class).should == Section
  end

  it "should load parent resource through proper id parameter" do
    project = Project.create!
    @params.merge!(:controller => "categories", :action => "index", :project_id => project.id)
    resource = CanCan::ControllerResource.new(@controller, :project)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should load resource through the association of another parent resource using instance variable" do
    @params.merge!(:action => "show", :id => "123")
    category = Object.new
    @controller.instance_variable_set(:@category, category)
    stub(category).projects.stub!.find("123") { :some_project }
    resource = CanCan::ControllerResource.new(@controller, :through => :category)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should load resource through the custom association name" do
    @params.merge!(:action => "show", :id => "123")
    category = Object.new
    @controller.instance_variable_set(:@category, category)
    stub(category).custom_projects.stub!.find("123") { :some_project }
    resource = CanCan::ControllerResource.new(@controller, :through => :category, :through_association => :custom_projects)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should load resource through the association of another parent resource using method" do
    @params.merge!(:action => "show", :id => "123")
    category = Object.new
    stub(@controller).category { category }
    stub(category).projects.stub!.find("123") { :some_project }
    resource = CanCan::ControllerResource.new(@controller, :through => :category)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should not load through parent resource if instance isn't loaded when shallow" do
    project = Project.create!
    @params.merge!(:action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller, :through => :category, :shallow => true)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should raise AccessDenied when attempting to load resource through nil" do
    project = Project.create!
    @params.merge!(:action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller, :through => :category)
    lambda {
      resource.load_resource
    }.should raise_error(CanCan::AccessDenied) { |exception|
      exception.action.should == :show
      exception.subject.should == Project
    }
    @controller.instance_variable_get(:@project).should be_nil
  end

  it "should authorize nested resource through parent association on index action" do
    @params.merge!(:action => "index")
    category = Object.new
    @controller.instance_variable_set(:@category, category)
    stub(@controller).authorize!(:index, category => Project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller, :through => :category)
    lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should load through first matching if multiple are given" do
    @params.merge!(:action => "show", :id => "123")
    category = Object.new
    @controller.instance_variable_set(:@category, category)
    stub(category).projects.stub!.find("123") { :some_project }
    resource = CanCan::ControllerResource.new(@controller, :through => [:category, :user])
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should find record through has_one association with :singleton option without id param" do
    @params.merge!(:action => "show", :id => nil)
    category = Object.new
    @controller.instance_variable_set(:@category, category)
    stub(category).project { :some_project }
    resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == :some_project
  end

  it "should not build record through has_one association with :singleton option because it can cause it to delete it in the database" do
    @params.merge!(:action => "create", :project => {:name => "foobar"})
    category = Category.new
    @controller.instance_variable_set(:@category, category)
    resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "foobar"
    @controller.instance_variable_get(:@project).category.should == category
  end

  it "should find record through has_one association with :singleton and :shallow options" do
    project = Project.create!
    @params.merge!(:action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true, :shallow => true)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should build record through has_one association with :singleton and :shallow options" do
    @params.merge!(:action => "create", :project => {:name => "foobar"})
    resource = CanCan::ControllerResource.new(@controller, :through => :category, :singleton => true, :shallow => true)
    resource.load_resource
    @controller.instance_variable_get(:@project).name.should == "foobar"
  end

  it "should only authorize :show action on parent resource" do
    project = Project.create!
    @params.merge!(:action => "new", :project_id => project.id)
    stub(@controller).authorize!(:show, project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller, :project, :parent => true)
    lambda { resource.load_and_authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should load the model using a custom class" do
    project = Project.create!
    @params.merge!(:action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller, :class => Project)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should load the model using a custom namespaced class" do
    project = Sub::Project.create!
    @params.merge!(:action => "show", :id => project.id)
    resource = CanCan::ControllerResource.new(@controller, :class => ::Sub::Project)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should authorize based on resource name if class is false" do
    @params.merge!(:action => "show", :id => "123")
    stub(@controller).authorize!(:show, :project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller, :class => false)
    lambda { resource.authorize_resource }.should raise_error(CanCan::AccessDenied)
  end

  it "should load and authorize using custom instance name" do
    project = Project.create!
    @params.merge!(:action => "show", :id => project.id)
    stub(@controller).authorize!(:show, project) { raise CanCan::AccessDenied }
    resource = CanCan::ControllerResource.new(@controller, :instance_name => :custom_project)
    lambda { resource.load_and_authorize_resource }.should raise_error(CanCan::AccessDenied)
    @controller.instance_variable_get(:@custom_project).should == project
  end

  it "should load resource using custom ID param" do
    project = Project.create!
    @params.merge!(:action => "show", :the_project => project.id)
    resource = CanCan::ControllerResource.new(@controller, :id_param => :the_project)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  # CVE-2012-5664
  it "should always convert id param to string" do
    @params.merge!(:action => "show", :the_project => { :malicious => "I am" })
    resource = CanCan::ControllerResource.new(@controller, :id_param => :the_project)
    resource.send(:id_param).class.should == String
  end

  it "should load resource using custom find_by attribute" do
    project = Project.create!(:name => "foo")
    @params.merge!(:action => "show", :id => "foo")
    resource = CanCan::ControllerResource.new(@controller, :find_by => :name)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should allow full find method to be passed into find_by option" do
    project = Project.create!(:name => "foo")
    @params.merge!(:action => "show", :id => "foo")
    resource = CanCan::ControllerResource.new(@controller, :find_by => :find_by_name)
    resource.load_resource
    @controller.instance_variable_get(:@project).should == project
  end

  it "should raise ImplementationRemoved when adding :name option" do
    lambda {
      CanCan::ControllerResource.new(@controller, :name => :foo)
    }.should raise_error(CanCan::ImplementationRemoved)
  end

  it "should raise ImplementationRemoved exception when specifying :resource option since it is no longer used" do
    lambda {
      CanCan::ControllerResource.new(@controller, :resource => Project)
    }.should raise_error(CanCan::ImplementationRemoved)
  end

  it "should raise ImplementationRemoved exception when passing :nested option" do
    lambda {
      CanCan::ControllerResource.new(@controller, :nested => :project)
    }.should raise_error(CanCan::ImplementationRemoved)
  end

  it "should skip resource behavior for :only actions in array" do
    stub(@controller_class).cancan_skipper { {:load => {nil => {:only => [:index, :show]}}} }
    @params.merge!(:action => "index")
    CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
    CanCan::ControllerResource.new(@controller, :some_resource).skip?(:load).should be_false
    @params.merge!(:action => "show")
    CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
    @params.merge!(:action => "other_action")
    CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
  end

  it "should skip resource behavior for :only one action on resource" do
    stub(@controller_class).cancan_skipper { {:authorize => {:project => {:only => :index}}} }
    @params.merge!(:action => "index")
    CanCan::ControllerResource.new(@controller).skip?(:authorize).should be_false
    CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_true
    @params.merge!(:action => "other_action")
    CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_false
  end

  it "should skip resource behavior :except actions in array" do
    stub(@controller_class).cancan_skipper { {:load => {nil => {:except => [:index, :show]}}} }
    @params.merge!(:action => "index")
    CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
    @params.merge!(:action => "show")
    CanCan::ControllerResource.new(@controller).skip?(:load).should be_false
    @params.merge!(:action => "other_action")
    CanCan::ControllerResource.new(@controller).skip?(:load).should be_true
    CanCan::ControllerResource.new(@controller, :some_resource).skip?(:load).should be_false
  end

  it "should skip resource behavior :except one action on resource" do
    stub(@controller_class).cancan_skipper { {:authorize => {:project => {:except => :index}}} }
    @params.merge!(:action => "index")
    CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_false
    @params.merge!(:action => "other_action")
    CanCan::ControllerResource.new(@controller).skip?(:authorize).should be_false
    CanCan::ControllerResource.new(@controller, :project).skip?(:authorize).should be_true
  end

  it "should skip loading and authorization" do
    stub(@controller_class).cancan_skipper { {:authorize => {nil => {}}, :load => {nil => {}}} }
    @params.merge!(:action => "new")
    resource = CanCan::ControllerResource.new(@controller)
    lambda { resource.load_and_authorize_resource }.should_not raise_error
    @controller.instance_variable_get(:@project).should be_nil
  end
end