docbrown/spec/services/search/reading_list_spec.rb
Jeremy Friesen 9df5f6af49
Ensuring reading list excludes unpublished articles (#15490)
* Ensuring reading list excludes unpublished articles

Prior to this commit, if a given user adds an article to their reading
list then the author unpublishes the article, the article remains in the
reading list.  When the given user would then "click" on the now
unpublished article, they would get a 404 Not Found notice.

With this commit, we now exclude unpublished articles from the search
results for the reading list.

This should also address the issue of attempting to archive the reading
list item (because it won't be visible in the listing).

One of the behaviors that is expected is if, in the above scenario, the
author again publishes the article, that article will again "appear" on
the reading list for the given user.

Closes #14796

* Updating documentation and adding spec

* Addressing pull request feedback

Yes, I should've used a scope!  And also, no need to test present.  Just
let it's "truthy"-ness speak for itself!

* Update spec/services/search/reading_list_spec.rb

Co-authored-by: Jamie Gaskins <jamie@forem.com>

Co-authored-by: Jamie Gaskins <jamie@forem.com>
2021-11-24 13:22:26 -05:00

362 lines
15 KiB
Ruby

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