docbrown/spec/services/feed_events/bulk_upsert_spec.rb
Ben Halpern cadebde66f
Add feed_success_score and other count tallies (#20106)
* Add initial feed events score

* Add feed events score

* Clean up code styles

* Adjust how tests are run

* Fix conflicts

* Update app/javascript/articles/Feed.jsx

* Add additional feed events tabulation queries for more reliable eventual consistency

* Add additional feed events tabulation queries for more reliable eventual consistency

* Fix test logic

* Updates from feedback
2023-09-15 12:02:26 -04:00

193 lines
7.2 KiB
Ruby

require "rails_helper"
RSpec.describe FeedEvents::BulkUpsert, type: :service do
let(:user) { create(:user) }
let(:second_user) { create(:user) }
def feed_event(**attributes)
build(:feed_event, **attributes).attributes
end
context "when there are no duplicates in the list or database" do
let(:articles) { create_list(:article, 5) }
let(:feed_events_data) do
articles
.map
.with_index do |article, index|
{
article: article,
user: user,
article_position: index + 1,
category: :impression,
context_type: FeedEvent::CONTEXT_TYPE_HOME
}
end
end
it "inserts all the feed events" do
expect { described_class.call(feed_events_data) }.to change(FeedEvent, :count).by(5)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to match_array(
articles.map { |article| [article.id, user.id, "impression"] },
)
end
it "calls bulk_update_counters_by_article_id for article ids" do
allow(FeedEvent).to receive(:bulk_update_counters_by_article_id)
described_class.call(feed_events_data)
expect(FeedEvent).to have_received(:bulk_update_counters_by_article_id).with(match_array(articles.map(&:id)))
end
end
context "when there are more than 5 events" do
let(:articles) { create_list(:article, 8) }
let(:feed_events_data) do
articles
.map
.with_index do |article, index|
{
article: article,
user: user,
article_position: index + 1,
category: :impression,
context_type: FeedEvent::CONTEXT_TYPE_HOME
}
end
end
it "calls bulk_update_counters_by_article_id for article ids" do
allow(FeedEvent).to receive(:bulk_update_counters_by_article_id)
described_class.call(feed_events_data)
expect(FeedEvent).to have_received(:bulk_update_counters_by_article_id).with(satisfy do |arg|
arg.is_a?(Array) && arg.length == 5
end)
end
end
context "when there are duplicate events within the list" do
let(:article) { create(:article) }
let(:feed_events_data) do
Array.new(5) do
feed_event(article: article, user: user, category: :click)
end
end
it "does not insert extra duplicate events" do
expect { described_class.call(feed_events_data) }.to change(FeedEvent, :count).by(1)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to contain_exactly(
[article.id, user.id, "click"],
)
end
end
context "when there are invalid events in the list" do
let(:article) { create(:article) }
let(:feed_events_data) do
[
feed_event(article: nil, user: user, category: :click),
feed_event(article: article, user: user).merge(category: :not_a_real_category),
feed_event(article: article, user: user, category: :impression),
feed_event(article: article, user: nil, category: :impression),
feed_event(article: article, user: second_user, category: :impression, context_type: "blahblahblah"),
feed_event(article: article, user: second_user, category: :click, article_position: -2),
]
end
it "filters them out" do
expect { described_class.call(feed_events_data, timebox: nil) }.to change(FeedEvent, :count).by(2)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to contain_exactly(
[article.id, user.id, "impression"],
[article.id, nil, "impression"],
)
end
end
context "when there is only one valid item in the list" do
let(:article) { create(:article) }
let(:feed_event_data) do
[
feed_event(article: article, user: user, category: :click, context_type: "foobar"),
feed_event(article: article, user: second_user, category: :impression),
]
end
it "handles it appropriately" do
expect { described_class.call(feed_event_data) }.to change(FeedEvent, :count).by(1)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to contain_exactly(
[article.id, second_user.id, "impression"],
)
end
end
context "when there are no valid items in the list" do
let(:feed_event_data) do
[
feed_event(article: nil, user: user, category: :click),
feed_event(article: nil, user: second_user, category: :click),
]
end
it "does nothing and returns" do
allow(FeedEvent).to receive(:where).and_call_original
expect { described_class.call(feed_event_data) }.not_to change(FeedEvent, :count)
expect(FeedEvent).not_to have_received(:where)
end
end
context "when there are already existing events with the same article, user and category" do
let(:article) { create(:article) }
let(:second_article) { create(:article) }
let(:feed_events_data) do
[
feed_event(article: article, user: user, category: :impression),
feed_event(article: article, user: user, category: :click),
feed_event(article: article, user: second_user, category: :impression),
feed_event(article: second_article, user: user, category: :impression),
]
end
before do
create(:feed_event, article: article, user: user, category: :impression)
create(:feed_event, article: article, user: second_user, category: :impression)
end
it "ignores them if no timebox is provided" do
expect { described_class.call(feed_events_data, timebox: nil) }.to change(FeedEvent, :count).by(4)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to contain_exactly(
[article.id, user.id, "impression"],
[article.id, second_user.id, "impression"],
[article.id, user.id, "impression"],
[article.id, user.id, "click"],
[article.id, second_user.id, "impression"],
[second_article.id, user.id, "impression"],
)
end
it "does not create new events if the existing matching events were created within the provided timebox" do
Timecop.travel(7.minutes.from_now) do
expect { described_class.call(feed_events_data, timebox: 10.minutes) }.to change(FeedEvent, :count).by(2)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to contain_exactly(
[article.id, user.id, "impression"],
[article.id, second_user.id, "impression"],
[article.id, user.id, "click"],
[second_article.id, user.id, "impression"],
)
end
end
it "creates new events if the existing matching events were created outside the provided timebox" do
Timecop.travel(12.minutes.from_now) do
expect { described_class.call(feed_events_data, timebox: 10.minutes) }.to change(FeedEvent, :count).by(4)
expect(FeedEvent.pluck(:article_id, :user_id, :category)).to contain_exactly(
[article.id, user.id, "impression"],
[article.id, second_user.id, "impression"],
[article.id, user.id, "impression"],
[article.id, user.id, "click"],
[article.id, second_user.id, "impression"],
[second_article.id, user.id, "impression"],
)
end
end
end
end