5

I am using Pundit for authorization on my Rails app and I am unit testing my requests. I have already successfully tested the policy but now I want to verify that my request is using that policy. I want to do it in a generic way, that I can use in ANY request spec (regardless of the controller, the action, and the policy). To be honest at this point I would be conformed with a way of saying "expect any policy to receive authorize", generic to all requests.

For the index action (that uses the policy scope) it was easy:

In the request describe I say

RSpec.describe 'GET /companies', type: :request do
  include_context 'invokes policy scope'
end

And then I defined the shared context as:

RSpec.shared_context 'invokes policy scope', shared_context: :metadata do
  before do
    expect(Pundit).to receive(:policy_scope!) do |user_context, relation|
      expect(user_context.user).to eq(user)
      expect(user_context.current_group).to eq(group)

      relation
    end
  end
end

But how do I do the same for the authorize method. I don't know which concrete policy will be the one receiving it, neither which is the concrete controller that will invoke the authorize.

I don't understand how pundit does not provide a custom matcher to verify a certain request is actually invoking a given policy, or to "simulate" authorized/unauthorized scenarios so I test that my request returns the correct status code.

Any ideas?

Leticia Esperon
  • 2,499
  • 1
  • 18
  • 40

3 Answers3

2

when you include Pundit module in your controllers then the policy_scope, and the autherize methods become available inside your controller as public methods.

so when you send an get or post request to your controller rails behind the scenes creates an instance of the controller ControllerClass.new so what you need is to mock the authorize method on the instantiated controller object.

to mock the method on that object you need to know or have that object in your tests which is not possible. but hopefully you can mock a public method on any class instance in advance.

so to mock the authorize method you will write :

expect_any_instance_of(described_class).to receive(:authorize).with(any_params_you_want).and_return(nil)

expect_any_instance_of is a method provided by rspec to mock any instantiated object of a certain class. click here to learn more

so no need to reference Pundit class in your tests anymore. actually that would create a dependency in our tests on the class name of the gem, which you do not need since you can test both methods as explained above.

Abdullah Fadhel
  • 294
  • 2
  • 9
1

First, you need to test your pundit policy alone. Second, to pass pundit policy, you can stub object that satisfy pundit policy

Bùi Nhật Duy
  • 323
  • 2
  • 9
  • Thank you for your answe but this does not answer my question. I said I had already successfully tested the policy alone. The problem is stubbing the pundit policy generically, since I don't know WHICH is the policy that will get the request since I'm reusing the example for multiple requests specs. – Leticia Esperon Jan 16 '20 at 14:44
1

So, I have a few comments...

Expectations (expect statements) should be inside an example (it) block and not inside a before block. The kind of thing that goes in a before block are allow statements (e.g., allow(ClassX).to receive(:method) { object }), data modification that can't be done in a test variable declaration, or request triggers. See http://www.betterspecs.org/ for some examples. TL;DR, that shared context is not an appropriate way to test.

The way to test that policy_scope is being called with specific params is:

# You can put something generic like this in a shared context and then
# define 'params' and 'scoped_result' as let vars in the specs that include
# the shared context 
let(:request)       { get '/companies' }
let(:params)        { user_context or whatever }
let(:scoped_result) { relation }
# By using abstract variable names here, we make this reusable

it 'calls policy scope' do
  expect(Pundit).to receive(:policy_scope!).with(params)
  request
end

it 'scopes result' do 
  expect(Pundit.policy_scope!(params)).to eq(scoped_result) 
end

To mock it and stub its responses, you would do:

before do
  # This ensures Pundit.policy_scope!(context) always returns scoped_result
  allow(Pundit).to receive(:policy_scope!).with(context) { scoped_result }
end

...but these are extremely bad/brittle tests, especially as it pertains to a request spec. Your Pundit policies should already be tested in the policy spec files (see https://github.com/varvet/pundit#rspec), so what you really want to be doing is testing that your endpoint returns the correct output (scoped response) given a certain input (authenticated policy-managed-object). It's a bad idea to try and override the functionality of Pundit by mocking the response because your endpoint's specs will continue passing if you make a breaking change to your policy code. What you want to do here is set up test variables to suit the circumstances that would result in a successful request, but make sure everything is generic so it can be reused. For request specs, you could do something like:

# Shared context stuff
let(:json)    { JSON.parse(response.body) }
let(:headers) { ...define the headers to use across requests...}

before { request }

shared_examples_for 'success' do
  it { expect(response).to have_http_status(:success) }
  it { expect(json).to eq(expected) } # or something
end


# User spec that includes shared context
include_context 'above stuff'

let(:request) { get '/companies', params: params, headers: headers }
let(:params)  { { user_id: user.id } } # or something
let!(:admin_thing) { 
  ...something that should be excluded by the pundit policy used by endpoint... 
}

context 'restricted' do
  let!(:user)    { create :user, :restricted }
  let(:expected) { ...stuff scoped to restricted user... }

  it_behaves_like 'success'
end

context 'manager' do
  let!(:user)    { create :user, :manager }
  let(:expected) { ...stuff scoped to manager user... }

  it_behaves_like 'success'
end

context 'superuser' do
  let!(:user)   { create :user, :superuser }
  let(expected) { ...unscoped stuff visible to super user... }

  it_behaves_like 'success'
end

Notice that, at the higher level (shared context), the name and function are generic. At the lower level (the spec declaring permissioned users), the spec translates the abstract names into values specific to the endpoint being tested. The spec also creates an additional object that should not be returned by the policy scope (essentially testing the scoping by confirming that this object is excluded from the result). Hope that helps.

Allison
  • 1,925
  • 1
  • 18
  • 25
  • Thank you for you detailed answer. I understand the part where you say `It's a bad idea to try and override the functionality of Pundit by mocking the response because your endpoint's specs will continue passing if you make a breaking change to your policy code.`, but I said I already unit tested the policy, now I just want to ensure that the request controller is actually taking its output into account. So I wanted to avoid having to set up a context that would actually make for an authorized case since I am trying to unit test the controller since the policy was already unit tested. – Leticia Esperon Jan 29 '20 at 13:29
  • I also mentioned that I know how to mock the policy scope, but not the authorize method in a generic way. However, your answer was still useful – Leticia Esperon Jan 29 '20 at 13:30
  • Again, I still strongly advise against what you're trying to do; it is brittle and not consistent with best practices (I would not approve a PR that did that). If somebody changes the functionality of the pundit policy and updates the pundit policy's specs, your controller specs will continue to pass even though the functionality of the policy scope has changed. You should be creating a reusable shared context that sets up the different policy-managed objects and include that to set up the specs that rely on that policy. – Allison Jan 30 '20 at 20:01