Skip to content

Latest commit

 

History

History
417 lines (303 loc) · 10.4 KB

testing.md

File metadata and controls

417 lines (303 loc) · 10.4 KB

Testing

Authorization is one of the crucial parts of your application. Hence, it should be thoroughly tested (that is the place where 100% coverage makes sense).

When you use policies for authorization, it is possible to split testing into the following parts:

  • Test the policy class itself
  • Test that the required authorization is performed within your authorization layer (controller, channel, etc.)
  • Test that the required scoping has been applied.

Testing policies

You can test policies as plain-old Ruby classes, no special tooling is required.

Consider an RSpec example:

describe PostPolicy do
  let(:user) { build_stubbed(:user) }
  let(:post) { build_stubbed(:post) }

  let(:policy) { described_class.new(post, user: user) }

  describe "#update?" do
    subject { policy.apply(:update?) }

    it "returns false when the user is not admin nor author" do
      is_expected.to eq false
    end

    context "when the user is admin" do
      let(:user) { build_stubbed(:user, :admin) }

      it { is_expected.to eq true }
    end

    context "when the user is an author" do
      let(:post) { build_stubbed(:post, user: user) }

      it { is_expected.to eq true }
    end
  end
end

And we provide a #be_an_alias_of RSpec matcher for testing authorization rule aliases:

describe PostPolicy do
  let(:user) { build_stubbed(:user) }
  let(:post) { build_stubbed(:post) }

  let(:policy) { described_class.new(post, user: user) }

  describe "#show?" do
    it "is an alias of :index? authorization rule" do
      expect(:show?).to be_an_alias_of(policy, :index?)
    end
  end
end

RSpec DSL

We also provide a simple RSpec DSL which aims to reduce the boilerplate when writing policies specs.

Example:

# Add this to your spec_helper.rb / rails_helper.rb
require "action_policy/rspec/dsl"

describe PostPolicy do
  let(:user) { build_stubbed :user }
  # `record` must be defined – it is the authorization target
  let(:record) { build_stubbed :post, draft: false }

  # `context` is the authorization context
  let(:context) { {user: user} }

  # `describe_rule` is a combination of
  # `describe` and `subject { ... }` (returns the result of
  # applying the rule to the record)
  describe_rule :show? do
    # `succeed` is `context` + `specify`, which checks
    # that the result of application is successful
    succeed "when post is published"

    # `failed` is `context` + `specify`, which checks
    # that the result of application wasn't successful
    failed "when post is draft" do
      before { post.draft = false }

      succeed "when user is a manager" do
        before { user.role = "manager" }
      end
    end
  end
end

If test failed the exception message includes the result and failure reasons (if any):

1) PostPolicy#show? when post is draft
Failure/Error:  ...

Expected to fail but succeed:
<PostPolicy#show?: true (reasons: ...)>

If you have debugging utils installed the message also includes the annotated source code of the policy rule:

1) UserPolicy#manage? when post is draft
Failure/Error:  ...

Expected to fail but succeed:
<PostPolicy#show?: true (reasons: ...)>
↳ user.admin? #=> true
OR
!record.draft? #=> false

NOTE: DSL for focusing or skipping examples and groups is also available (e.g. xdescribe_rule, fsucceed, etc.).

NOTE: the DSL is included only to example with the tag type: :policy or in the spec/policies folder. If you want to add this DSL to other examples, add include ActionPolicy::RSpec::PolicyExampleGroup.

Testing scopes

Active Record relation example

There is no single rule on how to test scopes, 'cause it depends on the nature of the scope.

Here's an example of RSpec tests for Active Record scoping rules:

describe PostPolicy do
  describe "relation scope" do
    let(:user) { build_stubbed :user }
    let(:context) { {user: user} }

    # Feel free to replace with `before_all` from `test-prof`:
    # https://test-prof.evilmartians.io/#/before_all
    before do
      create(:post, name: "A")
      create(:post, name: "B", draft: true)
    end

    let(:target) do
      # We want to make sure that only the records created
      # for this test are affected, and they have a deterministic order
      Post.where(name: %w[A B]).order(name: :asc)
    end

    subject { policy.apply_scope(target, type: :active_record_relation).pluck(:name) }

    context "as user" do
      it { is_expected.to eq(%w[A]) }
    end

    context "as manager" do
      before { user.update!(role: :manager) }

      it { is_expected.to eq(%w[A B]) }
    end

    context "as banned user" do
      before { user.update!(banned: true) }

      it { is_expected.to be_empty }
    end
  end
end

Action Controller params example

Here's an example of RSpec tests for Action Controller parameters scoping rules:

describe PostPolicy do
  describe "params scope" do
    let(:user) { build_stubbed :user }
    let(:context) { {user: user} }

    let(:params) { {name: "a", password: "b"} }
    let(:target) { ActionController::Parameters.new(params) }

    # it's easier to asses the hash representation, not the AC::Params object
    subject { policy.apply_scope(target, type: :action_controller_params).to_h }

    context "as user" do
      it { is_expected.to eq({name: "a"}) }
    end

    context "as manager" do
      before { user.update!(role: :manager) }

      it { is_expected.to eq({name: "a", password: "b"}) }
    end
  end
end

Testing authorization

To test the act of authorization you have to make sure that the authorize! method is called with the appropriate arguments.

Action Policy provides tools for such kind of testing for Minitest and RSpec.

Minitest

Include ActionPolicy::TestHelper to your test class and you'll be able to use assert_authorized_to assertion:

# in your controller
class PostsController < ApplicationController
  def update
    @post = Post.find(params[:id])
    authorize! @post
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end
end

# in your test
require "action_policy/test_helper"

class PostsControllerTest < ActionDispatch::IntegrationTest
  include ActionPolicy::TestHelper

  test "update is authorized" do
    sign_in users(:john)

    post = posts(:example)

    assert_authorized_to(:update?, post, with: PostPolicy) do
      patch :update, id: post.id, name: "Bob"
    end
  end
end

You can omit the policy (then it would be inferred from the target):

assert_authorized_to(:update?, post) do
  patch :update, id: post.id, name: "Bob"
end

RSpec

Add the following to your rails_helper.rb (or spec_helper.rb):

require "action_policy/rspec"

Now you can use be_authorized_to matcher:

describe PostsController do
  subject { patch :update, id: post.id, params: params }

  it "is authorized" do
    expect { subject }.to be_authorized_to(:update?, post)
      .with(PostPolicy)
  end
end

If you omit .with(PostPolicy) then the inferred policy for the target (post) would be used.

RSpec composed matchers are available as target:

expect { subject }.to be_authorized_to(:show?, an_instance_of(Post))

Testing scoping

Action Policy provides a way to test that a correct scoping has been applied during the code execution.

For example, you can test that in your #index action the correct scoping is used:

class UsersController < ApplicationController
  def index
    @user = authorized(User.all)
  end
end

Minitest

Include ActionPolicy::TestHelper to your test class and you'll be able to use assert_have_authorized_scope assertion:

# in your test
require "action_policy/test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest
  include ActionPolicy::TestHelper

  test "index has authorized scope" do
    sign_in users(:john)

    assert_have_authorized_scope(type: :active_record_relation, with: UserPolicy) do
      get :index
    end
  end
end

You can also specify as and scope_options options.

NOTE: both type and with params are required.

It's not possible to test that a scoped has been applied to a particular target but we provide a way to perform additional assertions against the matching target (if the assertion didn't fail):

test "index has authorized scope" do
  sign_in users(:john)

  assert_have_authorized_scope(type: :active_record_relation, with: UserPolicy) do
    get :index
  end.with_target do |target|
    # target is a object passed to `authorized` call
    assert_equal User.all, target
  end
end

RSpec

Add the following to your rails_helper.rb (or spec_helper.rb):

require "action_policy/rspec"

Now you can use have_authorized_scope matcher:

describe UsersController do
  subject { get :index }

  it "has authorized scope" do
    expect { subject }.to have_authorized_scope(:active_record_relation)
      .with(PostPolicy)
  end
end

You can also add .as(:named_scope) and with_scope_options(options_hash) options.

RSpec composed matchers are available as scope options:

expect { subject }.to have_authorized_scope(:scope)
  .with_scope_options(matching(with_deleted: a_falsey_value))

You can use the with_target modifier to run additional expectations against the matching target (if the matcher didn't fail):

expect { subject }.to have_authorized_scope(:scope)
  .with_scope_options(matching(with_deleted: a_falsey_value))
  .with_target { |target|
    expect(target).to eq(User.all)
  }

Testing views

When you test views that call policies methods as allowed_to?, your may have Missing policy authorization context: user error. You may need to stub current_user to resolve the issue.

Consider an RSpec example:

describe "users/index.html.slim" do
  let(:user) { build_stubbed :user }
  let(:users) { create_list(:user, 2) }

  before do
    allow(controller).to receive(:current_user).and_return(user)

    assign :users, users
    render
  end

  describe "displays user#index correctly" do
    it { expect(rendered).to have_link(users.first.email, href: edit_user_path(users.first)) }
  end
end

Linting with RuboCop RSpec

When you lint your RSpec spec files with rubocop-rspec, it will fail to detect RSpec aliases that Action Policy defines. Make sure to use rubocop-rspec 2.0 or newer and add the following to your .rubocop.yml:

inherit_gem:
  action_policy: config/rubocop-rspec.yml