Skip to content

Commit

Permalink
Merge 86d553e into 8e69239
Browse files Browse the repository at this point in the history
  • Loading branch information
killondark committed Apr 6, 2024
2 parents 8e69239 + 86d553e commit 5cb186f
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 18 deletions.
13 changes: 13 additions & 0 deletions docs/testing.md
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
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}"
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
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
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
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
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

0 comments on commit 5cb186f

Please sign in to comment.