diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5d306f8e..fc38fc5bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,7 @@ jobs: - 3.1.4 - 3.0.6 appraisal: + - rails_7_2 - rails_7_1 - rails_7_0 - rails_6_1 @@ -44,6 +45,7 @@ jobs: - { ruby: 3.2.2, appraisal: rails_6_1 } - { ruby: 3.0.6, appraisal: rails_7_0 } - { ruby: 3.0.6, appraisal: rails_7_1 } + - { ruby: 3.0.6, appraisal: rails_7_2 } env: DATABASE_ADAPTER: ${{ matrix.adapter }} BUNDLE_GEMFILE: gemfiles/${{ matrix.appraisal }}.gemfile diff --git a/Appraisals b/Appraisals index 9608836d5..63ac3067f 100644 --- a/Appraisals +++ b/Appraisals @@ -96,3 +96,38 @@ appraise 'rails_7_1' do gem 'sqlite3', '~> 1.4' gem 'pg', '~> 1.1' end + +appraise 'rails_7_2' do + instance_eval(&shared_spring_dependencies) + instance_eval(&controller_test_dependency) + + gem 'rails', '~> 7.2.0' + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem 'brakeman', require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem 'rubocop-rails-omakase', require: false + + gem 'sprockets-rails' + gem 'puma', '~> 6.0' + gem 'importmap-rails' + gem 'turbo-rails' + gem 'stimulus-rails' + gem 'jbuilder' + gem 'bootsnap', require: false + gem 'capybara' + gem 'selenium-webdriver' + gem 'webdrivers' + + # test dependencies + gem 'rspec-rails', '~> 6.0' + gem 'shoulda-context', '~> 2.0.0' + + # other dependencies + gem 'bcrypt', '~> 3.1.7' + + # Database adapters + gem 'sqlite3', '~> 1.4' + gem 'pg', '~> 1.1' +end diff --git a/gemfiles/rails_7_2.gemfile b/gemfiles/rails_7_2.gemfile new file mode 100644 index 000000000..24a4760f4 --- /dev/null +++ b/gemfiles/rails_7_2.gemfile @@ -0,0 +1,44 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal", "2.5.0" +gem "bundler", "~> 2.0" +gem "pry" +gem "pry-byebug" +gem "rake", "13.0.1" +gem "rspec", "~> 3.9" +gem "rubocop", require: false +gem "rubocop-packaging", require: false +gem "rubocop-rails", require: false +gem "warnings_logger" +gem "zeus", require: false +gem "fssm" +gem "redcarpet" +gem "rouge" +gem "yard" +gem "spring" +gem "spring-watcher-listen", "~> 2.0.0" +gem "rails-controller-testing", ">= 1.0.1" +gem "rails", "~> 7.2.0" +gem "brakeman", require: false +gem "rubocop-rails-omakase", require: false +gem "sprockets-rails" +gem "puma", "~> 6.0" +gem "importmap-rails" +gem "turbo-rails" +gem "stimulus-rails" +gem "jbuilder" +gem "bootsnap", require: false +gem "capybara" +gem "selenium-webdriver" +gem "webdrivers" +gem "rspec-rails", "~> 6.0" +gem "shoulda-context", "~> 2.0.0" +gem "bcrypt", "~> 3.1.7" +gem "sqlite3", "~> 1.4" +gem "pg", "~> 1.1" + +if RUBY_VERSION >= "3.1" && RUBY_VERSION < "3.2" + gem "error_highlight", ">= 0.4.0", platforms: [:ruby] +end diff --git a/gemfiles/rails_7_2.gemfile.lock b/gemfiles/rails_7_2.gemfile.lock new file mode 100644 index 000000000..4d484f94d --- /dev/null +++ b/gemfiles/rails_7_2.gemfile.lock @@ -0,0 +1,373 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.2.0) + actionpack (= 7.2.0) + activesupport (= 7.2.0) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.2.0) + actionpack (= 7.2.0) + activejob (= 7.2.0) + activerecord (= 7.2.0) + activestorage (= 7.2.0) + activesupport (= 7.2.0) + mail (>= 2.8.0) + actionmailer (7.2.0) + actionpack (= 7.2.0) + actionview (= 7.2.0) + activejob (= 7.2.0) + activesupport (= 7.2.0) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.0) + actionview (= 7.2.0) + activesupport (= 7.2.0) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.0) + actionpack (= 7.2.0) + activerecord (= 7.2.0) + activestorage (= 7.2.0) + activesupport (= 7.2.0) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.2.0) + activesupport (= 7.2.0) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.0) + activesupport (= 7.2.0) + globalid (>= 0.3.6) + activemodel (7.2.0) + activesupport (= 7.2.0) + activerecord (7.2.0) + activemodel (= 7.2.0) + activesupport (= 7.2.0) + timeout (>= 0.4.0) + activestorage (7.2.0) + actionpack (= 7.2.0) + activejob (= 7.2.0) + activerecord (= 7.2.0) + activesupport (= 7.2.0) + marcel (~> 1.0) + activesupport (7.2.0) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + appraisal (2.5.0) + bundler + rake + thor (>= 0.14.0) + ast (2.4.2) + base64 (0.2.0) + bcrypt (3.1.20) + bigdecimal (3.1.8) + bootsnap (1.18.3) + msgpack (~> 1.2) + brakeman (6.1.2) + racc + builder (3.2.4) + byebug (11.1.3) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + coderay (1.1.3) + concurrent-ruby (1.3.1) + connection_pool (2.4.1) + crass (1.0.6) + date (3.3.4) + diff-lcs (1.5.1) + drb (2.2.1) + erubi (1.12.0) + ffi (1.16.3) + fssm (0.2.10) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + importmap-rails (2.0.1) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.7.2) + irb (1.13.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.12.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + json (2.7.2) + language_server-protocol (3.17.0.3) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.0) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + method_source (1.1.0) + mini_mime (1.1.5) + minitest (5.23.1) + msgpack (1.7.2) + net-imap (0.4.14) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.16.5-arm64-darwin) + racc (~> 1.4) + parallel (1.24.0) + parser (3.3.1.0) + ast (~> 2.4.1) + racc + pg (1.5.6) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.10.1) + byebug (~> 11.0) + pry (>= 0.13, < 0.15) + psych (5.1.2) + stringio + public_suffix (5.0.5) + puma (6.4.2) + nio4r (~> 2.0) + racc (1.8.0) + rack (3.0.11) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.2.0) + actioncable (= 7.2.0) + actionmailbox (= 7.2.0) + actionmailer (= 7.2.0) + actionpack (= 7.2.0) + actiontext (= 7.2.0) + actionview (= 7.2.0) + activejob (= 7.2.0) + activemodel (= 7.2.0) + activerecord (= 7.2.0) + activestorage (= 7.2.0) + activesupport (= 7.2.0) + bundler (>= 1.15.0) + railties (= 7.2.0) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.2.0) + actionpack (= 7.2.0) + activesupport (= 7.2.0) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.0.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rdoc (6.7.0) + psych (>= 4.0.0) + redcarpet (3.6.0) + regexp_parser (2.9.2) + reline (0.5.7) + io-console (~> 0.5) + rexml (3.2.8) + strscan (>= 3.0.9) + rouge (4.2.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + rubocop (1.64.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) + rubocop-minitest (0.35.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-packaging (0.5.2) + rubocop (>= 1.33, < 2.0) + rubocop-performance (1.21.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.25.0) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + ruby-progressbar (1.13.0) + rubyzip (2.3.2) + securerandom (0.3.1) + selenium-webdriver (4.10.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + shoulda-context (2.0.0) + spring (2.1.1) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + sprockets (>= 3.0.0) + sqlite3 (1.7.3-arm64-darwin) + stimulus-rails (1.3.3) + railties (>= 6.0.0) + stringio (3.1.0) + strscan (3.1.0) + thor (1.3.1) + timeout (0.4.1) + turbo-rails (2.0.5) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) + useragent (0.16.10) + warnings_logger (0.1.1) + webdrivers (5.3.1) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0, < 4.11) + webrick (1.8.1) + websocket (1.2.10) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + yard (0.9.36) + zeitwerk (2.6.15) + zeus (0.16.0) + method_source (>= 0.6.7) + +PLATFORMS + arm64-darwin-23 + +DEPENDENCIES + appraisal (= 2.5.0) + bcrypt (~> 3.1.7) + bootsnap + brakeman + bundler (~> 2.0) + capybara + fssm + importmap-rails + jbuilder + pg (~> 1.1) + pry + pry-byebug + puma (~> 6.0) + rails (~> 7.2.0) + rails-controller-testing (>= 1.0.1) + rake (= 13.0.1) + redcarpet + rouge + rspec (~> 3.9) + rspec-rails (~> 6.0) + rubocop + rubocop-packaging + rubocop-rails + rubocop-rails-omakase + selenium-webdriver + shoulda-context (~> 2.0.0) + spring + spring-watcher-listen (~> 2.0.0) + sprockets-rails + sqlite3 (~> 1.4) + stimulus-rails + turbo-rails + warnings_logger + webdrivers + yard + zeus + +BUNDLED WITH + 2.4.13 diff --git a/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb b/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb index b012f8b33..414002454 100644 --- a/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +++ b/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb @@ -19,10 +19,7 @@ def description def matches?(subject) self.subject = ModelReflector.new(subject, name) - if option_verifier.correct_for_string?( - :counter_cache, - counter_cache, - ) + if correct_value? true else self.missing_option = "#{name} should have #{description}" @@ -34,9 +31,42 @@ def matches?(subject) attr_accessor :subject, :counter_cache, :name + def correct_value? + expected = normalize_value + + if expected.is_a?(Hash) + option_verifier.correct_for_hash?( + :counter_cache, + expected, + ) + else + option_verifier.correct_for_string?( + :counter_cache, + expected, + ) + end + end + def option_verifier @_option_verifier ||= OptionVerifier.new(subject) end + + def normalize_value + if Rails::VERSION::STRING >= '7.2' + case counter_cache + when true + { active: true, column: nil } + when String, Symbol + { active: true, column: counter_cache.to_s } + when Hash + { active: true, column: nil }.merge(counter_cache) + else + raise ArgumentError, 'Invalid counter_cache option' + end + else + counter_cache + end + end end end end diff --git a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb index d179a31b9..4288422fd 100644 --- a/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +++ b/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb @@ -655,11 +655,22 @@ def expected_default_value end def actual_default_value - attribute_schema = model.attributes_to_define_after_schema_loads[attribute_name.to_s] + attribute_schema = if model.respond_to?(:_default_attributes) + model._default_attributes[attribute_name.to_s] + else + model.attributes_to_define_after_schema_loads[attribute_name.to_s] + end + + if Kernel.const_defined?('ActiveModel::Attribute::UserProvidedDefault') && + attribute_schema.is_a?(::ActiveModel::Attribute::UserProvidedDefault) + attribute_schema = attribute_schema.marshal_dump + end value = case attribute_schema in [_, { default: default_value } ] default_value + in [_, default_value, *] + default_value in [_, default_value] default_value end diff --git a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb index 7027b7222..6e869309e 100644 --- a/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/association_matcher_spec.rb @@ -113,6 +113,28 @@ expect(belonging_to_parent).not_to belong_to(:parent).counter_cache end + if rails_version >= 7.2 + it 'accepts :counter_cache with a hash' do + expect(belonging_to_parent(counter_cache: { active: true })). + to belong_to(:parent).counter_cache + end + + it 'accepts :counter_cache with active false when passed' do + expect(belonging_to_parent(counter_cache: { active: false })). + to belong_to(:parent).counter_cache(active: false) + end + + it 'rejects :counter_cache with active false when mismatch' do + expect(belonging_to_parent(counter_cache: { active: true })). + not_to belong_to(:parent).counter_cache(active: false) + end + + it 'rejects :counter_cache with when column mismatch' do + expect(belonging_to_parent(counter_cache: { column: :attribute_count })). + not_to belong_to(:parent).counter_cache(true) + end + end + it 'accepts an association with a valid :inverse_of option' do expect(belonging_to_with_inverse(:parent, :children)). to belong_to(:parent).inverse_of(:children) diff --git a/spec/unit/shoulda/matchers/active_record/have_attached_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/have_attached_matcher_spec.rb index 3b3fec53a..2f45e23e0 100644 --- a/spec/unit/shoulda/matchers/active_record/have_attached_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/have_attached_matcher_spec.rb @@ -185,7 +185,13 @@ def record_having_one_attached( end if remove_attachments - reflections.delete("#{attached_name}_attachment") + if respond_to?(:normalized_reflections) + clear_reflections_cache + _reflections.delete("#{attached_name}_attachment".to_sym) + normalized_reflections + else + reflections.delete("#{attached_name}_attachment") + end end if invalidate_blobs @@ -223,7 +229,13 @@ def record_having_many_attached( end if remove_attachments - reflections.delete("#{attached_name}_attachments") + if respond_to?(:normalized_reflections) + clear_reflections_cache + _reflections.delete("#{attached_name}_attachments".to_sym) + normalized_reflections + else + reflections.delete("#{attached_name}_attachments") + end end if invalidate_blobs diff --git a/spec/unit/shoulda/matchers/active_record/serialize_matcher_spec.rb b/spec/unit/shoulda/matchers/active_record/serialize_matcher_spec.rb index 7e63b6d1b..65f3e7768 100644 --- a/spec/unit/shoulda/matchers/active_record/serialize_matcher_spec.rb +++ b/spec/unit/shoulda/matchers/active_record/serialize_matcher_spec.rb @@ -68,13 +68,13 @@ def unserialized_model end def with_serialized_attr(type: nil, coder: YAML) - type_or_coder = rails_version >= 7.1 ? nil : type || coder + args = rails_version >= 7.1 ? [] : [type || coder] define_model(:example, attr: :string) do if type - serialize :attr, type_or_coder, type: type, coder: coder + serialize :attr, *args, type: type, coder: coder else - serialize :attr, type_or_coder, coder: coder + serialize :attr, *args, coder: coder end end.new end diff --git a/spec/unit_spec_helper.rb b/spec/unit_spec_helper.rb index 81f8cec7a..83fc24fcc 100644 --- a/spec/unit_spec_helper.rb +++ b/spec/unit_spec_helper.rb @@ -32,7 +32,11 @@ end end -ActiveSupport::Deprecation.behavior = :stderr +if Rails::VERSION::STRING >= '7.2' + Rails.application.deprecators.behavior = :stderr +else + ActiveSupport::Deprecation.behavior = :stderr +end Shoulda::Matchers.configure do |config| config.integrate do |with|