docbrown/spec/services/articles/feed_spec.rb
2020-02-27 14:44:04 -06:00

290 lines
8.9 KiB
Ruby

require "rails_helper"
RSpec.describe Articles::Feed, type: :service do
let(:user) { create(:user) }
let!(:feed) { described_class.new(user: user, number_of_articles: 100, page: 1) }
let!(:article) { create(:article) }
let!(:hot_story) { create(:article, hotness_score: 1000, score: 1000, published_at: 3.hours.ago) }
let!(:old_story) { create(:article, published_at: 3.days.ago) }
let!(:low_scoring_article) { create(:article, score: -1000) }
let!(:month_old_story) { create(:article, published_at: 1.month.ago) }
describe "#published_articles_by_tag" do
let(:unpublished_article) { create(:article, published: false) }
let(:tag) { "foo" }
let(:tagged_article) { create(:article, tags: tag) }
it "returns published articles" do
expect(feed.published_articles_by_tag).to include article
expect(feed.published_articles_by_tag).not_to include unpublished_article
end
context "with tag" do
it "returns articles with the specified tag" do
expect(described_class.new(tag: tag).published_articles_by_tag).to include tagged_article
end
end
end
describe "#top_articles_by_timeframe" do
let!(:moderately_high_scoring_article) { create(:article, score: 20) }
let(:result) { feed.top_articles_by_timeframe(timeframe: "week").to_a }
it "returns articles ordered by score" do
expect(result.slice(0, 2)).to eq [hot_story, moderately_high_scoring_article]
expect(result.last).to eq low_scoring_article
end
context "with week article timeframe specified" do
it "only returns articles from this week" do
expect(feed.top_articles_by_timeframe(timeframe: "week")).not_to include(month_old_story)
end
end
end
describe "#latest_feed" do
it "only returns articles with scores above -40" do
expect(feed.latest_feed).not_to include(low_scoring_article)
end
it "returns articles ordered by publishing date descending" do
expect(feed.latest_feed.last).to eq month_old_story
end
end
describe "#default_home_feed_and_featured_story" do
let(:featured_story) { feed.default_home_feed_and_featured_story.first }
let(:stories) { feed.default_home_feed_and_featured_story.second }
before { article.update(published_at: 1.week.ago) }
it "returns a featured article and array of other articles" do
expect(featured_story).to be_a(Article)
expect(stories).to be_a(Array)
expect(stories.first).to be_a(Article)
end
it "chooses a featured story with a main image" do
expect(featured_story).to eq hot_story
end
it "doesn't include low scoring stories" do
expect(stories).not_to include(low_scoring_article)
end
context "when user logged in" do
let(:result) { feed.default_home_feed_and_featured_story(user_signed_in: true) }
let(:featured_story) { result.first }
let(:stories) { result.second }
it "only includes stories from less than 6 hours ago" do
expect(stories).not_to include(old_story)
expect(stories).not_to include(article)
# Ideally we'd test for hot_story in the stories list, but the random offset selection makes that random
expect(featured_story).to eq(hot_story)
end
end
end
describe "#default_home_feed" do
let!(:new_story) { create(:article, published_at: 10.minutes.ago, score: 10) }
context "when user is not logged in" do
let(:stories) { feed.default_home_feed(user_signed_in: false) }
before { article.update(published_at: 1.week.ago) }
it "returns array of articles" do
expect(stories).to be_a(Array)
expect(stories.first).to be_a(Article)
end
it "doesn't include low scoring stories" do
expect(stories).not_to include(low_scoring_article)
end
end
context "when user logged in" do
let(:stories) { feed.default_home_feed(user_signed_in: true) }
it "includes stories from between 2 and 6 hours ago" do
expect(stories).not_to include(old_story)
expect(stories).to include(new_story)
end
end
end
describe "#score_followed_user" do
context "when article is written by a followed user" do
before { user.follow(article.user) }
it "returns a score of 1" do
expect(feed.score_followed_user(article)).to eq 1
end
end
context "when article is not written by a followed user" do
it "returns a score of 0" do
expect(feed.score_followed_user(article)).to eq 0
end
end
end
describe "#score_followed_organization" do
let(:organization) { create(:organization) }
let(:article) { create(:article, organization: organization) }
context "when article is from a followed organization" do
before { user.follow(organization) }
it "returns a score of 1" do
expect(feed.score_followed_organization(article)).to eq 1
end
end
context "when article is not from a followed organization" do
it "returns a score of 0" do
expect(feed.score_followed_organization(article)).to eq 0
end
end
context "when article has no organization" do
let(:article) { create(:article) }
it "returns a score of 0" do
expect(feed.score_followed_organization(article)).to eq 0
end
end
end
describe "#score_randomness" do
context "when random number is less than 0.6 but greater than 0.3" do
it "returns 6" do
allow(feed).to receive(:rand).and_return(0.5)
expect(feed.score_randomness).to eq 6
end
end
context "when random number is less than 0.3" do
it "returns 3" do
allow(feed).to receive(:rand).and_return(0.1)
expect(feed.score_randomness).to eq 3
end
end
context "when random number is greater than 0.6" do
it "returns 0" do
allow(feed).to receive(:rand).and_return(0.9)
expect(feed.score_randomness).to eq 0
end
end
end
describe "#score_language" do
context "when article is in a user's preferred language" do
it "returns a score of 1" do
expect(feed.score_language(article)).to eq 1
end
end
context "when article is not in user's prferred language" do
before { article.language = "de" }
it "returns a score of -10" do
expect(feed.score_language(article)).to eq(-10)
end
end
context "when article doesn't have a language, assume english" do
before { article.language = nil }
it "returns a score of 1" do
expect(feed.score_language(article)).to eq 1
end
end
end
describe "#score_followed_tags" do
let(:tag) { create(:tag) }
let(:unfollowed_tag) { create(:tag) }
context "when article includes a followed tag" do
let(:article) { create(:article, tags: tag.name) }
before do
user.follow(tag)
user.save
user.follows.last.update(points: 2)
end
it "returns the followed tag point value" do
expect(feed.score_followed_tags(article)).to eq 2
end
end
context "when article includes multiple followed tags" do
let(:tag2) { create(:tag) }
let(:article) { create(:article, tags: "#{tag.name}, #{tag2.name}") }
before do
user.follow(tag)
user.follow(tag2)
user.save
user.follows.each { |follow| follow.update(points: 2) }
end
it "returns the sum of followed tag point values" do
expect(feed.score_followed_tags(article)).to eq 4
end
end
context "when article includes an unfollowed tag" do
let(:article) { create(:article, tags: "#{tag.name}, #{unfollowed_tag.name}") }
before do
user.follow(tag)
user.save
end
it "doesn't score the unfollowed tag" do
expect(feed.score_followed_tags(article)).to eq 1
end
end
context "when article doesn't include any followed tags" do
let(:article) { create(:article, tags: unfollowed_tag.name) }
it "returns 0" do
expect(feed.score_followed_tags(article)).to eq 0
end
end
context "when user doesn't follow any tags" do
it "returns 0" do
expect(user.cached_followed_tag_names).to be_empty
expect(feed.score_followed_tags(article)).to eq 0
end
end
end
describe "#score_experience_level" do
let(:article) { create(:article, experience_level_rating: 9) }
context "when user has an experience level" do
let(:user) { create(:user, experience_level: 3) }
it "returns negative of (absolute value of the difference between article and user experience) divided by 2" do
expect(feed.score_experience_level(article)).to eq(-3)
end
end
context "when the user does not have an experience level set" do
let(:user) { create(:user, experience_level: nil) }
it "uses a value of 5 for user experience level" do
expect(feed.score_experience_level(article)).to eq(-2)
end
end
end
end