require "rails_helper" RSpec.describe Search::ReadingList, type: :service do let(:user) { create(:user) } let(:article) { create(:article) } let(:article_not_in_reading_list) { create(:article) } let(:article_1) { create(:article, with_tags: false) } let(:article_2) { create(:article, with_tags: false) } let(:unpublished_article) { create(:article) } def extract_from_results(result, attribute) result[:items].pluck(:reactable).pluck(attribute) end def count_of_published_readling_items_for(user:) Article.published.where(id: user.reactions.readinglist.map(&:reactable_id)).count end def published_reading_list_item_for(user:) user.reactions.readinglist.detect { |item| item.reactable.published? } end describe "::search_documents" do before do create(:reaction, user: user, reactable: article, category: :readinglist, status: :valid) end it "returns an empty result without a user" do expect(described_class.search_documents(nil)).to be_empty end it "does not include an article not in the reading list" do result = described_class.search_documents(user) expect(extract_from_results(result, :path)).not_to include(article_not_in_reading_list.path) end it "does not return an article belonging to another user's reading list" do create( :reaction, reactable: article_not_in_reading_list, category: :readinglist, user: article_not_in_reading_list.user, ) result = described_class.search_documents(user) expect(extract_from_results(result, :path)).not_to include(article_not_in_reading_list.path) end it "returns results of articles in the reading list" do result = described_class.search_documents(user) expect(extract_from_results(result, :path)).to include(article.path) end it "returns the total count of the articles in the reading list" do articles = create_list(:article, 2) articles.each { |article| create(:reaction, category: :readinglist, reactable: article, user: user) } result = described_class.search_documents(user) expect(result[:total]).to eq(count_of_published_readling_items_for(user: user)) end context "with an article added to a reading list then unpublished" do before do create(:reaction, user: user, reactable: unpublished_article, category: :readinglist, status: :valid) unpublished_article.update_columns(published: false) end it "does not include the unpublished article" do result = described_class.search_documents(user) expect(extract_from_results(result, :path)).not_to include(unpublished_article.path) end end context "when describing the result format" do let(:result) { described_class.search_documents(user) } it "returns the correct attributes for the result", :aggregate_failures do expect(result.keys).to match_array(%i[items total]) expect(result[:total]).to be_present expect(result[:items]).to be_present end it "returns the correct attributes for a reading list item", :aggregate_failures do item = result[:items].first expect(item.keys).to match_array(%i[id user_id reactable]) # The user's reading includes published and unpublished # articles. We want to make sure that what we found in the # search is on the user's reading list. expect(user.reactions.readinglist.map(&:id)).to include(item[:id]) expect(item[:user_id]).to eq(user.id) end it "returns the correct attributes for a reading list item's reactable", :aggregate_failures do item = result[:items].first reactable = item[:reactable] expect(reactable.keys).to match_array( %i[ path readable_publish_date_string reading_time tag_list tags title user ], ) expect(reactable[:path]).to eq(article.path) expect(reactable[:readable_publish_date_string]).to eq(article.readable_publish_date) expect(reactable[:reading_time]).to eq(article.reading_time) tags = article.cached_tag_list.to_s.split(", ") expect(reactable[:tag_list]).to eq(tags) expect(reactable[:tags]).to eq(tags.map { |tag| { name: tag } }) expect(reactable[:title]).to eq(article.title) end it "returns the correct attributes for a reading list item's reactable's user", :aggregate_failures do item = result[:items].first reactable = item[:reactable] reactable_user = reactable[:user] expect(reactable_user.keys).to eq(%i[name profile_image_90 username]) expect(reactable_user[:name]).to eq(article.user.name) expect(reactable_user[:profile_image_90]).to eq(article.user.profile_image_90) expect(reactable_user[:username]).to eq(article.user.username) end end context "when filtering by statuses" do it "returns confirmed items by default" do item = published_reading_list_item_for(user: user) item.update_columns(status: :confirmed) expect(described_class.search_documents(user)[:items].first[:id]).to eq(item.id) end it "returns valid items by default" do item = published_reading_list_item_for(user: user) item.update_columns(status: :valid) expect(described_class.search_documents(user)[:items].first[:id]).to eq(item.id) end it "selects archived items upon request", :aggregate_failures do valid_item = user.reactions.readinglist.last archived_item = create(:reaction, user: user, category: :readinglist, status: :archived) # rubocop:disable Rails/PluckId ids = described_class.search_documents(user, statuses: :archived)[:items].pluck(:id) # rubocop:enable Rails/PluckId expect(ids).to include(archived_item.id) expect(ids).not_to include(valid_item.id) end end context "when filtering by tags" do after do user.reactions.readinglist.delete_all end it "filters out items belonging to an article without the requested tag" do article_1.tag_list.add(:beginners) article_1.save! create(:reaction, reactable: article_1, user: user, category: :readinglist) result = described_class.search_documents(user, tags: [:foobar]) expect(extract_from_results(result, :path)).to be_empty end it "selects items belonging to an article with the requested tag" do article_1.tag_list.add(:beginners) article_1.save! create(:reaction, reactable: article_1, user: user, category: :readinglist) result = described_class.search_documents(user, tags: [:beginners]) expect(extract_from_results(result, :path)).to match_array([article_1.path]) end it "selects items belonging to an article with all the requested tags", :aggregate_failures do article_1.tag_list.add(:beginners) article_1.tag_list.add(:ruby) article_1.save! article_2.tag_list.add(:beginners) article_2.save! create(:reaction, reactable: article_1, user: user, category: :readinglist) create(:reaction, reactable: article_2, user: user, category: :readinglist) result = described_class.search_documents(user, tags: %i[beginners ruby]) expect(extract_from_results(result, :path)).to include(article_1.path) expect(extract_from_results(result, :path)).not_to include(article_2.path) end it "does not match on partial tags", :aggregate_failures do article_1.tag_list.add(:javascript) article_1.save! create(:reaction, reactable: article_1, user: user, category: :readinglist) result = described_class.search_documents(user, tags: [:java]) expect(extract_from_results(result, :path)).to be_empty result = described_class.search_documents(user, tags: [:asc]) expect(extract_from_results(result, :path)).to be_empty result = described_class.search_documents(user, tags: [:script]) expect(extract_from_results(result, :path)).to be_empty end end context "when filtering by statuses and tags" do it "selects items with the requested statuses and articles tags", :aggregate_failures do article_1.tag_list.add(:beginners) article_2.tag_list.add(:beginners) article_1.save! article_2.save! valid_item = create(:reaction, category: :readinglist, user: user, reactable: article_1, status: :valid) archived_item = create(:reaction, category: :readinglist, user: user, reactable: article_2, status: :archived) result = described_class.search_documents(user, statuses: %i[valid], tags: %i[beginners]) # rubocop:disable Rails/PluckId item_ids = result[:items].pluck(:id) # rubocop:enable Rails/PluckId expect(item_ids).to include(valid_item.id) expect(item_ids).not_to include(archived_item.id) end end context "when searching for a term" do it "matches against the article's body_markdown", :aggregate_failures do article.update_columns(body_markdown: "Life of the party") result = described_class.search_documents(user, term: "part") expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "fiesta") expect(extract_from_results(result, :path)).to be_empty end it "matches against the article's title", :aggregate_failures do article.update_columns(title: "Life of the party") result = described_class.search_documents(user, term: "part") expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "fiesta") expect(extract_from_results(result, :path)).to be_empty end it "matches against the article's tags", :aggregate_failures do article.update_columns(cached_tag_list: "javascript, beginners, ruby") result = described_class.search_documents(user, term: "beginner") expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "newbie") expect(extract_from_results(result, :path)).to be_empty end it "matches against the article's organization's name", :aggregate_failures do article.organization = create(:organization, name: "ACME corp") article.save! result = described_class.search_documents(user, term: "ACME") expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "ECMA") expect(extract_from_results(result, :path)).to be_empty end it "matches against the article's user's name", :aggregate_failures do result = described_class.search_documents(user, term: article.user_name.first(3)) expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "notaname") expect(extract_from_results(result, :path)).to be_empty end it "matches against the article's user's username", :aggregate_failures do result = described_class.search_documents(user, term: article.user_username.first(3)) expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "notausername") expect(extract_from_results(result, :path)).to be_empty end end context "when searching for a term and filtering by statuses" do it "selects items with the requested status belonging to articles matching the term", :aggregate_failures do article.update_columns(body_markdown: "Life of the party") result = described_class.search_documents(user, term: "part", statuses: %i[valid]) expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "part", statuses: %i[archived]) expect(extract_from_results(result, :path)).to be_empty result = described_class.search_documents(user, term: "fiesta", statuses: %i[valid]) expect(extract_from_results(result, :path)).to be_empty end end context "when searching for a term and filtering by tags" do it "selects items with the requested status belonging to articles matching the term", :aggregate_failures do article.update_columns(body_markdown: "Life of the party", cached_tag_list: "javascript, beginners, ruby") result = described_class.search_documents(user, term: "part", tags: %i[javascript]) expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "part", tags: %i[python]) expect(extract_from_results(result, :path)).to be_empty result = described_class.search_documents(user, term: "fiesta", tags: %i[javascript]) expect(extract_from_results(result, :path)).to be_empty end end context "when searching for a term and filtering by statuses and tags" do it "selects items with the requested status belonging to articles matching the term", :aggregate_failures do article.update_columns(body_markdown: "Life of the party", cached_tag_list: "javascript, beginners, ruby") result = described_class.search_documents(user, term: "part", statuses: %i[valid], tags: %i[javascript]) expect(extract_from_results(result, :path)).to include(article.path) result = described_class.search_documents(user, term: "part", statuses: %i[archived], tags: %i[javascript]) expect(extract_from_results(result, :path)).to be_empty result = described_class.search_documents(user, term: "part", statuses: %i[valid], tags: %i[python]) expect(extract_from_results(result, :path)).to be_empty end end context "when paginating" do let(:articles) { create_list(:article, 2) } before do articles.each { |article| create(:reaction, category: :readinglist, reactable: article, user: user) } end it "returns the total count of the articles pre-pagination" do result = described_class.search_documents(user, page: 1, per_page: 1) expect(result[:total]).to eq(count_of_published_readling_items_for(user: user)) end it "returns no items when out of pagination bounds" do result = described_class.search_documents(user, page: 99) expect(result[:items]).to be_empty end it "returns paginated items", :aggregate_failures do result = described_class.search_documents(user, page: 1, per_page: 1) expect(result[:items].length).to eq(1) result = described_class.search_documents(user, page: 2, per_page: 1) expect(result[:items].length).to eq(1) end end end end