activerecord-hackery--ransack/spec/ransack/adapters/active_record/base_spec.rb

714 lines
26 KiB
Ruby

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 '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: 'babies@example.com')
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
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' }
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 rails7_and_mysql
::ActiveRecord::VERSION::MAJOR >= 7 && ENV['DB'] == 'mysql'
end
end
end
end
end