inspired by
https://github.com/ryanb/cancan/wiki/Testing-Abilities
then
https://gist.github.com/fotinakis/3a532a0929f64b4b5352
then
| # https://gist.github.com/rilian/e89d9dbc096f56ebcb1d | |
| # | |
| # Custom rspec matcher for testing CanCan abilities. | |
| # Originally inspired by https://github.com/ryanb/cancan/wiki/Testing-Abilities | |
| # | |
| # Usage: | |
| # expect.to have_abilities(:create).on(Post.new) | |
| # expect.to have_abilities([:read, :update].on(post) | |
| # expect.to have_abilities({manage: false, destroy: true}.on(post) | |
| # expect.to have_abilities({create: false}.on(Post.new) | |
| # expect.to not_have_abilities(:update.on(post) | |
| # expect.to not_have_abilities([:update, :destroy].on(post) | |
| # | |
| # WARNING: never use "should_not have_abilities" or you may get false positives due to | |
| # whitelisting/blacklisting issues. Use "should not_have_abilities" instead. | |
| RSpec::Matchers.define :have_abilities do |actions| | |
| include HaveAbilitiesMixin | |
| chain :on do |obj| | |
| @obj = obj | |
| end | |
| match do |ability| | |
| verify_ability_type(ability) | |
| @expected_hash = build_expected_hash(actions, default_expectation: true) | |
| @actual_hash = {} | |
| @expected_hash.each do |action, _| | |
| @actual_hash[action] = ability.can?(action, @obj) | |
| end | |
| @actual_hash == @expected_hash | |
| end | |
| description do | |
| obj_name = @obj.class.name | |
| obj_name = @obj.to_s.capitalize if [Class, Module, Symbol].include?(@obj.class) | |
| "have abilities #{@expected_hash.keys.join(', ')} on #{obj_name}" | |
| end | |
| failure_message do | |
| obj_name = @obj.class.name | |
| obj_name = @obj.to_s.capitalize if [Class, Module, Symbol].include?(@obj.class) | |
| "expected user to have abilities: #{@expected_hash} for #{obj_name}, but got #{@actual_hash}" | |
| end | |
| end | |
| RSpec::Matchers.define :not_have_abilities do |actions| | |
| include HaveAbilitiesMixin | |
| chain :on do |obj| | |
| @obj = obj | |
| end | |
| match do |ability| | |
| verify_ability_type(ability) | |
| if actions.is_a?(Hash) | |
| raise ArgumentError, 'You cannot pass a hash to not_have_abilities. Use have_abilities instead.' | |
| end | |
| @expected_hash = build_expected_hash(actions, default_expectation: false) | |
| @actual_hash = {} | |
| @expected_hash.each do |action, _| | |
| @actual_hash[action] = ability.can?(action, @obj) | |
| end | |
| @actual_hash == @expected_hash | |
| end | |
| description do | |
| obj_name = @obj.class.name | |
| obj_name = @obj.to_s.capitalize if [Class, Module, Symbol].include?(@obj.class) | |
| "not have abilities #{@expected_hash.keys.join(', ')} on #{obj_name}" if @expected_hash.present? | |
| end | |
| failure_message do | |
| obj_name = @obj.class.name | |
| obj_name = @obj.to_s.capitalize if [Class, Module, Symbol].include?(@obj.class) | |
| "expected user NOT to have abilities #{@expected_hash.keys.join(', ')} for #{obj_name}, but got #{@actual_hash}" | |
| end | |
| end | |
| module HaveAbilitiesMixin | |
| def build_expected_hash(actions, default_expectation:) | |
| return actions if actions.is_a?(Hash) | |
| expected_hash = {} | |
| if actions.is_a?(Array) | |
| # If given an array like [:create, read] build a hash like: | |
| # {create: default_expectation, read: default_expectation} | |
| actions.each { |action| expected_hash[action] = default_expectation } | |
| elsif actions.is_a?(Symbol) | |
| # Build a hash if it's just a symbol. | |
| expected_hash = { actions => default_expectation } | |
| end | |
| expected_hash | |
| end | |
| def verify_ability_type(ability) | |
| return if ability.class.ancestors.include?(CanCan::Ability) | |
| raise TypeError, "subject must mixin CanCan::Ability, got a #{ability.class.name} class." | |
| end | |
| end |