From 43d14fbcf63d7a9995bf7592732640a3be1624b4 Mon Sep 17 00:00:00 2001 From: Marcin Nowicki Date: Tue, 1 Oct 2024 07:40:04 +0200 Subject: [PATCH 1/2] Specify desired state in unit spec --- .../delegate_method_matcher_spec.rb | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb b/spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb index 0eb0b44e3..9d94125ff 100644 --- a/spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/independent/delegate_method_matcher_spec.rb @@ -102,6 +102,15 @@ def country end end end + + context 'qualified with #with_private' do + it 'states that it should delegate method to the right object with right argument and makes is private' do + matcher = delegate_method(:method_name).to(:delegate).with_private + message = 'delegate #method_name to the #delegate object privately' + + expect(matcher.description).to eq message + end + end end context 'when the subject is a class' do @@ -655,4 +664,116 @@ def hello end end end + + context 'qualified with #with_private' do + context 'when using delegate from Rails' do + context 'when delegations were defined with :private' do + it 'accepts' do + define_class('Person') do + delegate :hello, to: :country, private: true + def country + end + end + + person = Person.new + + expect(person).to delegate_method(:hello).to(:country).with_private + end + end + + context 'when delegations were not defined with :private' do + it 'rejects with the correct failure message' do + define_class('Person') do + delegate :hello, to: :country + def country + end + end + + person = Person.new + + message = <<-MESSAGE +Expected Person to delegate #hello to the #country object privately. + +Person#hello did delegate to #country, but 'private: true' is missing. + MESSAGE + + expectation = lambda do + expect(person).to delegate_method(:hello).to(:country).with_private + end + + expect(&expectation).to fail_with_message(message) + end + + context 'with :prefix' do + it 'accepts' do + define_class('Person') do + delegate :hello, to: :country, private: true, prefix: :user + def country + end + end + + person = Person.new + + expect(person).to delegate_method(:hello).to(:country).with_prefix(:user).with_private + end + end + + context 'with :as' do + it 'accepts' do + define_class('Company') do + def name + 'Acme Company' + end + end + + define_class('Person') do + private + + def company_name + company.name + end + + def company + Company.new + end + end + + person = Person.new + matcher = delegate_method(:company_name).to(:company).as(:name).with_private + matcher.matches?(person) + + expect(person.send(:company).name).to eq 'Acme Company' + end + + context 'and :prefix' do + it 'accepts' do + define_class('Company') do + def name + 'Acme Company' + end + end + + define_class('Person') do + private + + def company_name + company.name + end + + def company + Company.new + end + end + + person = Person.new + matcher = delegate_method(:company_name).to(:company).with_prefix(:user).as(:name).with_private + matcher.matches?(person) + + expect(person.send(:company).name).to eq 'Acme Company' + end + end + end + end + end + end end From 9c62dbba878c83b955aa688cf09f99a43bc563d6 Mon Sep 17 00:00:00 2001 From: Marcin Nowicki Date: Tue, 1 Oct 2024 07:42:51 +0200 Subject: [PATCH 2/2] Implement delegate_method supporting with_private --- .../independent/delegate_method_matcher.rb | 91 ++++++++++++++++++- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/lib/shoulda/matchers/independent/delegate_method_matcher.rb b/lib/shoulda/matchers/independent/delegate_method_matcher.rb index defd5f2b8..0bd21b37e 100644 --- a/lib/shoulda/matchers/independent/delegate_method_matcher.rb +++ b/lib/shoulda/matchers/independent/delegate_method_matcher.rb @@ -168,8 +168,27 @@ module Independent # should delegate_method(:plan).to(:subscription).allow_nil # end # - # @return [DelegateMethodMatcher] + # ##### with_private + # + # Use `with_private` if the delegation accounts for the fact that your + # delegation is private. (This is mostly intended as an analogue to + # the `private` option that Rails' `delegate` helper takes.) + # + # class Account + # delegate :plan, to: :subscription, private: true + # end + # + # # RSpec + # describe Account do + # it { should delegate_method(:plan).to(:subscription).with_private } + # end + # + # # Minitest + # class PageTest < Minitest::Test + # should delegate_method(:plan).to(:subscription).with_private + # end # + # @return [DelegateMethodMatcher] def delegate_method(delegating_method) DelegateMethodMatcher.new(delegating_method).in_context(self) end @@ -187,6 +206,7 @@ def initialize(delegating_method) @delegate_object_reader_method = nil @delegated_arguments = [] @expects_to_allow_nil_delegate_object = false + @expects_private_delegation = false end def in_context(context) @@ -202,7 +222,8 @@ def matches?(subject) subject_has_delegating_method? && subject_has_delegate_object_reader_method? && subject_delegates_to_delegate_object_correctly? && - subject_handles_nil_delegate_object? + subject_handles_nil_delegate_object? && + subject_handles_private_delegation? end def description @@ -210,6 +231,10 @@ def description "delegate #{formatted_delegating_method_name} to the " + "#{formatted_delegate_object_reader_method_name} object" + if expects_private_delegation? + string << ' privately' + end + if delegated_arguments.any? string << " passing arguments #{delegated_arguments.inspect}" end @@ -254,6 +279,11 @@ def allow_nil self end + def with_private + @expects_private_delegation = true + self + end + def build_delegating_method_prefix(prefix) case prefix when true, nil then delegate_object_reader_method @@ -264,14 +294,19 @@ def build_delegating_method_prefix(prefix) def failure_message message = "Expected #{class_under_test} to #{description}.\n\n" - if failed_to_allow_nil_delegate_object? + if failed_to_allow_nil_delegate_object? || failed_to_handle_private_delegation? message << formatted_delegating_method_name(include_module: true) message << ' did delegate to ' message << formatted_delegate_object_reader_method_name + end + + if failed_to_allow_nil_delegate_object? message << ' when it was non-nil, but it failed to account ' message << 'for when ' message << formatted_delegate_object_reader_method_name message << ' *was* nil.' + elsif failed_to_handle_private_delegation? + message << ", but 'private: true' is missing." else message << 'Method calls sent to ' message << formatted_delegate_object_reader_method_name( @@ -322,6 +357,10 @@ def expects_to_allow_nil_delegate_object? @expects_to_allow_nil_delegate_object end + def expects_private_delegation? + @expects_private_delegation + end + def formatted_delegate_method(options = {}) formatted_method_name_for(delegate_method, options) end @@ -367,7 +406,11 @@ def delegate_object_received_call_with_delegated_arguments? end def subject_has_delegating_method? - subject.respond_to?(delegating_method) + if expects_private_delegation? + !subject.respond_to?(delegating_method) && subject.respond_to?(delegating_method, true) + else + subject.respond_to?(delegating_method) + end end def subject_has_delegate_object_reader_method? @@ -381,7 +424,11 @@ def ensure_delegate_object_has_been_specified! end def subject_delegates_to_delegate_object_correctly? - call_delegating_method_with_delegate_method_returning(delegate_object) + if expects_private_delegation? + privately_call_delegating_method_with_delegate_method_returning(delegate_object) + else + call_delegating_method_with_delegate_method_returning(delegate_object) + end if delegated_arguments.any? delegate_object_received_call_with_delegated_arguments? @@ -411,11 +458,37 @@ def subject_handles_nil_delegate_object? end end + def subject_handles_private_delegation? + @subject_handled_private_delegation = + if expects_private_delegation? + begin + call_delegating_method_with_delegate_method_returning(delegate_object) + true + rescue Module::DelegationError + false + rescue NoMethodError => e + if e.message =~ + /private method `#{delegating_method}' called for/ + true + else + raise e + end + end + else + true + end + end + def failed_to_allow_nil_delegate_object? expects_to_allow_nil_delegate_object? && !@subject_handled_nil_delegate_object end + def failed_to_handle_private_delegation? + expects_private_delegation? && + !@subject_handled_private_delegation + end + def call_delegating_method_with_delegate_method_returning(value) register_subject_double_collection_to(value) @@ -424,6 +497,14 @@ def call_delegating_method_with_delegate_method_returning(value) end end + def privately_call_delegating_method_with_delegate_method_returning(value) + register_subject_double_collection_to(value) + + Doublespeak.with_doubles_activated do + subject.__send__(delegating_method, *delegated_arguments) + end + end + def register_subject_double_collection_to(returned_value) double_collection = Doublespeak.double_collection_for(subject.singleton_class)