* create feed event on feed reaction * move feed event creation to feed event model * tiny whoops * fix failing specs * slightly less flaky e2e spec (maybe) * tiny fixes (couldn't resist)
329 lines
11 KiB
Ruby
329 lines
11 KiB
Ruby
require "rails_helper"
|
|
|
|
RSpec.describe ReactionHandler, type: :service do
|
|
# existing reaction by other user
|
|
# existing reaction by same user, other category
|
|
# no existing reaction = create
|
|
# existing reaction = no-op
|
|
# existing contradictory mod reaction
|
|
|
|
let(:user) { create(:user) }
|
|
let(:article) { create(:article) }
|
|
let(:category) { "like" }
|
|
|
|
let!(:other_category) { article.reactions.create! user: user, category: "hands" }
|
|
let!(:other_existing) { article.reactions.create! user: create(:user), category: "like" }
|
|
|
|
let(:moderator) { create(:user, :trusted) }
|
|
let!(:contradictory_mod) { article.reactions.create! user: moderator, category: "thumbsup" }
|
|
|
|
let(:params) do
|
|
{
|
|
reactable_id: article.id,
|
|
reactable_type: article.class.polymorphic_name,
|
|
category: category
|
|
}
|
|
end
|
|
|
|
let(:reactable_data) do
|
|
{
|
|
reactable_id: article.id,
|
|
reactable_type: "Article",
|
|
reactable_user_id: article.user.id
|
|
}
|
|
end
|
|
|
|
describe "#create" do
|
|
subject(:result) { described_class.new(params, current_user: user).create }
|
|
|
|
context "when no existing/matching reaction by user" do
|
|
it "justs create" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
end
|
|
|
|
it "ignores other existing reactions" do
|
|
expect(result).to be_success
|
|
expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id)
|
|
end
|
|
|
|
it "sends a notification to the author" do
|
|
receiver = { klass: "User", id: article.user.id }
|
|
sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, receiver]) do
|
|
expect(result).to be_success
|
|
end
|
|
end
|
|
|
|
it "records a feed event for articles reached through a feed" do
|
|
create(:feed_event, category: :click, article: article, user: user)
|
|
|
|
expect { result }.to change(FeedEvent, :count).by(1)
|
|
expect(user.feed_events.last).to have_attributes(
|
|
category: "reaction",
|
|
article_id: article.id,
|
|
user_id: user.id,
|
|
)
|
|
end
|
|
|
|
it "does not record a feed event for articles that were not reached through a feed" do
|
|
# activity by a different user!
|
|
create(:feed_event, category: :click, article: article, user: moderator)
|
|
|
|
expect { result }.not_to change(FeedEvent, :count)
|
|
expect(user.feed_events).to be_empty
|
|
end
|
|
end
|
|
|
|
context "when the article is written for an organization" do
|
|
let(:org_author) { create(:user, :org_member) }
|
|
let(:organization) { org_author.organizations.first }
|
|
let(:article) { create(:article, organization: organization, user: org_author) }
|
|
|
|
it "sends a notification to both the author and the organization" do
|
|
author = { klass: "User", id: article.user.id }
|
|
org = { klass: "Organization", id: organization.id }
|
|
|
|
expect(result).to be_success
|
|
sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, author])
|
|
sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, org])
|
|
end
|
|
end
|
|
|
|
context "when the reaction is not a public reaction or bookmark" do
|
|
let(:user) { moderator }
|
|
let(:category) { "vomit" }
|
|
|
|
it "does not record a feed event" do
|
|
click = create(:feed_event, category: :click, article: article, user: moderator)
|
|
|
|
expect { result }.not_to change(FeedEvent, :count)
|
|
expect(moderator.feed_events).to contain_exactly(click)
|
|
end
|
|
end
|
|
|
|
context "when there's an existing/matching reaction by user" do
|
|
let!(:existing) { article.reactions.create! user: user, category: "like" }
|
|
|
|
it "does nothing" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("none")
|
|
end
|
|
|
|
it "ignores other existing reactions" do
|
|
expect(result).to be_success
|
|
expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id, existing.id)
|
|
end
|
|
|
|
it "does not send a notification to the author" do
|
|
sidekiq_assert_no_enqueued_jobs(only: Notifications::NewReactionWorker) do
|
|
expect(result).to be_success
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the reaction is a bookmark" do
|
|
let(:category) { "readinglist" }
|
|
|
|
it "does not send a notification to the author" do
|
|
sidekiq_assert_no_enqueued_jobs(only: Notifications::NewReactionWorker) do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
end
|
|
end
|
|
|
|
it "records a feed event if reached through a feed" do
|
|
create(:feed_event, category: :click, article: article, user: user)
|
|
|
|
expect { result }.to change(FeedEvent, :count).by(1)
|
|
expect(user.feed_events.last).to have_attributes(
|
|
category: "reaction",
|
|
article_id: article.id,
|
|
user_id: user.id,
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when there's an existing, contradictory mod reaction" do
|
|
let(:user) { moderator }
|
|
let(:category) { "vomit" }
|
|
|
|
it "creates" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
end
|
|
|
|
it "destroys the other reaction as a side-effect" do
|
|
expect(result).to be_success
|
|
expect(Reaction.ids).not_to include(contradictory_mod.id)
|
|
end
|
|
end
|
|
|
|
it "updates the last_reacted_at field" do
|
|
Timecop.freeze(Time.current) do
|
|
reaction_handler = described_class.new(params, current_user: user).create
|
|
expect(reaction_handler.reaction.user.last_reacted_at).to eq Time.current
|
|
end
|
|
end
|
|
|
|
context "when the reactable is a comment" do
|
|
let(:user) { moderator }
|
|
let(:comment) { create(:comment) }
|
|
let!(:contradictory_mod) { comment.reactions.create! user: moderator, category: "thumbsup" }
|
|
let(:params) do
|
|
{
|
|
reactable_id: comment.id,
|
|
reactable_type: comment.class.polymorphic_name,
|
|
category: "vomit"
|
|
}
|
|
end
|
|
|
|
it "also destroys contradictory mod reactions" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
expect(Reaction.ids).not_to include(contradictory_mod.id)
|
|
end
|
|
|
|
it "does not record a feed event" do
|
|
click = create(:feed_event, category: :click, article: comment.commentable, user: moderator)
|
|
|
|
expect { result }.not_to change(FeedEvent, :count)
|
|
expect(moderator.feed_events).to contain_exactly(click)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#toggle" do
|
|
subject(:result) { described_class.new(params, current_user: user).toggle }
|
|
|
|
context "when no existing/matching reaction by user" do
|
|
it "justs create" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
end
|
|
|
|
it "ignores other existing reactions" do
|
|
expect(result).to be_success
|
|
expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id)
|
|
end
|
|
|
|
it "sends a notification to the author" do
|
|
receiver = { klass: "User", id: article.user.id }
|
|
sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, receiver]) do
|
|
expect(result).to be_success
|
|
end
|
|
end
|
|
|
|
it "records a feed event for articles reached through a feed" do
|
|
create(:feed_event, category: :click, article: article, user: user)
|
|
|
|
expect { result }.to change(FeedEvent, :count).by(1)
|
|
expect(user.feed_events.last).to have_attributes(
|
|
category: "reaction",
|
|
article_id: article.id,
|
|
user_id: user.id,
|
|
)
|
|
end
|
|
|
|
it "does not record a feed event for articles that were not reached through a feed" do
|
|
# activity by a different user!
|
|
create(:feed_event, category: :click, article: article, user: moderator)
|
|
|
|
expect { result }.not_to change(FeedEvent, :count)
|
|
expect(user.feed_events).to be_empty
|
|
end
|
|
end
|
|
|
|
context "when the reaction is not in a notifiable category" do
|
|
let(:category) { "readinglist" }
|
|
|
|
it "does not send a notification to the author" do
|
|
sidekiq_assert_no_enqueued_jobs(only: Notifications::NewReactionWorker) do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when there's an existing/matching reaction by user" do
|
|
let!(:existing) { article.reactions.create! user: user, category: "like" }
|
|
|
|
it "un-likes" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("destroy")
|
|
expect(Reaction.ids).not_to include(existing.id)
|
|
end
|
|
|
|
it "ignores other existing reactions" do
|
|
expect(result).to be_success
|
|
expect(Reaction.ids).to include(other_category.id, other_existing.id, contradictory_mod.id)
|
|
end
|
|
|
|
it "immediately sends a notification to the author" do
|
|
allow(Notifications::Reactions::Send).to receive(:call)
|
|
|
|
expect(result).to be_success
|
|
|
|
expect(Notifications::Reactions::Send).to have_received(:call).with(reactable_data, article.user)
|
|
end
|
|
end
|
|
|
|
context "when the article is written for an organization" do
|
|
let(:org_author) { create(:user, :org_member) }
|
|
let(:organization) { org_author.organizations.first }
|
|
let(:article) { create(:article, organization: organization, user: org_author) }
|
|
|
|
it "sends a notification to both the author and the organization" do
|
|
author = { klass: "User", id: article.user.id }
|
|
org = { klass: "Organization", id: organization.id }
|
|
|
|
expect(result).to be_success
|
|
sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, author])
|
|
sidekiq_assert_enqueued_with(job: Notifications::NewReactionWorker, args: [reactable_data, org])
|
|
end
|
|
|
|
it "sends the notifications immediately if there was an existing reaction" do
|
|
allow(Notifications::Reactions::Send).to receive(:call)
|
|
|
|
article.reactions.create! user: user, category: "like"
|
|
expect(result).to be_success
|
|
|
|
expect(Notifications::Reactions::Send).to have_received(:call).with(reactable_data, org_author)
|
|
expect(Notifications::Reactions::Send).to have_received(:call).with(reactable_data, organization)
|
|
end
|
|
end
|
|
|
|
context "when there's an existing, contradictory mod reaction" do
|
|
let(:user) { moderator }
|
|
let(:category) { "vomit" }
|
|
|
|
it "creates" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
end
|
|
|
|
it "destroys the other reaction as a side-effect" do
|
|
expect(result).to be_success
|
|
expect(Reaction.ids).not_to include(contradictory_mod.id)
|
|
end
|
|
end
|
|
|
|
context "when the reactable is a comment" do
|
|
let(:user) { moderator }
|
|
let(:comment) { create(:comment) }
|
|
let!(:contradictory_mod) { comment.reactions.create! user: moderator, category: "thumbsup" }
|
|
let(:params) do
|
|
{
|
|
reactable_id: comment.id,
|
|
reactable_type: comment.class.polymorphic_name,
|
|
category: "vomit"
|
|
}
|
|
end
|
|
|
|
it "also destroys contradictory mod reactions" do
|
|
expect(result).to be_success
|
|
expect(result.action).to eq("create")
|
|
expect(Reaction.ids).not_to include(contradictory_mod.id)
|
|
end
|
|
end
|
|
end
|
|
end
|