Back to Repositories

Testing ActiveRecord Search Adapter Implementation in Ransack

This test suite validates the core functionality of Ransack’s ActiveRecord adapter, focusing on search and query building capabilities. It covers advanced search features including scopes, associations, ransackers, and attribute allowlisting.

Test Coverage Overview

The test suite provides comprehensive coverage of Ransack’s integration with ActiveRecord.

Key areas tested include:
  • Basic search functionality and query building
  • Complex associations (has_one through, HABTM, self-referential)
  • Custom ransackers and search helpers
  • Scopes and attribute allowlisting
  • Sorting and authorization controls

Implementation Analysis

The testing approach uses RSpec to validate search behavior at multiple levels.

Notable patterns include:
  • Context-based test organization for different search scenarios
  • Database adapter-specific test conditionals
  • Extensive use of let blocks for test data setup
  • SQL query validation for complex searches

Technical Details

Testing tools and configuration:
  • RSpec as the test framework
  • SQLite3 and PostgreSQL adapter support
  • Custom ransacker implementations
  • Mock objects for authorization testing
  • Database cleaner integration

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Comprehensive edge case coverage
  • Proper test isolation and data cleanup
  • Clear test descriptions and organization
  • Validation of both positive and negative cases
  • Database-agnostic test implementations where possible

activerecord-hackery/ransack

spec/ransack/adapters/active_record/base_spec.rb

            
require 'spec_helper'

module Ransack
  module Adapters
    module ActiveRecord
      describe Base do

        subject { ::ActiveRecord::Base }

        it { should respond_to :ransack }

        describe '#search' do
          subject { Person.ransack }

          it { should be_a Search }
          it 'has a Relation as its object' do
            expect(subject.object).to be_an ::ActiveRecord::Relation
          end

          context "multiple database connection" do
            it "does not raise error" do
              expect { Person.ransack(name_cont: "test") }.not_to raise_error
              expect { SubDB::OperationHistory.ransack(people_id_eq: 1) }.not_to raise_error
            end
          end

          context 'with scopes' do
            before do
              allow(Person)
              .to receive(:ransackable_scopes)
              .and_return([:active, :over_age, :of_age])
            end

            it 'applies true scopes' do
              s = Person.ransack('active' => true)
              expect(s.result.to_sql).to (include 'active = 1')
            end

            it 'applies stringy true scopes' do
              s = Person.ransack('active' => 'true')
              expect(s.result.to_sql).to (include 'active = 1')
            end

            it 'applies stringy boolean scopes with true value in an array' do
              s = Person.ransack('of_age' => ['true'])
              expect(s.result.to_sql).to (include rails7_and_mysql ? %q{(age >= '18')} : 'age >= 18')
            end

            it 'applies stringy boolean scopes with false value in an array' do
              s = Person.ransack('of_age' => ['false'])
              expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age < '18'} : 'age < 18')
            end

            it 'ignores unlisted scopes' do
              s = Person.ransack('restricted' => true)
              expect(s.result.to_sql).to_not (include 'restricted')
            end

            it 'ignores false scopes' do
              s = Person.ransack('active' => false)
              expect(s.result.to_sql).not_to (include 'active')
            end

            it 'ignores stringy false scopes' do
              s = Person.ransack('active' => 'false')
              expect(s.result.to_sql).to_not (include 'active')
            end

            it 'passes values to scopes' do
              s = Person.ransack('over_age' => 18)
              expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '18'} : 'age > 18')
            end

            it 'chains scopes' do
              s = Person.ransack('over_age' => 18, 'active' => true)
              expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '18'} : 'age > 18')
              expect(s.result.to_sql).to (include 'active = 1')
            end

            it 'applies scopes that define string SQL joins' do
              allow(Article)
                .to receive(:ransackable_scopes)
                .and_return([:latest_comment_cont])

              # Including a negative condition to test removing the scope
              s = Search.new(Article, notes_note_not_eq: 'Test', latest_comment_cont: 'Test')
              expect(s.result.to_sql).to include 'latest_comment'
            end

            context "with sanitize_custom_scope_booleans set to false" do
              before(:all) do
                Ransack.configure { |c| c.sanitize_custom_scope_booleans = false }
              end

              after(:all) do
                Ransack.configure { |c| c.sanitize_custom_scope_booleans = true }
              end

              it 'passes true values to scopes' do
                s = Person.ransack('over_age' => 1)
                expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '1'} : 'age > 1')
              end

              it 'passes false values to scopes'  do
                s = Person.ransack('over_age' => 0)
                expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '0'} : 'age > 0')
              end
            end

            context "with ransackable_scopes_skip_sanitize_args enabled for scope" do
              before do
                allow(Person)
                .to receive(:ransackable_scopes_skip_sanitize_args)
                .and_return([:over_age])
              end

              it 'passes true values to scopes' do
                s = Person.ransack('over_age' => 1)
                expect(s.result.to_sql).to (include rails7_and_mysql ? %q{age > '1'} : 'age > 1')
              end

              it 'passes false values to scopes'  do
                s = Person.ransack('over_age' => 0)
                expect(s.result.to_sql).to (include  rails7_and_mysql ? %q{age > '0'} : 'age > 0')
              end
            end

          end

          it 'does not raise exception for string :params argument' do
            expect { Person.ransack('') }.to_not raise_error
          end

          it 'raises exception if ransack! called with unknown condition' do
            expect { Person.ransack!(unknown_attr_eq: 'Ernie') }.to raise_error(ArgumentError)
          end

          it 'does not modify the parameters' do
            params = { name_eq: '' }
            expect { Person.ransack(params) }.not_to change { params }
          end
        end

        context 'has_one through associations' do
          let(:address)  { Address.create!(city: 'Boston') }
          let(:org) { Organization.create!(name: 'Testorg', address: address) }
          let!(:employee) { Employee.create!(name: 'Ernie', organization: org) }

          it 'works when has_one through association is first' do
            s = Employee.ransack(address_city_eq: 'Boston', organization_name_eq: 'Testorg')
            expect(s.result.to_a).to include(employee)
          end

          it 'works when has_one through association is last' do
            s = Employee.ransack(organization_name_eq: 'Testorg', address_city_eq: 'Boston')
            expect(s.result.to_a).to include(employee)
          end
        end

        context 'negative conditions on HABTM associations' do
          let(:medieval) { Tag.create!(name: 'Medieval') }
          let(:fantasy)  { Tag.create!(name: 'Fantasy') }
          let(:arthur)   { Article.create!(title: 'King Arthur') }
          let(:marco)    { Article.create!(title: 'Marco Polo') }

          before do
            marco.tags << medieval
            arthur.tags << medieval
            arthur.tags << fantasy
          end

          it 'removes redundant joins from top query' do
            s = Article.ransack(tags_name_not_eq: "Fantasy")
            sql = s.result.to_sql
            expect(sql).to_not include('LEFT OUTER JOIN')
          end

          it 'handles != for single values' do
            s = Article.ransack(tags_name_not_eq: "Fantasy")
            articles = s.result.to_a
            expect(articles).to include marco
            expect(articles).to_not include arthur
          end

          it 'handles NOT IN for multiple attributes' do
            s = Article.ransack(tags_name_not_in: ["Fantasy", "Scifi"])
            articles = s.result.to_a

            expect(articles).to include marco
            expect(articles).to_not include arthur
          end
        end

        context 'negative conditions on self-referenced associations' do
          let(:pop) { Person.create!(name: 'Grandpa') }
          let(:dad) { Person.create!(name: 'Father') }
          let(:mom) { Person.create!(name: 'Mother') }
          let(:son) { Person.create!(name: 'Grandchild') }

          before do
            son.parent = dad
            dad.parent = pop
            dad.children << son
            mom.children << son
            pop.children << dad
            son.save! && dad.save! && mom.save! && pop.save!
          end

          it 'handles multiple associations and aliases' do
            s = Person.ransack(
              c: {
                '0' => { a: ['name'], p: 'not_eq', v: ['Father'] },
                '1' => {
                        a: ['children_name', 'parent_name'],
                        p: 'not_eq', v: ['Father'], m: 'or'
                      },
                '2' => { a: ['children_salary'], p: 'eq', v: [nil] }
              })
            people = s.result

            expect(people.to_a).to include son
            expect(people.to_a).to include mom
            expect(people.to_a).to_not include dad  # rule '0': 'name'
            expect(people.to_a).to_not include pop  # rule '1': 'children_name'
          end
        end

        describe '#ransack_alias' do
          it 'translates an alias to the correct attributes' do
            p = Person.create!(name: 'Meatloaf', email: '[email protected]')

            s = Person.ransack(term_cont: 'atlo')
            expect(s.result.to_a).to eq [p]

            s = Person.ransack(term_cont: 'babi')
            expect(s.result.to_a).to eq [p]

            s = Person.ransack(term_cont: 'nomatch')
            expect(s.result.to_a).to eq []
          end

          it 'also works with associations' do
            dad = Person.create!(name: 'Birdman')
            son = Person.create!(name: 'Weezy', parent: dad)

            s = Person.ransack(daddy_eq: 'Birdman')
            expect(s.result.to_a).to eq [son]

            s = Person.ransack(daddy_eq: 'Drake')
            expect(s.result.to_a).to eq []
          end

          it 'makes aliases available to subclasses' do
            yngwie = Musician.create!(name: 'Yngwie Malmsteen')

            musicians = Musician.ransack(term_cont: 'ngw').result
            expect(musicians).to eq([yngwie])
          end

          it 'handles naming collisions gracefully' do
            frank = Person.create!(name: 'Frank Stallone')

            people = Person.ransack(term_cont: 'allon').result
            expect(people).to eq([frank])

            Class.new(Article) do
              ransack_alias :term, :title
            end

            people = Person.ransack(term_cont: 'allon').result
            expect(people).to eq([frank])
          end
        end

        describe '#ransacker' do
          # For infix tests
          def self.sane_adapter?
            case ::ActiveRecord::Base.connection.adapter_name
            when 'SQLite3', 'PostgreSQL'
              true
            else
              false
            end
          end
          # in schema.rb, class Person:
          # ransacker :reversed_name, formatter: proc { |v| v.reverse } do |parent|
          #   parent.table[:name]
          # end
          #
          # ransacker :doubled_name do |parent|
          #   Arel::Nodes::InfixOperation.new(
          #     '||', parent.table[:name], parent.table[:name]
          #   )
          # end

          it 'creates ransack attributes' do
            person = Person.create!(name: 'Aric Smith')

            s = Person.ransack(reversed_name_eq: 'htimS cirA')
            expect(s.result.size).to eq(1)

            expect(s.result.first).to eq person
          end

          it 'can be accessed through associations' do
            s = Person.ransack(children_reversed_name_eq: 'htimS cirA')
            expect(s.result.to_sql).to match(
              /#{quote_table_name("children_people")}.#{
                 quote_column_name("name")} = 'Aric Smith'/
            )
          end

          it 'allows an attribute to be an InfixOperation' do
            s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
            expect(s.result.first).to eq Person.where(name: 'Aric Smith').first
          end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?

          it 'does not break #count if using InfixOperations' do
            s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
            expect(s.result.count).to eq 1
          end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?

          it 'should remove empty key value pairs from the params hash' do
            s = Person.ransack(children_reversed_name_eq: '')
            expect(s.result.to_sql).not_to match /LEFT OUTER JOIN/
          end

          it 'should keep proper key value pairs in the params hash' do
            s = Person.ransack(children_reversed_name_eq: 'Testing')
            expect(s.result.to_sql).to match /LEFT OUTER JOIN/
          end

          it 'should function correctly when nil is passed in' do
            s = Person.ransack(nil)
          end

          it 'should function correctly when a blank string is passed in' do
            s = Person.ransack('')
          end

          it 'should function correctly with a multi-parameter attribute' do
            if ::ActiveRecord::VERSION::MAJOR >= 7
              ::ActiveRecord.default_timezone = :utc
            else
              ::ActiveRecord::Base.default_timezone = :utc
            end
            Time.zone = 'UTC'

            date = Date.current
            s = Person.ransack(
              { 'created_at_gteq(1i)' => date.year,
                'created_at_gteq(2i)' => date.month,
                'created_at_gteq(3i)' => date.day
              }
            )
            expect(s.result.to_sql).to match />=/
            expect(s.result.to_sql).to match date.to_s
          end

          it 'should function correctly when using fields with dots in them' do
            s = Person.ransack(email_cont: 'example.com')
            expect(s.result.exists?).to be true
          end

          it 'should function correctly when using fields with % in them' do
            p = Person.create!(name: '110%-er')
            s = Person.ransack(name_cont: '10%')
            expect(s.result.to_a).to eq [p]
          end

          it 'should function correctly when using fields with backslashes in them' do
            p = Person.create!(name: "\\WINNER\\")
            s = Person.ransack(name_cont: "\\WINNER\\")
            expect(s.result.to_a).to eq [p]
          end

          context 'searching by underscores' do
            # when escaping is supported right in LIKE expression without adding extra expressions
            def self.simple_escaping?
              case ::ActiveRecord::Base.connection.adapter_name
                when 'Mysql2', 'PostgreSQL'
                  true
                else
                  false
              end
            end

            it 'should search correctly if matches exist' do
              p = Person.create!(name: 'name_with_underscore')
              s = Person.ransack(name_cont: 'name_')
              expect(s.result.to_a).to eq [p]
            end if simple_escaping?

            it 'should return empty result if no matches' do
              Person.create!(name: 'name_with_underscore')
              s = Person.ransack(name_cont: 'n_')
              expect(s.result.to_a).to eq []
            end if simple_escaping?
          end

          context 'searching on an `in` predicate with a ransacker' do
            it 'should function correctly when passing an array of ids' do
              s = Person.ransack(array_people_ids_in: true)
              expect(s.result.count).to be > 0

              s = Person.ransack(array_where_people_ids_in: [1, '2', 3])
              expect(s.result.count).to be 3
              expect(s.result.map(&:id)).to eq [3, 2, 1]
            end

            it 'should function correctly when passing an array of strings' do
              a, b = Person.select(:id).order(:id).limit(2).map { |a| a.id.to_s }

              Person.create!(name: a)
              s = Person.ransack(array_people_names_in: true)
              expect(s.result.count).to be > 0
              s = Person.ransack(array_where_people_names_in: a)
              expect(s.result.count).to be 1

              Person.create!(name: b)
              s = Person.ransack(array_where_people_names_in: [a, b])
              expect(s.result.count).to be 2
            end

            it 'should function correctly with an Arel SqlLiteral' do
              s = Person.ransack(sql_literal_id_in: 1)
              expect(s.result.count).to be 1
              s = Person.ransack(sql_literal_id_in: ['2', 4, '5', 8])
              expect(s.result.count).to be 4
            end
          end

          context 'search on an `in` predicate with an array' do
            it 'should function correctly when passing an array of ids' do
              array = Person.all.map(&:id)
              s = Person.ransack(id_in: array)
              expect(s.result.count).to eq array.size
            end
          end

          it 'should work correctly when an attribute name ends with _start' do
            p = Person.create!(new_start: 'Bar and foo', name: 'Xiang')

            s = Person.ransack(new_start_end: ' and foo')
            expect(s.result.to_a).to eq [p]

            s = Person.ransack(name_or_new_start_start: 'Xia')
            expect(s.result.to_a).to eq [p]

            s = Person.ransack(new_start_or_name_end: 'iang')
            expect(s.result.to_a).to eq [p]
          end

          it 'should work correctly when an attribute name ends with _end' do
            p = Person.create!(stop_end: 'Foo and bar', name: 'Marianne')

            s = Person.ransack(stop_end_start: 'Foo and')
            expect(s.result.to_a).to eq [p]

            s = Person.ransack(stop_end_or_name_end: 'anne')
            expect(s.result.to_a).to eq [p]

            s = Person.ransack(name_or_stop_end_end: ' bar')
            expect(s.result.to_a).to eq [p]
          end

          it 'should work correctly when an attribute name has `and` in it' do
            p = Person.create!(terms_and_conditions: true)
            s = Person.ransack(terms_and_conditions_eq: true)
            expect(s.result.to_a).to eq [p]
          end

          context 'attribute aliased column names',
          if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
            it 'should be translated to original column name' do
              s = Person.ransack(full_name_eq: 'Nicolas Cage')
              expect(s.result.to_sql).to match(
                /WHERE #{quote_table_name("people")}.#{quote_column_name("name")}/
              )
            end

            it 'should translate on associations' do
              s = Person.ransack(articles_content_cont: 'Nicolas Cage')
              expect(s.result.to_sql).to match(
                /#{quote_table_name("articles")}.#{
                   quote_column_name("body")} I?LIKE '%Nicolas Cage%'/
              )
            end
          end

          it 'sorts with different join variants' do
            comments = [
              Comment.create(article: Article.create(title: 'Avenger'), person: Person.create(salary: 100_000)),
              Comment.create(article: Article.create(title: 'Avenge'), person: Person.create(salary: 50_000)),
            ]
            expect(Comment.ransack(article_title_cont: 'aven', s: 'person_salary desc').result).to eq(comments)
            expect(Comment.joins(:person).ransack(s: 'persons_salarydesc', article_title_cont: 'aven').result).to eq(comments)
            expect(Comment.joins(:person).ransack(article_title_cont: 'aven', s: 'persons_salary desc').result).to eq(comments)
          end

          it 'allows sort by `only_sort` field' do
            s = Person.ransack(
              's' => { '0' => { 'dir' => 'asc', 'name' => 'only_sort' } }
            )
            expect(s.result.to_sql).to match(
              /ORDER BY #{quote_table_name("people")}.#{
                quote_column_name("only_sort")} ASC/
            )
          end

          it 'does not sort by `only_search` field' do
            s = Person.ransack(
              's' => { '0' => { 'dir' => 'asc', 'name' => 'only_search' } }
            )
            expect(s.result.to_sql).not_to match(
              /ORDER BY #{quote_table_name("people")}.#{
                quote_column_name("only_search")} ASC/
            )
          end

          it 'allows search by `only_search` field' do
            s = Person.ransack(only_search_eq: 'htimS cirA')
            expect(s.result.to_sql).to match(
              /WHERE #{quote_table_name("people")}.#{
                quote_column_name("only_search")} = 'htimS cirA'/
            )
          end

          it 'cannot be searched by `only_sort`' do
            s = Person.ransack(only_sort_eq: 'htimS cirA')
            expect(s.result.to_sql).not_to match(
              /WHERE #{quote_table_name("people")}.#{
                quote_column_name("only_sort")} = 'htimS cirA'/
            )
          end

          it 'allows sort by `only_admin` field, if auth_object: :admin' do
            s = Person.ransack(
              { 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } } },
              { auth_object: :admin }
            )
            expect(s.result.to_sql).to match(
              /ORDER BY #{quote_table_name("people")}.#{
                quote_column_name("only_admin")} ASC/
            )
          end

          it 'does not sort by `only_admin` field, if auth_object: nil' do
            s = Person.ransack(
              's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } }
            )
            expect(s.result.to_sql).not_to match(
              /ORDER BY #{quote_table_name("people")}.#{
                quote_column_name("only_admin")} ASC/
            )
          end

          it 'allows search by `only_admin` field, if auth_object: :admin' do
            s = Person.ransack(
              { only_admin_eq: 'htimS cirA' },
              { auth_object: :admin }
            )
            expect(s.result.to_sql).to match(
              /WHERE #{quote_table_name("people")}.#{
                quote_column_name("only_admin")} = 'htimS cirA'/
            )
          end

          it 'cannot be searched by `only_admin`, if auth_object: nil' do
            s = Person.ransack(only_admin_eq: 'htimS cirA')
            expect(s.result.to_sql).not_to match(
              /WHERE #{quote_table_name("people")}.#{
                quote_column_name("only_admin")} = 'htimS cirA'/
            )
          end

          it 'should allow passing ransacker arguments to a ransacker' do
            s = Person.ransack(
              c: [{
                a: {
                  '0' => {
                    name: 'with_arguments', ransacker_args: [10, 100]
                  }
                },
                p: 'cont',
                v: ['Passing arguments to ransackers!']
              }]
            )
            expect(s.result.to_sql).to match(
              /LENGTH\(articles.body\) BETWEEN 10 AND 100/
            )
            expect(s.result.to_sql).to match(
              /LIKE \'\%Passing arguments to ransackers!\%\'/
              )
            expect { s.result.first }.to_not raise_error
          end

          it 'should allow sort passing arguments to a ransacker' do
            s = Person.ransack(
              s: {
                '0' => {
                  name: 'with_arguments', dir: 'desc', ransacker_args: [2, 6]
                }
              }
            )
            expect(s.result.to_sql).to match(
              /ORDER BY \(SELECT MAX\(articles.title\) FROM articles/
              )
            expect(s.result.to_sql).to match(
              /WHERE articles.person_id = people.id AND LENGTH\(articles.body\)/
              )
            expect(s.result.to_sql).to match(
              /BETWEEN 2 AND 6 GROUP BY articles.person_id \) DESC/
            )
          end

          context 'case insensitive sorting' do
            it 'allows sort by desc' do
              search = Person.ransack(sorts: ['name_case_insensitive desc'])
              expect(search.result.to_sql).to match /ORDER BY LOWER(.*) DESC/
            end

            it 'allows sort by asc' do
              search = Person.ransack(sorts: ['name_case_insensitive asc'])
              expect(search.result.to_sql).to match /ORDER BY LOWER(.*) ASC/
            end
          end

          context 'regular sorting' do
            it 'allows sort by desc' do
              search = Person.ransack(sorts: ['name desc'])
              expect(search.result.to_sql).to match /ORDER BY .* DESC/
            end

            it 'allows sort by asc' do
              search = Person.ransack(sorts: ['name asc'])
              expect(search.result.to_sql).to match /ORDER BY .* ASC/
            end
          end

          context 'sorting by a scope' do
            it 'applies the correct scope' do
              search = Person.ransack(sorts: ['reverse_name asc'])
              expect(search.result.to_sql).to include("ORDER BY REVERSE(name) ASC")
            end
          end
        end

        describe '#ransackable_attributes' do
          context 'when auth_object is nil' do
            subject { Person.ransackable_attributes }

            it { should include 'name' }
            it { should include 'reversed_name' }
            it { should include 'doubled_name' }
            it { should include 'term' }
            it { should include 'only_search' }
            it { should_not include 'only_sort' }
            it { should_not include 'only_admin' }

            if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
              it { should include 'full_name' }
            end
          end

          context 'with auth_object :admin' do
            subject { Person.ransackable_attributes(:admin) }

            it { should include 'name' }
            it { should include 'reversed_name' }
            it { should include 'doubled_name' }
            it { should include 'only_search' }
            it { should_not include 'only_sort' }
            it { should include 'only_admin' }
          end

          context 'when not defined in model, nor in ApplicationRecord' do
            subject { Article.ransackable_attributes }

            it "raises a helpful error" do
              without_application_record_method(:ransackable_attributes) do
                expect { subject }.to raise_error(RuntimeError, /Ransack needs Article attributes explicitly allowlisted/)
              end
            end
          end

          context 'when defined only in model by delegating to super' do
            subject { Article.ransackable_attributes }

            around do |example|
              Article.singleton_class.define_method(:ransackable_attributes) do
                super(nil) - super(nil)
              end

              example.run
            ensure
              Article.singleton_class.remove_method(:ransackable_attributes)
            end

            it "returns the allowlist in the model, and warns" do
              without_application_record_method(:ransackable_attributes) do
                expect { subject }.to output(/Ransack's builtin `ransackable_attributes` method is deprecated/).to_stderr
                expect(subject).to be_empty
              end
            end
          end
        end

        describe '#ransortable_attributes' do
          context 'when auth_object is nil' do
            subject { Person.ransortable_attributes }

            it { should include 'name' }
            it { should include 'reversed_name' }
            it { should include 'doubled_name' }
            it { should include 'only_sort' }
            it { should_not include 'only_search' }
            it { should_not include 'only_admin' }
          end

          context 'with auth_object :admin' do
            subject { Person.ransortable_attributes(:admin) }

            it { should include 'name' }
            it { should include 'reversed_name' }
            it { should include 'doubled_name' }
            it { should include 'only_sort' }
            it { should_not include 'only_search' }
            it { should include 'only_admin' }
          end
        end

        describe '#ransackable_associations' do
          subject { Person.ransackable_associations }

          it { should include 'parent' }
          it { should include 'children' }
          it { should include 'articles' }

          context 'when not defined in model, nor in ApplicationRecord' do
            subject { Article.ransackable_associations }

            it "raises a helpful error" do
              without_application_record_method(:ransackable_associations) do
                expect { subject }.to raise_error(RuntimeError, /Ransack needs Article associations explicitly allowlisted/)
              end
            end
          end

          context 'when defined only in model by delegating to super' do
            subject { Article.ransackable_associations }

            around do |example|
              Article.singleton_class.define_method(:ransackable_associations) do
                super(nil) - super(nil)
              end

              example.run
            ensure
              Article.singleton_class.remove_method(:ransackable_associations)
            end

            it "returns the allowlist in the model, and warns" do
              without_application_record_method(:ransackable_associations) do
                expect { subject }.to output(/Ransack's builtin `ransackable_associations` method is deprecated/).to_stderr
                expect(subject).to be_empty
              end
            end
          end
        end

        describe '#ransackable_scopes' do
          subject { Person.ransackable_scopes }

          it { should eq [] }
        end

        describe '#ransackable_scopes_skip_sanitize_args' do
          subject { Person.ransackable_scopes_skip_sanitize_args }

          it { should eq [] }
        end

        private

        def without_application_record_method(method)
          ApplicationRecord.singleton_class.alias_method :"original_#{method}", :"#{method}"
          ApplicationRecord.singleton_class.remove_method :"#{method}"

          yield
        ensure
          ApplicationRecord.singleton_class.alias_method :"#{method}", :"original_#{method}"
          ApplicationRecord.singleton_class.remove_method :"original_#{method}"
        end

        def rails7_and_mysql
          ::ActiveRecord::VERSION::MAJOR >= 7 && ENV['DB'] == 'mysql'
        end
      end
    end
  end
end