docbrown/spec/models/notification_spec.rb
gverger c7de0d762b Add let_it_be_readonly to avoid updating variables (#5407)
With let_it_be, we define variables that are created for the whole
context. In case we update them in an example, there might be a
discrepancy between the object and its value in the database, since the
database is cleaned between examples, but not the object itself.

This is what happened here with the `article` object. In one of the
tests, we update its organization, and in another, we reupdate it. The
problem is that the `article` object still thinks it has an
organization, but it has been cleaned in the database. So setting the
same organization id to the object doesn't trigger the SQL query, and
the article in the database still doesn't have an organization attached.

One solution is to use `reload: true` as a parameter of `let_it_be`: it
will reload the object from the database at the beginning of each test. To
make sure we don't have other dormant traps like this one, what I
propose is to either have `reload: true`, or to have the object
readonly to avoid getting to a difference between the object and its
database value.
2020-01-16 10:53:30 -05:00

520 lines
21 KiB
Ruby

require "rails_helper"
RSpec.describe Notification, type: :model do
let_it_be_readonly(:user) { create(:user) }
let_it_be_readonly(:user2) { create(:user) }
let_it_be_readonly(:user3) { create(:user) }
let_it_be_readonly(:organization) { create(:organization) }
let_it_be_changeable(:article) do
create(:article, :with_notification_subscription, user: user, page_views_count: 4000, positive_reactions_count: 70)
end
let_it_be_readonly(:user_follows_user2) { user.follow(user2) }
let_it_be_changeable(:comment) { create(:comment, user: user2, commentable: article) }
let_it_be_readonly(:badge_achievement) { create(:badge_achievement) }
it do
scopes = %i[organization_id notifiable_id notifiable_type action]
# rubocop:disable RSpec/NamedSubject
expect(subject).to validate_uniqueness_of(:user_id).scoped_to(scopes)
# rubocop:enable RSpec/NamedSubject
end
describe "when trying to create duplicate notifications" do
# Duplicate notifications are not allowed even when validations are skipped
it "doesn't allow to create a duplicate notification via import" do
create(:notification, user: user, notifiable: article, action: "Reaction")
duplicate_notification = build(:notification, user: user, notifiable: article, action: "Reaction")
expect do
described_class.import([duplicate_notification],
on_duplicate_key_update: {
conflict_target: %i[notifiable_id notifiable_type user_id action],
index_predicate: "action IS NOT NULL",
columns: %i[json_data notified_at read]
})
end.not_to change(described_class, :count)
end
it "updates when trying to create a duplicate notification via import" do
notification = create(
:notification, user: user, notifiable: article, action: "Reaction", json_data: { "user_id" => 1 }
)
duplicate_notification = build(
:notification, user: user, notifiable: article, action: "Reaction", json_data: { "user_id" => 2 }
)
described_class.import([duplicate_notification],
on_duplicate_key_update: {
conflict_target: %i[notifiable_id notifiable_type user_id action],
index_predicate: "action IS NOT NULL",
columns: %i[json_data notified_at read]
})
notification.reload
expect(notification.json_data["user_id"]).to eq(2)
end
it "doesn't allow to create a duplicate organization notification via import" do
create(:notification, organization: organization, notifiable: article, action: "Reaction")
duplicate_notification = build(:notification, organization: organization, notifiable: article, action: "Reaction")
expect do
described_class.import([duplicate_notification],
on_duplicate_key_update: {
conflict_target: %i[notifiable_id notifiable_type organization_id action],
index_predicate: "action IS NOT NULL",
columns: %i[json_data notified_at read]
})
end.not_to change(described_class, :count)
end
describe "when notifiable is a Comment" do
let!(:comment) { create(:comment, commentable: article) }
it "doesn't allow to create a duplicate user notification via import when action is nil" do
notification_attributes = { user: user, notifiable: comment, action: nil }
create(:notification, notification_attributes)
duplicate_notification = build(:notification, notification_attributes)
expect do
described_class.import([duplicate_notification],
on_duplicate_key_update: {
conflict_target: %i[notifiable_id notifiable_type user_id],
index_predicate: "action IS NULL",
columns: %i[json_data notified_at read]
})
end.not_to change(described_class, :count)
end
it "doesn't allow to create a duplicate org notification via import when action is nil" do
notification_attributes = { organization: organization, notifiable: comment, action: nil }
create(:notification, notification_attributes)
duplicate_notification = build(:notification, notification_attributes)
expect do
described_class.import([duplicate_notification],
on_duplicate_key_update: {
conflict_target: %i[notifiable_id notifiable_type organization_id],
index_predicate: "action IS NULL",
columns: %i[json_data notified_at read]
})
end.not_to change(described_class, :count)
end
end
end
context "when callbacks are triggered after create" do
it "sets the notified_at column" do
notification = create(:notification, notifiable: article, user: user2)
expect(notification.notified_at).to be_present
end
end
describe "#send_new_follower_notification" do
context "when trying to a send notification after following a tag" do
it "does not enqueue a notification job" do
assert_no_enqueued_jobs(only: Notifications::NewFollowerJob) do
tag_follow = user.follow(create(:tag))
described_class.send_new_follower_notification(tag_follow)
end
end
end
context "when trying to send a notification after following a user" do
it "creates a notification belonging to the person being followed" do
perform_enqueued_jobs do
expect do
described_class.send_new_follower_notification(user_follows_user2)
end.to change(user2.notifications, :count).by(1)
end
end
it "creates a notification from the follow instance" do
perform_enqueued_jobs do
described_class.send_new_follower_notification(user_follows_user2)
end
notification = user2.notifications.last
notifiable_data = { notifiable_id: notification.notifiable_id, notifiable_type: notification.notifiable_type }
follow_data = { notifiable_id: user_follows_user2.id, notifiable_type: user_follows_user2.class.name }
expect(notifiable_data).to eq(follow_data)
end
end
context "when a user follows an organization" do
let_it_be_readonly(:user_follows_organization) { user.follow(organization) }
it "creates a notification belonging to the organization" do
perform_enqueued_jobs do
expect do
described_class.send_new_follower_notification(user_follows_organization)
end.to change(organization.notifications, :count).by(1)
end
end
it "does not create a notification belonging to a user" do
perform_enqueued_jobs do
described_class.send_new_follower_notification(user_follows_organization)
expect(organization.notifications.last.user_id).to be(nil)
end
end
it "creates a notification from the follow instance" do
perform_enqueued_jobs do
described_class.send_new_follower_notification(user_follows_organization)
end
notification = organization.notifications.last
notifiable_data = { notifiable_id: notification.notifiable_id, notifiable_type: notification.notifiable_type }
follow_data = {
notifiable_id: user_follows_organization.id,
notifiable_type: user_follows_organization.class.name
}
expect(notifiable_data).to eq(follow_data)
end
end
context "when a user unfollows another user" do
it "destroys the follow notification" do
# first we follow the user
perform_enqueued_jobs do
described_class.send_new_follower_notification(user_follows_user2)
end
# then we stop following them
perform_enqueued_jobs do
expect do
user_stops_following_user2 = user.stop_following(user2)
described_class.send_new_follower_notification(user_stops_following_user2)
end.to change(user2.notifications, :count).by(-1)
end
end
end
end
describe "#send_new_comment_notifications" do
let_it_be_changeable(:comment) { create(:comment, user: user2, commentable: article) }
let_it_be_readonly(:child_comment) { create(:comment, user: user3, commentable: article, parent: comment) }
context "when all commenters are subscribed" do
it "sends a notification to the author of the article" do
expect do
described_class.send_new_comment_notifications_without_delay(comment)
end.to change(user.notifications, :count).by(1)
end
it "does not send a notification to the author of the article if the commenter is the author" do
comment = create(:comment, user: user, commentable: article)
expect do
described_class.send_new_comment_notifications_without_delay(comment)
end.to change(user.notifications, :count).by(0)
end
it "does not send a notification to the author of the comment" do
expect do
described_class.send_new_comment_notifications_without_delay(comment)
end.to change(user2.notifications, :count).by(0)
end
it "sends a notification to the author of the article about the child comment" do
expect do
described_class.send_new_comment_notifications_without_delay(child_comment)
end.to change(user.notifications, :count).by(1)
end
it "sends a notification to the organization" do
create(:organization_membership, user: user, organization: organization)
article.update(organization: organization)
comment = create(:comment, user: user2, commentable: article)
expect do
described_class.send_new_comment_notifications_without_delay(comment)
end.to change(organization.notifications, :count).by(1)
end
end
context "when the author of the article is not subscribed" do
before do
article.update(receive_notifications: false)
article.notification_subscriptions.delete_all
end
it "does not send a notification to the author of the article" do
expect do
described_class.send_new_comment_notifications_without_delay(comment)
end.to change(user.notifications, :count).by(0)
end
it "doesn't send a notification to the author of the article about the child comment" do
expect do
described_class.send_new_comment_notifications_without_delay(child_comment)
end.to change(user.notifications, :count).by(0)
end
end
context "when the author of a comment is not subscribed" do
before do
comment.update(receive_notifications: false)
end
it "does not send a notification to the author of the comment" do
expect do
described_class.send_new_comment_notifications_without_delay(child_comment)
end.to change(child_comment.user.notifications, :count).by(0)
end
it "sends a notification to the author of the article" do
expect do
described_class.send_new_comment_notifications_without_delay(child_comment)
end.to change(user.notifications, :count).by(1)
end
end
end
describe "#send_reaction_notification" do
before do
article.update(receive_notifications: true)
comment.update(receive_notifications: true)
end
context "when reactable is receiving notifications" do
it "sends a notification to the author of a comment" do
reaction = create(:reaction, reactable: comment, user: user)
expect do
sidekiq_perform_enqueued_jobs do
described_class.send_reaction_notification(reaction, reaction.reactable.user)
end
end.to change(comment.user.notifications, :count).by(1)
end
it "sends a notification to the author of an article" do
reaction = create(:reaction, reactable: article, user: user2)
expect do
sidekiq_perform_enqueued_jobs do
described_class.send_reaction_notification(reaction, reaction.reactable.user)
end
end.to change(article.user.notifications, :count).by(1)
end
end
context "when a reaction is destroyed" do
let(:comment) { create(:comment, user: user2, commentable: article) }
let!(:notification) { create(:notification, user: user, notifiable: comment, action: "Reaction") }
let(:sibling_reaction) { create(:reaction, reactable: comment, user: user3) }
it "destroys the notification if it exists" do
reaction = create(:reaction, reactable: comment, user: user)
reaction.destroy
described_class.send_reaction_notification_without_delay(reaction, article.user)
expect(described_class.exists?(id: notification.id)).to be(false)
end
it "keeps the notification if siblings exist" do
reaction = create(:reaction, reactable: comment, user: user)
sibling_reaction # to create it
reaction.destroy
described_class.send_reaction_notification_without_delay(reaction, article.user)
notification.reload
expect(notification).to be_persisted
end
it "doesn't keep data of the destroyed reaction in the notification" do
reaction = create(:reaction, reactable: comment, user: user)
sibling_reaction # to create it
reaction.destroy
described_class.send_reaction_notification_without_delay(reaction, article.user)
notification.reload
expect(notification.json_data["reaction"]["aggregated_siblings"].map { |s| s["user"]["id"] }).to eq([user3.id])
# not the user of the destroyed reaction!
expect(notification.json_data["user"]["id"]).to eq(user3.id)
end
it "creates and destroys the notification properly" do
reaction = create(:reaction, user: user2, reactable: article, category: "like")
expect do
sidekiq_perform_enqueued_jobs do
described_class.send_reaction_notification(reaction, reaction.reactable.user)
end
end.to change(user.notifications, :count).by(1)
reaction.destroy!
expect do
sidekiq_perform_enqueued_jobs do
described_class.send_reaction_notification(reaction, reaction.reactable.user)
end
end.to change(user.notifications, :count).by(-1)
end
end
context "when reactable is not receiving notifications" do
it "does not send a notification to the author of a comment" do
comment = create(:comment, user: user2, commentable: article)
comment.update(receive_notifications: false)
reaction = create(:reaction, reactable: comment, user: user)
expect do
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
end.to change(user.notifications, :count).by(0)
end
it "does not send a notification to the author of an article" do
article.update(receive_notifications: false)
reaction = create(:reaction, reactable: article, user: user2)
expect do
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
end.to change(user.notifications, :count).by(0)
end
end
context "when the reactable is an organization's article" do
before do
create(:organization_membership, user: user, organization: organization, type_of_user: "admin")
article.update(organization: organization)
end
it "creates a notification with the organization's ID" do
reaction = create(:reaction, reactable: article, user: user2)
expect do
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.organization)
end.to change(organization.notifications, :count).by(1)
end
end
context "when dealing with positive and negative reactions" do
it "creates a notification for a positive reaction" do
reaction = create(:reaction, reactable: article, user: user2, category: "like")
expect do
sidekiq_perform_enqueued_jobs do
described_class.send_reaction_notification(reaction, reaction.reactable.user)
end
end.to change(article.notifications, :count).by(1)
end
it "does not create a notification for a negative reaction" do
user2.add_role(:trusted)
reaction = create(:reaction, reactable: article, user: user2, category: "vomit")
expect do
sidekiq_perform_enqueued_jobs do
described_class.send_reaction_notification(reaction, reaction.reactable.user)
end
end.to change(article.notifications, :count).by(0)
end
end
end
describe "#send_to_followers" do
context "when the notifiable is an article from a user" do
it "sends a notification to the author's followers" do
user2.follow(user)
perform_enqueued_jobs do
expect do
described_class.send_to_followers(article, "Published")
end.to change(user2.notifications, :count).by(1)
end
end
end
context "when the notifiable is an article from an organization" do
let_it_be_readonly(:org_article) { create(:article, organization: organization, user: user) }
it "sends a notification to author's followers" do
user2.follow(user)
perform_enqueued_jobs do
expect do
described_class.send_to_followers(org_article, "Published")
end.to change(user2.notifications, :count).by(1)
end
end
it "sends a notification to the organization's followers" do
user3.follow(organization)
perform_enqueued_jobs do
expect do
described_class.send_to_followers(org_article, "Published")
end.to change(user3.notifications, :count).by(1)
end
end
end
end
describe "#update_notifications" do
context "when there are article notifications to update" do
before do
user2.follow(user)
perform_enqueued_jobs { described_class.send_to_followers(article, "Published") }
end
it "updates the notification with the new article title" do
new_title = "hehehe hohoho!"
article.update_attribute(:title, new_title)
perform_enqueued_jobs do
described_class.update_notifications(article, "Published")
expected_notification_article_title = user2.notifications.last.json_data["article"]["title"]
expect(expected_notification_article_title).to eq(new_title)
end
end
it "adds organization data when the article now belongs to an org" do
article.update(organization_id: organization.id)
perform_enqueued_jobs do
described_class.update_notifications(article, "Published")
expected_notification_organization_id = described_class.last.json_data["organization"]["id"]
expect(expected_notification_organization_id).to eq(organization.id)
end
end
end
end
describe "#aggregated?" do
let_it_be_readonly(:notification) { build(:notification) }
it "returns true if a notification's action is 'Reaction'" do
notification.action = "Reaction"
expect(notification.aggregated?).to be(true)
end
it "returns true if a notification's action is 'Follow'" do
notification.action = "Follow"
expect(notification.aggregated?).to be(true)
end
it "returns false if a notification's action is not 'Reaction' or 'Follow'" do
notification.action = "Published"
expect(notification.aggregated?).to be(false)
end
end
describe "#send_new_badge_achievement_notification" do
it "enqueues a new badge achievement job" do
sidekiq_assert_enqueued_with(job: Notifications::NewBadgeAchievementWorker, args: [badge_achievement.id]) do
described_class.send_new_badge_achievement_notification(badge_achievement)
end
end
end
describe "#remove_all" do
it "removes all mention related notifications" do
mention = create(:mention, user: user, mentionable: comment)
create(:notification, user: mention.user, notifiable: mention)
expect do
sidekiq_perform_enqueued_jobs do
described_class.remove_all(notifiable_ids: mention.id, notifiable_type: "Mention")
end
end.to change(user.notifications, :count).by(-1)
end
end
describe "#fast_destroy_old_notifications" do
it "bulk deletes notifications older than given timestamp" do
allow(BulkSqlDelete).to receive(:delete_in_batches)
described_class.fast_destroy_old_notifications("a_time")
expect(BulkSqlDelete).to have_received(:delete_in_batches).with(a_string_including("< 'a_time'"))
end
end
end