docbrown/spec/models/feed_event_spec.rb
2023-10-02 10:28:31 +07:00

196 lines
8.6 KiB
Ruby

require "rails_helper"
RSpec.describe FeedEvent do
let(:reaction_multiplier) { FeedEvent::REACTION_SCORE_MULTIPLIER }
let(:comment_multiplier) { FeedEvent::COMMENT_SCORE_MULTIPLIER }
let(:valid_categories) { %w[impression click reaction comment extended_pageview] }
describe "validations" do
it { is_expected.to belong_to(:article).optional }
it { is_expected.to validate_numericality_of(:article_id).only_integer }
it { is_expected.to belong_to(:user).optional }
it { is_expected.to validate_numericality_of(:user_id).only_integer.allow_nil }
it { is_expected.to define_enum_for(:category).with_values(valid_categories) }
it { is_expected.to validate_numericality_of(:article_position).is_greater_than(0).only_integer }
it { is_expected.to validate_inclusion_of(:context_type).in_array(%w[home search tag]) }
end
describe ".record_journey_for" do
subject(:record_journey) { described_class.record_journey_for(user, article: article, category: category) }
let(:article) { create(:article) }
let(:user) { create(:user) }
let(:category) { :reaction }
it "records a feed event if the user's last click was on the specified article" do
click = create(:feed_event, user: user, article: article, category: :click)
expect { record_journey }.to change(described_class, :count).by(1)
expect(user.feed_events.last).to have_attributes(
category: "reaction",
article_id: article.id,
user_id: user.id,
context_type: click.context_type,
article_position: click.article_position,
)
end
it "does not record a feed event if the user has no feed events for the specified article" do
expect { record_journey }.not_to change(described_class, :count)
expect(user.feed_events).to be_empty
end
it "does not record a feed event if the user did not click through from the feed" do
impression = create(:feed_event, user: user, article: article, category: :impression)
expect { record_journey }.not_to change(described_class, :count)
expect(user.feed_events).to contain_exactly(impression)
end
it "does not record a feed event if the user's last click was not on the specified article" do
click = create(:feed_event, user: user, article: article, category: :click)
other_click = create(:feed_event, user: user, category: :click)
expect { record_journey }.not_to change(described_class, :count)
expect(user.feed_events).to contain_exactly(click, other_click)
end
context "when the interaction is not a comment or reaction" do
let(:category) { :impression }
it "does not record a feed event" do
click = create(:feed_event, user: user, article: article, category: :click)
expect { record_journey }.not_to change(described_class, :count)
expect(user.feed_events).to contain_exactly(click)
end
end
end
describe "after_save .update_article_counters_and_scores" do
let!(:article) { create(:article) }
let!(:user1) { create(:user) }
let!(:user2) { create(:user) }
it "updates article counters and scores when a FeedEvent is saved" do
# Create some feed events to simulate behavior
create(:feed_event, category: "impression", article: article, user: user1)
create(:feed_event, category: "click", article: article, user: user1)
create(:feed_event, category: "reaction", article: article, user: user1)
create(:feed_event, category: "comment", article: article, user: user2)
# Trigger the after_save
create(:feed_event, category: "impression", article: article, user: user2)
# Reload the article to get the updated counters and scores
article.reload
expect(article.feed_success_score).to eq((1 + reaction_multiplier + comment_multiplier) / 2.0) # Calculated score
expect(article.feed_impressions_count).to eq(2) # Two impressions
expect(article.feed_clicks_count).to eq(1) # One click
end
it "returns early if article is nil" do
feed_event_without_article = build(:feed_event, category: "impression")
allow(feed_event_without_article).to receive(:update_article_counters_and_scores)
feed_event_without_article.save
expect(feed_event_without_article).not_to have_received(:update_article_counters_and_scores)
end
it "initializes counters and scores to zero when no events have occurred" do
expect do
create(:feed_event, category: "impression", article: article, user: user1)
end.to not_change { article.reload.feed_success_score }
.and not_change { article.reload.feed_clicks_count }
.and change { article.reload.feed_impressions_count }.from(0).to(1)
end
it "correctly updates when only one type of event occurs" do
create(:feed_event, category: "reaction", article: article, user: user1)
# Trigger the after_save
create(:feed_event, category: "impression", article: article, user: user1)
article.reload
expect(article.feed_success_score).to eq(reaction_multiplier) # One reaction by one distinct user
expect(article.feed_impressions_count).to eq(1) # One impression
expect(article.feed_clicks_count).to eq(0) # Zero clicks
end
it "considers only distinct users for each type of event for score, but not count" do
create_list(:feed_event, 2, category: "impression", article: article, user: user1)
create_list(:feed_event, 4, category: "click", article: article, user: user1)
create_list(:feed_event, 3, category: "reaction", article: article, user: user2)
create_list(:feed_event, 2, category: "comment", article: article, user: user2)
create_list(:feed_event, 3, category: "extended_pageview", article: article, user: user2)
# Trigger the after_save
create(:feed_event, category: "impression", article: article, user: user2)
article.reload
expect(article.feed_success_score).to eq((1 + 1 + reaction_multiplier + comment_multiplier) / 2.0)
expect(article.feed_clicks_count).to eq(4)
expect(article.feed_impressions_count).to eq(3)
end
end
describe ".bulk_update_counters_by_article_id" do
let!(:article1) { create(:article) }
let!(:article2) { create(:article) }
let!(:user1) { create(:user) }
let!(:user2) { create(:user) }
it "updates counters and scores for multiple articles" do
# Create some feed events for article1
create(:feed_event, category: "impression", article: article1, user: user1)
create(:feed_event, category: "impression", article: article1, user: user2)
create(:feed_event, category: "impression", article: article1, user: user2) # Duplicate
create(:feed_event, category: "click", article: article1, user: user1)
create(:feed_event, category: "reaction", article: article1, user: user1)
create(:feed_event, category: "comment", article: article1, user: user2)
create(:feed_event, category: "extended_pageview", article: article1, user: user2)
# Create some feed events for article2
create(:feed_event, category: "impression", article: article2, user: user2)
create(:feed_event, category: "click", article: article2, user: user1)
create(:feed_event, category: "reaction", article: article2, user: user2)
described_class.bulk_update_counters_by_article_id([article1.id, article2.id])
article1.reload
article2.reload
expect(article1.feed_success_score).to eq((1 + 1 + reaction_multiplier + comment_multiplier) / 2.0)
expect(article1.feed_impressions_count).to eq(3)
expect(article1.feed_clicks_count).to eq(1)
expect(article2.feed_success_score).to eq((1 + reaction_multiplier) / 1.0) # Calculated score
expect(article2.feed_impressions_count).to eq(1)
expect(article2.feed_clicks_count).to eq(1)
end
it "skips articles with no impressions" do
# Create some feed events for article2, but none for article1
create(:feed_event, category: "click", article: article2, user: user1)
create(:feed_event, category: "reaction", article: article2, user: user2)
create(:feed_event, category: "comment", article: article2, user: user2)
described_class.bulk_update_counters_by_article_id([article1.id, article2.id])
article1.reload
article2.reload
expect(article1.feed_success_score).to eq(0.0) # Should remain uninitialized
expect(article1.feed_impressions_count).to eq(0)
expect(article1.feed_clicks_count).to eq(0)
# Scores and counters for article2 should remain uninitialized
expect(article2.feed_success_score).to eq(0.0)
expect(article2.feed_impressions_count).to eq(0)
expect(article2.feed_clicks_count).to eq(0)
end
end
end