otwarchive-symphonyarchive/spec/models/search/work_search_form_spec.rb
2026-03-11 22:22:11 +00:00

634 lines
24 KiB
Ruby

require "spec_helper"
describe WorkSearchForm, work_search: true do
describe "#process_options" do
it "removes blank options" do
options = { foo: nil, bar: "", baz: false, boo: true }
searcher = WorkSearchForm.new(options)
expect(searcher.options.keys).to include(:boo)
expect(searcher.options.keys).not_to include(:foo, :bar, :baz)
end
end
describe "#standardize_creator_queries" do
it "renames old creator option" do
options = { query: "creator: alice" }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:query]).to eq("creators: alice")
end
end
describe "#clean_up_angle_brackets" do
it "unescapes angle brackets for numeric and date fields" do
options = {
word_count: "<2000",
hits: "> 100",
kudos_count: ">10",
comments_count: "< 100",
bookmarks_count: ">50",
revised_at: "<1 week ago",
query: "a > b < c",
title: "<3"
}
searcher = WorkSearchForm.new(options)
expect(searcher.options[:word_count]).to eq("<2000")
expect(searcher.options[:hits]).to eq("> 100")
expect(searcher.options[:kudos_count]).to eq(">10")
expect(searcher.options[:comments_count]).to eq("< 100")
expect(searcher.options[:bookmarks_count]).to eq(">50")
expect(searcher.options[:revised_at]).to eq("<1 week ago")
expect(searcher.options[:query]).to eq("a > b < c")
expect(searcher.options[:title]).to eq("&lt;3")
end
end
describe "#rename_warning_field" do
it "renames a legacy field" do
options = { warning_ids: [12] }
searcher = WorkSearchForm.new(options)
expect(searcher.options).not_to have_key(:warning_ids)
expect(searcher.options[:archive_warning_ids]).to eq([12])
end
end
describe "#set_sorting" do
it "does not override provided sort column" do
options = { sort_column: "authors_to_sort_on" }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_column]).to eq("authors_to_sort_on")
end
it "does not override provided sort direction" do
options = { sort_direction: "asc" }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_direction]).to eq("asc")
end
it "sorts by relevance by default" do
searcher = WorkSearchForm.new({})
expect(searcher.options[:sort_column]).to eq("_score")
end
context "when filtering" do
it "sorts by date" do
options = { faceted: true }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_column]).to eq("revised_at")
end
end
context "when viewing collected works" do
it "sorts by date" do
options = { collected: true }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_column]).to eq("revised_at")
end
end
context "when sorting by author" do
it "sets the sort direction to ascending" do
options = { sort_column: "authors_to_sort_on" }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_direction]).to eq("asc")
end
end
context "when sorting by title" do
it "sets the sort direction to ascending" do
options = { sort_column: "title_to_sort_on" }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_direction]).to eq("asc")
end
end
context "when sorting by other fields" do
it "sets the sort direction to descending" do
options = { sort_column: "word_count" }
searcher = WorkSearchForm.new(options)
expect(searcher.options[:sort_direction]).to eq("desc")
end
end
context "when sorting by field without pretty name" do
it "displays the field name in search summary" do
options = { sort_column: "expected_number_of_chapters", sort_direction: "desc" }
searcher = WorkSearchForm.new(options)
expect(searcher.summary).to eq("sort by: expected_number_of_chapters descending")
end
end
end
describe "searching" do
let!(:collection) do
FactoryBot.create(:collection, id: 1)
end
let(:language) { create(:language, short: "ptPT") }
let!(:work) do
FactoryBot.create(:work,
title: "There and back again",
authors: [Pseud.find_by(name: "JRR Tolkien") || FactoryBot.create(:pseud, name: "JRR Tolkien")],
summary: "An unexpected journey",
fandom_string: "The Hobbit",
character_string: "Bilbo Baggins",
expected_number_of_chapters: 3,
complete: false)
end
let!(:second_work) do
FactoryBot.create(:work,
title: "Harry Potter and the Sorcerer's Stone",
authors: [Pseud.find_by(name: "JK Rowling") || FactoryBot.create(:pseud, name: "JK Rowling")],
summary: "Mr and Mrs Dursley, of number four Privet Drive...",
fandom_string: "Harry Potter",
character_string: "Harry Potter, Ron Weasley, Hermione Granger",
language_id: language.id)
end
before(:each) do
# This doesn't work properly in the factory.
second_work.collection_ids = [collection.id]
second_work.save
work.stat_counter.update!(kudos_count: 1200, comments_count: 120, bookmarks_count: 12)
second_work.stat_counter.update!(kudos_count: 999, comments_count: 99, bookmarks_count: 9)
run_all_indexing_jobs
end
it "finds works that match" do
results = WorkSearchForm.new(query: "Hobbit").search_results
expect(results).to include work
expect(results).not_to include second_work
end
it "finds works with tags having numbers" do
work.freeform_string = "Episode: s01e01,Season/Series 01,Brooklyn 99"
work.save
second_work.freeform_string = "Episode: s02e01,Season/Series 99"
second_work.save
run_all_indexing_jobs
# The colon is a reserved character we cannot automatically escape
# without breaking all the hidden search operators.
# We just have to quote it.
results = WorkSearchForm.new(query: "\"Episode: s01e01\"").search_results
expect(results).to include work
expect(results).not_to include second_work
# Quote the search term since it has a space.
results = WorkSearchForm.new(query: "\"Season/Series 99\"").search_results
expect(results).not_to include work
expect(results).to include second_work
end
describe "when searching using user_ids in the query" do
let(:user_id) { second_work.user_ids.first }
context "when the work is in an anonymous collection" do
let(:collection) { create(:anonymous_collection) }
it "doesn't include the work" do
work_search = WorkSearchForm.new(query: "user_ids: #{user_id}")
expect(work_search.search_results).not_to include second_work
end
end
context "when the work is in an unrevealed collection" do
let(:collection) { create(:unrevealed_collection) }
it "doesn't include the work" do
work_search = WorkSearchForm.new(query: "user_ids: #{user_id}")
expect(work_search.search_results).not_to include second_work
end
end
context "when the work is neither anonymous or unrevealed" do
it "includes the work" do
work_search = WorkSearchForm.new(query: "user_ids: #{user_id}")
expect(work_search.search_results).to include second_work
end
end
end
describe "when searching using pseud_ids in the query" do
let(:pseud_id) { second_work.pseud_ids.first }
context "when the work is in an anonymous collection" do
let(:collection) { create(:anonymous_collection) }
it "doesn't include the work" do
work_search = WorkSearchForm.new(query: "pseud_ids: #{pseud_id}")
expect(work_search.search_results).not_to include second_work
end
end
context "when the work is in an unrevealed collection" do
let(:collection) { create(:unrevealed_collection) }
it "doesn't include the work" do
work_search = WorkSearchForm.new(query: "pseud_ids: #{pseud_id}")
expect(work_search.search_results).not_to include second_work
end
end
context "when the work is neither anonymous or unrevealed" do
it "includes the work" do
work_search = WorkSearchForm.new(query: "pseud_ids: #{pseud_id}")
expect(work_search.search_results).to include second_work
end
end
end
describe "when searching unposted works" do
before(:each) do
work.update_attribute(:posted, false)
run_all_indexing_jobs
end
it "should not return them by default" do
work_search = WorkSearchForm.new(query: "Hobbit")
expect(work_search.search_results).not_to include work
end
end
describe "when searching restricted works" do
before(:each) do
work.update_attribute(:restricted, true)
run_all_indexing_jobs
end
it "should not return them by default" do
work_search = WorkSearchForm.new(query: "Hobbit")
expect(work_search.search_results).not_to include work
end
it "should return them when asked" do
work_search = WorkSearchForm.new(query: "Hobbit", show_restricted: true)
expect(work_search.search_results).to include work
end
end
describe "when searching incomplete works" do
it "should not return them when asked for complete works" do
work_search = WorkSearchForm.new(query: "Hobbit", complete: true)
expect(work_search.search_results).not_to include work
end
end
describe "when searching by title" do
it "should match partial titles" do
work_search = WorkSearchForm.new(title: "back again")
expect(work_search.search_results).to include work
end
it "should not match fields other than titles" do
work_search = WorkSearchForm.new(title: "Privet Drive")
expect(work_search.search_results).not_to include second_work
end
end
describe "when searching by author" do
it "should match partial author names" do
work_search = WorkSearchForm.new(creators: "Rowling")
expect(work_search.search_results).to include second_work
end
it "should not match fields other than authors" do
work_search = WorkSearchForm.new(creators: "Baggins")
expect(work_search.search_results).not_to include work
end
it "should turn - into NOT" do
work_search = WorkSearchForm.new(creators: "-Tolkien")
expect(work_search.search_results).not_to include work
end
end
describe "when searching by language" do
let(:unused_language) { create(:language, name: "unused", short: "tlh") }
it "should only return works in that language" do
# "Language" dropdown, with short names
results = WorkSearchForm.new(language_id: "ptPT").search_results
expect(results).not_to include work
expect(results).to include second_work
# "Language" dropdown, with IDs (backward compatibility)
wsf = WorkSearchForm.new(language_id: language.id)
expect(wsf.language_id).to eq("ptPT")
results = wsf.search_results
expect(results).not_to include work
expect(results).to include second_work
# "Any field" or "Search within results", with short names
results = WorkSearchForm.new(query: "language_id: ptPT").search_results
expect(results).not_to include work
expect(results).to include second_work
# "Any field" or "Search within results", with IDs (backward compatibility)
wsf = WorkSearchForm.new(query: "language_id: #{language.id} OR language_id: #{unused_language.id}")
expect(wsf.query).to eq("language_id: ptPT OR language_id: tlh")
results = wsf.search_results
expect(results).not_to include work
expect(results).to include second_work
end
end
describe "when searching by fandom" do
it "should only return works in that fandom" do
work_search = WorkSearchForm.new(fandom_names: "Harry Potter")
expect(work_search.search_results).not_to include work
expect(work_search.search_results).to include second_work
end
it "should not choke on exclamation points" do
work_search = WorkSearchForm.new(fandom_names: "Potter!")
expect(work_search.search_results).to include second_work
expect(work_search.search_results).not_to include work
end
end
describe "when searching by collection" do
it "should only return works in that collection" do
work_search = WorkSearchForm.new(collection_ids: [1])
expect(work_search.search_results).to include second_work
expect(work_search.search_results).not_to include work
end
end
describe "when searching by series title" do
let!(:main_series) { create(:series, title: "Persona: Dancing in Starlight", works: [work]) }
let!(:spinoff_series) { create(:series, title: "Persona 5", works: [second_work]) }
let!(:standalone_work) { create(:work) }
context "using the \"series_titles\" field" do
before { run_all_indexing_jobs }
it "returns only works in matching series" do
results = WorkSearchForm.new(series_titles: "dancing").search_results
expect(results).to include(work)
expect(results).not_to include(second_work, standalone_work)
end
it "returns only works in matching series with numbers in titles" do
results = WorkSearchForm.new(series_titles: "persona 5").search_results
expect(results).to include(second_work)
expect(results).not_to include(work, standalone_work)
end
it "returns all works in series for wildcard queries" do
results = WorkSearchForm.new(series_titles: "*").search_results
expect(results).to include(work, second_work)
expect(results).not_to include(standalone_work)
end
end
context "using the \"query\" field" do
before { run_all_indexing_jobs }
it "works with general queries" do
results = WorkSearchForm.new(query: "dancing").search_results
expect(results).to include(work)
expect(results).not_to include(second_work, standalone_work)
end
it "returns only works in matching series" do
results = WorkSearchForm.new(query: "series.title: dancing").search_results
expect(results).to include(work)
expect(results).not_to include(second_work, standalone_work)
end
it "returns only works in matching series with numbers in titles" do
results = WorkSearchForm.new(query: "series.title: \"persona 5\"").search_results
expect(results).to include(second_work)
expect(results).not_to include(work, standalone_work)
end
it "returns all works in series for wildcard queries" do
results = WorkSearchForm.new(query: "series.title: *").search_results
expect(results).to include(work, second_work)
expect(results).not_to include(standalone_work)
end
end
context "after a series is renamed" do
before do
main_series.update!(title: "Megami Tensei")
run_all_indexing_jobs
end
it "returns only works in matching series" do
results = WorkSearchForm.new(series_titles: "megami").search_results
expect(results).to include(work)
expect(results).not_to include(second_work, standalone_work)
end
end
context "after a work is removed from a series" do
before do
work.serial_works.first.destroy!
run_all_indexing_jobs
end
it "returns only works in matching series" do
results = WorkSearchForm.new(series_titles: "persona").search_results
expect(results).to include(second_work)
expect(results).not_to include(work, standalone_work)
end
end
context "after a series is deleted" do
before do
spinoff_series.destroy!
run_all_indexing_jobs
end
it "returns only works in matching series" do
results = WorkSearchForm.new(series_titles: "persona").search_results
expect(results).to include(work)
expect(results).not_to include(second_work, standalone_work)
end
end
end
describe "when searching by word count" do
before(:each) do
work.chapters.first.update!(content: "This is a work with a word count of ten.")
work.save
second_work.chapters.first.update!(content: "This is a work with a word count of fifteen which is more than ten.")
second_work.save
run_all_indexing_jobs
end
it "should find the right works less than a given number" do
work_search = WorkSearchForm.new(word_count: "<13")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
it "should find the right works more than a given number" do
work_search = WorkSearchForm.new(word_count: "> 10")
expect(work_search.search_results).not_to include work
expect(work_search.search_results).to include second_work
end
it "should find the right works within a range" do
work_search = WorkSearchForm.new(word_count: "0-10")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
end
describe "when searching by kudos count" do
it "should find the right works less than a given number" do
work_search = WorkSearchForm.new(kudos_count: "< 1,000")
expect(work_search.search_results).to include second_work
expect(work_search.search_results).not_to include work
end
it "should find the right works more than a given number" do
work_search = WorkSearchForm.new(kudos_count: "> 999")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
it "should find the right works within a range" do
work_search = WorkSearchForm.new(kudos_count: "1,000-2,000")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
end
describe "when searching by comments count" do
it "should find the right works less than a given number" do
work_search = WorkSearchForm.new(comments_count: "< 100")
expect(work_search.search_results).to include second_work
expect(work_search.search_results).not_to include work
end
it "should find the right works more than a given number" do
work_search = WorkSearchForm.new(comments_count: "> 99")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
it "should find the right works within a range" do
work_search = WorkSearchForm.new(comments_count: "100-2,000")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
end
describe "when searching by bookmarks count" do
it "should find the right works less than a given number" do
work_search = WorkSearchForm.new(bookmarks_count: "< 10")
expect(work_search.search_results).to include second_work
expect(work_search.search_results).not_to include work
end
it "should find the right works more than a given number" do
work_search = WorkSearchForm.new(bookmarks_count: ">9")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
it "should find the right works within a range" do
work_search = WorkSearchForm.new(bookmarks_count: "10-20")
expect(work_search.search_results).to include work
expect(work_search.search_results).not_to include second_work
end
end
end
describe "searching for authors who changes username" do
let!(:user) { create(:user, login: "81_white_chain") }
let!(:second_pseud) { create(:pseud, name: "peacekeeper", user: user) }
let!(:work_by_default_pseud) { create(:work, authors: [user.default_pseud]) }
let!(:work_by_second_pseud) { create(:work, authors: [second_pseud]) }
before { run_all_indexing_jobs }
it "matches only on their current username" do
results = WorkSearchForm.new(creators: "81_white_chain").search_results
expect(results).to include(work_by_default_pseud)
expect(results).to include(work_by_second_pseud)
user.reload
user.login = "82_white_chain"
user.save!
run_all_indexing_jobs
results = WorkSearchForm.new(creators: "81_white_chain").search_results
expect(results).to be_empty
results = WorkSearchForm.new(creators: "82_white_chain").search_results
expect(results).to include(work_by_default_pseud)
expect(results).to include(work_by_second_pseud)
end
end
describe "sorting results" do
describe "by authors" do
before do
%w[21st_wombat 007aardvark].each do |pseud_name|
create(:work, authors: [create(:pseud, name: pseud_name)])
end
run_all_indexing_jobs
end
it "returns all works in the correct order of sortable pseud values" do
sorted_pseuds_asc = %w[007aardvark 21st_wombat]
work_search = WorkSearchForm.new(sort_column: "authors_to_sort_on")
expect(work_search.search_results.map(&:authors_to_sort_on)).to eq sorted_pseuds_asc
work_search = WorkSearchForm.new(sort_column: "authors_to_sort_on", sort_direction: "asc")
expect(work_search.search_results.map(&:authors_to_sort_on)).to eq sorted_pseuds_asc
work_search = WorkSearchForm.new(sort_column: "authors_to_sort_on", sort_direction: "desc")
expect(work_search.search_results.map(&:authors_to_sort_on)).to eq sorted_pseuds_asc.reverse
end
end
describe "by authors who changes username" do
let!(:user1) { create(:user, login: "cioelle") }
let!(:user2) { create(:user, login: "ruth") }
before do
create(:work, authors: [user1.default_pseud])
create(:work, authors: [user2.default_pseud])
run_all_indexing_jobs
end
it "returns all works in the correct order of sortable pseud values" do
work_search = WorkSearchForm.new(sort_column: "authors_to_sort_on")
expect(work_search.search_results.map(&:authors_to_sort_on)).to eq %w[cioelle ruth]
user1.login = "yabalchoath"
user1.save!
run_all_indexing_jobs
work_search = WorkSearchForm.new(sort_column: "authors_to_sort_on")
expect(work_search.search_results.map(&:authors_to_sort_on)).to eq %w[ruth yabalchoath]
end
end
it "keeps sort order of tied works the same when work info is updated" do
user = FactoryBot.create(:user)
work1 = FactoryBot.create(:work, authors: [user.default_pseud])
work2 = FactoryBot.create(:work, authors: [user.default_pseud])
q = WorkQuery.new(sort_column: "authors_to_sort_on", sort_direction: "desc")
run_all_indexing_jobs
res = q.search_results.map(&:id)
[work1, work2].each do |work|
work.update!(summary: "Updated")
run_all_indexing_jobs
expect(q.search_results.map(&:id)).to eq(res)
end
end
end
end