Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added with_context qualifier to have_authorized_scope matcher #260

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ class UsersController < ApplicationController
def index
@user = authorized(User.all)
end

def for_user
user = User.find(params[:id])
authorized_scope(User.all, context: {user:})
end
end
```

Expand Down Expand Up @@ -408,6 +413,14 @@ expect { subject }.to have_authorized_scope(:scope)
}
```

Also can use the `with_context` options:

```ruby
expect { get :for_user, params: {id: user.id} }.to have_authorized_scope(:scope)
.with_scope_options(matching(with_deleted: a_falsey_value))
.with_context(a_hash_including(user:))
```

## Testing views

When you test views that call policies methods as `allowed_to?`, your may have `Missing policy authorization context: user` error.
Expand Down
14 changes: 12 additions & 2 deletions lib/action_policy/rspec/have_authorized_scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ module RSpec
#
class HaveAuthorizedScope < ::RSpec::Matchers::BuiltIn::BaseMatcher
attr_reader :type, :name, :policy, :scope_options, :actual_scopes,
:target_expectations
:target_expectations, :context

def initialize(type)
@type = type
Expand Down Expand Up @@ -49,14 +49,19 @@ def with_target(&block)
self
end

def with_context(context)
@context = context
self
end

def match(_expected, actual)
raise "This matcher only supports block expectations" unless actual.is_a?(Proc)

ActionPolicy::Testing::AuthorizeTracker.tracking { actual.call }

@actual_scopes = ActionPolicy::Testing::AuthorizeTracker.scopings

matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options) }
matching_scopes = actual_scopes.select { _1.matches?(policy, type, name, scope_options, context) }

return false if matching_scopes.empty?

Expand All @@ -80,6 +85,7 @@ def supports_block_expectations?() = true
def failure_message
"expected a scoping named :#{name} for type :#{type} " \
"#{scope_options_message} " \
"and #{context_message} " \
"from #{policy} to have been applied, " \
"but #{actual_scopes_message}"
end
Expand All @@ -97,6 +103,10 @@ def scope_options_message
end
end

def context_message
context.blank? ? "without context" : "with context: #{context}"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot use #blank? here, we have no dependency on Active Support core extensions (I'll fix it).

end

def actual_scopes_message
if actual_scopes.empty?
"no scopings have been made"
Expand Down
7 changes: 5 additions & 2 deletions lib/action_policy/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def assert_authorized_to(rule, target, with: nil, context: {})
# end
# end
#
def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil)
def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil, context: {})
raise ArgumentError, "Block is required" unless block_given?

policy = with
Expand All @@ -97,10 +97,13 @@ def assert_have_authorized_scope(type:, with:, as: :default, scope_options: nil)
"without scope options"
end

context_message = context.empty? ? "without context" : "with context: #{context}"

assert(
actual_scopes.any? { |scope| scope.matches?(policy, type, as, scope_options) },
actual_scopes.any? { |scope| scope.matches?(policy, type, as, scope_options, context) },
"Expected a scoping named :#{as} for :#{type} type " \
"#{scope_options_message} " \
"and #{context_message} " \
"from #{policy} to have been applied, " \
"but no such scoping has been made.\n" \
"Registered scopings: " \
Expand Down
27 changes: 17 additions & 10 deletions lib/action_policy/testing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ module ActionPolicy
module Testing
# Collects all Authorizer calls
module AuthorizeTracker
module Context
private

def context_matches?(context, actual)
return true unless context

context === actual || actual >= context
end
end

class Call # :nodoc:
include Context

attr_reader :policy, :rule

def initialize(policy, rule)
Expand All @@ -23,17 +35,11 @@ def inspect
"#{policy.record.inspect} was authorized with #{policy.class}##{rule} " \
"and context #{policy.authorization_context.inspect}"
end

private

def context_matches?(context, actual)
return true unless context

context === actual || actual >= context
end
end

class Scoping # :nodoc:
include Context

attr_reader :policy, :target, :type, :name, :scope_options

def initialize(policy, target, type, name, scope_options)
Expand All @@ -44,11 +50,12 @@ def initialize(policy, target, type, name, scope_options)
@scope_options = scope_options
end

def matches?(policy_class, actual_type, actual_name, actual_scope_options)
def matches?(policy_class, actual_type, actual_name, actual_scope_options, actual_context)
policy_class == policy.class &&
type == actual_type &&
name == actual_name &&
actual_scope_options === scope_options
actual_scope_options === scope_options &&
context_matches?(actual_context, policy.authorization_context)
end

def inspect
Expand Down
35 changes: 35 additions & 0 deletions spec/action_policy/rspec_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class TestService # :nodoc:

class CustomPolicy < UserPolicy
authorize :able_to_yell, optional: true
authorize :all_users, optional: true

def some_action?
true
Expand All @@ -16,6 +17,10 @@ def yell?
able_to_yell
end

scope_for :data, :all do |users|
all_users ? users : []
end

alias_rule :aliased_action?, to: :some_action?
end

Expand Down Expand Up @@ -75,6 +80,10 @@ def filter_with_options(users, with_admins: false)
def own(users)
authorized_scope users, type: :data, as: :own, with: UserPolicy
end

def filter_with_context(users, context:)
authorized_scope users, type: :data, as: :all, with: CustomPolicy, context: context
end
end

describe "ActionPolicy RSpec matchers" do
Expand Down Expand Up @@ -293,6 +302,13 @@ def own(users)
expect(target.first.name).to eq "admin"
}
end

specify "with context" do
expect { subject.filter_with_context(target, context: { all_users: false, able_to_yell: true }) }
.to have_authorized_scope(:data)
.with(TestService::CustomPolicy).as(:all)
.with_context(all_users: false, able_to_yell: true)
end
end

context "when no scoping performed" do
Expand All @@ -303,6 +319,7 @@ def own(users)
end.to raise_error(
RSpec::Expectations::ExpectationNotMetError,
Regexp.new("expected a scoping named :default for type :datum without scope options " \
"and without context " \
"from TestService::CustomPolicy to have been applied")
)
end
Expand All @@ -314,6 +331,7 @@ def own(users)
end.to raise_error(
RSpec::Expectations::ExpectationNotMetError,
Regexp.new("expected a scoping named :default for type :data without scope options " \
"and without context " \
"from UserPolicy to have been applied")
)
end
Expand All @@ -325,6 +343,7 @@ def own(users)
end.to raise_error(
RSpec::Expectations::ExpectationNotMetError,
Regexp.new("expected a scoping named :default for type :data without scope options " \
"and without context " \
"from UserPolicy to have been applied")
)
end
Expand All @@ -338,6 +357,7 @@ def own(users)
RSpec::Expectations::ExpectationNotMetError,
Regexp.new("expected a scoping named :default for type :data " \
"with scope options {:with_admins=>false} " \
"and without context " \
"from TestService::CustomPolicy to have been applied")
)
end
Expand All @@ -351,6 +371,7 @@ def own(users)
RSpec::Expectations::ExpectationNotMetError,
Regexp.new("expected a scoping named :default for type :data " \
"with scope options matching {:with_admins=>\\(a falsey value\\)} " \
"and without context " \
"from TestService::CustomPolicy to have been applied")
)
end
Expand All @@ -367,6 +388,20 @@ def own(users)
/^\s+expected: "Guest"\n\s+got: "admin"/
)
end

specify "context mismatch" do
expect do
expect { subject.filter_with_context(target, context: { all_users: true }) }
.to have_authorized_scope(:data)
.with(TestService::CustomPolicy)
.with_context(all_users: false)
end.to raise_error(
RSpec::Expectations::ExpectationNotMetError,
Regexp.new("expected a scoping named :default for type :data without scope options " \
"and with context: {:all_users=>false} " \
"from TestService::CustomPolicy to have been applied")
)
end
end
end
end
38 changes: 34 additions & 4 deletions test/action_policy/test_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
class TestHelperTest < Minitest::Test
include ActionPolicy::TestHelper

class CustomPolicy < ::UserPolicy; end
class CustomPolicy < ::UserPolicy
authorize :all_users, optional: true

scope_for :data, :all do |users|
all_users ? users : []
end
end

class Channel
include ActionPolicy::Behaviour
Expand Down Expand Up @@ -40,6 +46,10 @@ def filter_with_options(users, with_admins: false)
def own(users)
authorized_scope users, type: :data, as: :own, with: CustomPolicy
end

def filter_with_context(users, context:)
authorized_scope users, type: :data, as: :all, with: CustomPolicy, context: context
end
end

def setup
Expand Down Expand Up @@ -116,6 +126,12 @@ def test_assert_have_authorized_scope_with_target_block
end
end

def test_assert_have_authorized_scope_with_context
assert_have_authorized_scope(type: :data, as: :all, with: CustomPolicy, context: {all_users: false}) do
subject.filter_with_context([user], context: {all_users: false})
end
end

def test_assert_have_authorized_scope_raised_when_policy_mismatch
error = assert_raises Minitest::Assertion do
assert_have_authorized_scope(type: :data, with: ::UserPolicy) do
Expand All @@ -125,7 +141,7 @@ def test_assert_have_authorized_scope_raised_when_policy_mismatch

assert_match(
Regexp.new("Expected a scoping named :default for :data type without scope options " \
"from UserPolicy to have been applied"),
"and without context from UserPolicy to have been applied"),
error.message
)
end
Expand All @@ -139,7 +155,7 @@ def test_assert_have_authorized_scope_raised_when_scope_name_mismatch

assert_match(
Regexp.new("Expected a scoping named :own for :data type without scope options " \
"from UserPolicy to have been applied"),
"and without context from UserPolicy to have been applied"),
error.message
)
assert_match(
Expand All @@ -159,7 +175,7 @@ def test_assert_have_authorized_scope_raised_when_scope_options_mismatch
assert_match(
Regexp.new("Expected a scoping named :default for :data type " \
"with scope options {:with_admins=>false} " \
"from UserPolicy to have been applied"),
"and without context from UserPolicy to have been applied"),
error.message
)
assert_match(
Expand All @@ -168,4 +184,18 @@ def test_assert_have_authorized_scope_raised_when_scope_options_mismatch
error.message
)
end

def test_assert_have_authorized_scope_raised_when_context_mismatch
error = assert_raises Minitest::Assertion do
assert_have_authorized_scope(type: :data, as: :all, with: CustomPolicy, context: {all_users: false}) do
subject.filter_with_context([user], context: {all_users: true})
end
end

assert_match(
Regexp.new("Expected a scoping named :all for :data type without scope options " \
"and with context: {:all_users=>false} from TestHelperTest::CustomPolicy to have been applied"),
error.message
)
end
end
Loading