discourse/spec/services/post_alerter_spec.rb
Osama Sayegh 43ef44127c
UX: Send notification of type replied to topic author if they're watching the topic (#18684)
Related to aeee7ed.

Before the change in aeee7ed, notifications for direct replies to your posts and notifications for replies in watched topics looked the same in the notifications menu -- they both used the arrow icon.

We decided in aeee7ed to distinguish them by changing "watched topics" notifications to use the bell icon because it was confusing for users who watch topics to see the same icon for direct replies and "watched topics". However, that change also means that non-power/new users who receive replies to topics _they create_ will get notifications with the bell icon because technically they're watching the topic, but the arrow icon is more appropriate for this case because we use it throughout the app to indicate "replies".

This commit adds a special-case so that if a user is watching a topic AND the topic is created by them, they receive notifications with the arrow icon (type `replied`) instead of the bell icon (type `posted`) for new posts in the topic.

Internal topic: t/79051.
2022-10-25 11:53:35 +03:00

2095 lines
83 KiB
Ruby

# frozen_string_literal: true
RSpec::Matchers.define :add_notification do |user, notification_type|
match(notify_expectation_failures: true) do |actual|
notifications = user.notifications
before = notifications.count
actual.call
expect(notifications.count).to eq(before + 1), "expected 1 new notification, got #{notifications.count - before}"
last_notification_type = notifications.last.notification_type
expect(last_notification_type).to eq(Notification.types[notification_type]),
"expected notification type to be '#{notification_type}', got '#{Notification.types.key(last_notification_type)}'"
end
match_when_negated do |actual|
expect { actual.call }.to_not change { user.notifications.where(notification_type: Notification.types[notification_type]).count }
end
supports_block_expectations
end
RSpec::Matchers.define_negated_matcher :not_add_notification, :add_notification
RSpec.describe PostAlerter do
fab!(:category) { Fabricate(:category) }
fab!(:topic) { Fabricate(:topic) }
fab!(:post) { Fabricate(:post) }
fab!(:private_message_topic) { Fabricate(:private_message_topic) }
fab!(:private_message_topic_post1) { Fabricate(:post, topic: private_message_topic) }
fab!(:private_message_topic_post2) { Fabricate(:post, topic: private_message_topic) }
fab!(:group) { Fabricate(:group) }
fab!(:admin) { Fabricate(:admin) }
fab!(:evil_trout) { Fabricate(:evil_trout) }
fab!(:coding_horror) { Fabricate(:coding_horror) }
fab!(:walterwhite) { Fabricate(:walter_white) }
fab!(:user) { Fabricate(:user) }
fab!(:tl2_user) { Fabricate(:user, trust_level: TrustLevel[2]) }
def create_post_with_alerts(args = {})
post = Fabricate(:post, args)
PostAlerter.post_created(post)
end
context "with private message" do
it "notifies for pms correctly" do
pm = Fabricate(:topic, archetype: 'private_message', category_id: nil)
op = Fabricate(:post, user: pm.user)
pm.allowed_users << pm.user
PostAlerter.post_created(op)
reply = Fabricate(:post, user: pm.user, topic: pm, reply_to_post_number: 1)
PostAlerter.post_created(reply)
reply2 = Fabricate(:post, topic: pm, reply_to_post_number: 1)
PostAlerter.post_created(reply2)
# we get a green notification for a reply
expect(Notification.where(user_id: pm.user_id).pluck_first(:notification_type)).to eq(Notification.types[:private_message])
TopicUser.change(pm.user_id, pm.id, notification_level: TopicUser.notification_levels[:tracking])
Notification.destroy_all
reply3 = Fabricate(:post, topic: pm)
PostAlerter.post_created(reply3)
# no notification cause we are tracking
expect(Notification.where(user_id: pm.user_id).count).to eq(0)
Notification.destroy_all
reply4 = Fabricate(:post, topic: pm, reply_to_post_number: 1)
PostAlerter.post_created(reply4)
# yes notification cause we were replied to
expect(Notification.where(user_id: pm.user_id).count).to eq(1)
end
context "with group inboxes" do
fab!(:user1) { Fabricate(:user) }
fab!(:user2) { Fabricate(:user) }
fab!(:group) { Fabricate(:group, users: [user2], name: "TestGroup", default_notification_level: 2) }
fab!(:pm) { Fabricate(:topic, archetype: 'private_message', category_id: nil, allowed_groups: [group]) }
fab!(:op) { Fabricate(:post, user: pm.user, topic: pm) }
it "triggers :before_create_notifications_for_users" do
pm.allowed_users << user1
events = DiscourseEvent.track_events do
PostAlerter.post_created(op)
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user1], op])
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user2], op])
end
it "triggers group summary notification" do
Jobs.run_immediately!
TopicUser.change(user2.id, pm.id, notification_level: TopicUser.notification_levels[:tracking])
PostAlerter.post_created(op)
group_summary_notification = Notification.where(user_id: user2.id)
expect(group_summary_notification.count).to eq(1)
expect(group_summary_notification.first.notification_type).to eq(Notification.types[:group_message_summary])
notification_payload = JSON.parse(group_summary_notification.first.data)
expect(notification_payload["group_name"]).to eq(group.name)
expect(notification_payload["inbox_count"]).to eq(1)
# archiving the only PM clears the group summary notification
GroupArchivedMessage.archive!(group.id, pm)
expect(Notification.where(user_id: user2.id)).to be_blank
# moving to inbox the only PM restores the group summary notification
GroupArchivedMessage.move_to_inbox!(group.id, pm)
group_summary_notification = Notification.where(user_id: user2.id)
expect(group_summary_notification.first.notification_type).to eq(Notification.types[:group_message_summary])
updated_payload = JSON.parse(group_summary_notification.first.data)
expect(updated_payload["group_name"]).to eq(group.name)
expect(updated_payload["inbox_count"]).to eq(1)
# adding a second PM updates the count
pm2 = Fabricate(:topic, archetype: 'private_message', category_id: nil, allowed_groups: [group])
op2 = Fabricate(:post, user: pm2.user, topic: pm2)
TopicUser.change(user2.id, pm2.id, notification_level: TopicUser.notification_levels[:tracking])
PostAlerter.post_created(op2)
group_summary_notification = Notification.where(user_id: user2.id)
updated_payload = JSON.parse(group_summary_notification.first.data)
expect(updated_payload["group_name"]).to eq(group.name)
expect(updated_payload["inbox_count"]).to eq(2)
# archiving the second PM quietly updates the group summary count for the acting user
GroupArchivedMessage.archive!(group.id, pm2, acting_user_id: user2.id)
group_summary_notification = Notification.where(user_id: user2.id)
expect(group_summary_notification.first.read).to eq(true)
updated_payload = JSON.parse(group_summary_notification.first.data)
expect(updated_payload["inbox_count"]).to eq(1)
# moving to inbox the second PM quietly updates the group summary count for the acting user
GroupArchivedMessage.move_to_inbox!(group.id, pm2, acting_user_id: user2.id)
group_summary_notification = Notification.where(user_id: user2.id)
expect(group_summary_notification.first.read).to eq(true)
updated_payload = JSON.parse(group_summary_notification.first.data)
expect(updated_payload["group_name"]).to eq(group.name)
expect(updated_payload["inbox_count"]).to eq(2)
end
it 'updates the consolidated group summary inbox count and bumps the notification' do
user2.update!(last_seen_at: 5.minutes.ago)
TopicUser.change(user2.id, pm.id, notification_level: TopicUser.notification_levels[:tracking])
PostAlerter.post_created(op)
starting_count = Notification.where(
user_id: user2.id,
notification_type: Notification.types[:group_message_summary]
).pluck("data::json ->> 'inbox_count'").last.to_i
another_pm = Fabricate(:topic, archetype: 'private_message', category_id: nil, allowed_groups: [group])
another_post = Fabricate(:post, user: another_pm.user, topic: another_pm)
TopicUser.change(user2.id, another_pm.id, notification_level: TopicUser.notification_levels[:tracking])
message_data = MessageBus.track_publish("/notification/#{user2.id}") do
PostAlerter.post_created(another_post)
end.first.data
expect(Notification.where(user: user2).count).to eq(1)
expect(message_data.dig(:last_notification, :notification, :data, :inbox_count)).to eq(starting_count + 1)
expect(message_data[:unread_notifications]).to eq(1)
end
it 'sends a PM notification when replying to a member tracking the topic' do
group.add(user1)
post = Fabricate(:post, topic: pm, user: user1)
TopicUser.change(user1.id, pm.id, notification_level: TopicUser.notification_levels[:tracking])
expect {
create_post_with_alerts(
raw: 'this is a reply to your post...', topic: pm, user: user2,
reply_to_post_number: post.post_number
)
}.to change(
user1.notifications.where(notification_type: Notification.types[:private_message]),
:count
).by(1)
end
it 'notifies a group member if someone replies to their post' do
group.add(user1)
post = Fabricate(:post, topic: pm, user: user1)
TopicUser.change(user1.id, pm.id, notification_level: TopicUser.notification_levels[:regular])
expect {
create_post_with_alerts(
raw: 'this is a reply to your post...', topic: pm, user: user2,
reply_to_post_number: post.post_number
)
}.to change(user1.notifications, :count).by(1)
end
it 'notifies a group member if someone quotes their post' do
group.add(user1)
post = Fabricate(:post, topic: pm, user: user1)
TopicUser.change(user1.id, pm.id, notification_level: TopicUser.notification_levels[:regular])
quote_raw = <<~MD
[quote="#{user1.username}, post:1, topic:#{pm.id}"]#{post.raw}[/quote]
MD
expect {
create_post_with_alerts(
raw: quote_raw, topic: pm, user: user2,
)
}.to change(user1.notifications, :count).by(1)
end
it "Doesn't notify non-admin users when their post is quoted inside a whisper" do
group.add(admin)
TopicUser.change(user2.id, pm.id, notification_level: TopicUser.notification_levels[:regular])
quote_raw = <<~MD
[quote="#{user2.username}, post:1, topic:#{pm.id}"]#{op.raw}[/quote]
MD
expect {
create_post_with_alerts(
raw: quote_raw, topic: pm, user: admin, post_type: Post.types[:whisper]
)
}.not_to change(user2.notifications, :count)
end
end
end
context "with unread" do
it "does not return whispers as unread posts" do
_whisper = Fabricate(:post, raw: 'this is a whisper post',
user: admin,
topic: post.topic,
reply_to_post_number: post.post_number,
post_type: Post.types[:whisper])
expect(PostAlerter.new.first_unread_post(post.user, post.topic)).to be_blank
end
end
context 'with edits' do
it 'notifies correctly on edits' do
Jobs.run_immediately!
PostActionNotifier.enable
post = Fabricate(:post, raw: 'I love waffles')
expect do
post.revise(admin, raw: 'I made a revision')
end.to add_notification(post.user, :edited)
# lets also like this post which should trigger a notification
expect do
PostActionCreator.new(
admin,
post,
PostActionType.types[:like]
).perform
end.to add_notification(post.user, :liked)
# skip this notification cause we already notified on an edit by the same user
# in the previous edit
freeze_time 2.hours.from_now
expect do
post.revise(admin, raw: 'I made another revision')
end.to_not change { Notification.count }
# this we do not skip cause 1 day has passed
freeze_time 23.hours.from_now
expect do
post.revise(admin, raw: 'I made another revision xyz')
end.to add_notification(post.user, :edited)
expect do
post.revise(Fabricate(:admin), raw: 'I made a revision')
end.to add_notification(post.user, :edited)
freeze_time 2.hours.from_now
expect do
post.revise(admin, raw: 'I made another revision')
end.to add_notification(post.user, :edited)
end
it 'notifies flaggers when flagged post gets unhidden by edit' do
post = create_post
PostActionNotifier.enable
Reviewable.set_priorities(high: 4.0)
SiteSetting.hide_post_sensitivity = Reviewable.sensitivities[:low]
PostActionCreator.spam(evil_trout, post)
PostActionCreator.spam(walterwhite, post)
post.reload
expect(post.hidden).to eq(true)
expect {
post.revise(post.user, raw: post.raw + " ha I edited it ")
}.to add_notification(evil_trout, :edited)
.and add_notification(walterwhite, :edited)
post.reload
expect(post.hidden).to eq(false)
notification = walterwhite.notifications.last
expect(notification.topic_id).to eq(post.topic.id)
expect(notification.post_number).to eq(post.post_number)
expect(notification.data_hash["display_username"]).to eq(post.user.username)
PostActionCreator.create(coding_horror, post, :spam)
PostActionCreator.create(walterwhite, post, :off_topic)
post.reload
expect(post.hidden).to eq(true)
expect {
post.revise(post.user, raw: post.raw + " ha I edited it again ")
}.to not_add_notification(evil_trout, :edited)
.and not_add_notification(coding_horror, :edited)
.and not_add_notification(walterwhite, :edited)
end
end
context 'with quotes' do
fab!(:category) { Fabricate(:category) }
fab!(:topic) { Fabricate(:topic, category: category) }
it 'does not notify for muted users' do
post = Fabricate(:post, raw: '[quote="Eviltrout, post:1"]whatup[/quote]', topic: topic)
MutedUser.create!(user_id: evil_trout.id, muted_user_id: post.user_id)
expect {
PostAlerter.post_created(post)
}.not_to change(evil_trout.notifications, :count)
end
it 'does not notify for ignored users' do
post = Fabricate(:post, raw: '[quote="EvilTrout, post:1"]whatup[/quote]', topic: topic)
Fabricate(:ignored_user, user: evil_trout, ignored_user: post.user)
expect {
PostAlerter.post_created(post)
}.not_to change(evil_trout.notifications, :count)
end
it 'does not notify for users with new reply notification' do
post = Fabricate(:post, raw: '[quote="eviltRout, post:1"]whatup[/quote]', topic: topic)
notification = Notification.create!(topic: post.topic,
post_number: post.post_number,
read: false,
notification_type: Notification.types[:replied],
user: evil_trout,
data: { topic_title: "test topic" }.to_json
)
expect {
PostAlerter.post_edited(post)
}.not_to change(evil_trout.notifications, :count)
notification.destroy
expect {
PostAlerter.post_edited(post)
}.to change(evil_trout.notifications, :count).by(1)
end
it 'does not collapse quote notifications' do
expect {
2.times do
create_post_with_alerts(
raw: '[quote="eviltrout, post:1"]whatup[/quote]',
topic: topic
)
end
}.to change(evil_trout.notifications, :count).by(2)
end
it "won't notify the user a second time on revision" do
p1 = create_post_with_alerts(raw: '[quote="Evil Trout, post:1"]whatup[/quote]')
expect {
p1.revise(p1.user, raw: '[quote="Evil Trout, post:1"]whatup now?[/quote]')
}.not_to change(evil_trout.notifications, :count)
end
it "doesn't notify the poster" do
topic = create_post_with_alerts.topic
expect {
Fabricate(:post, topic: topic, user: topic.user, raw: '[quote="Bruce Wayne, post:1"]whatup[/quote]')
}.not_to change(topic.user.notifications, :count)
end
it "triggers :before_create_notifications_for_users" do
post = Fabricate(:post, raw: '[quote="eviltrout, post:1"]whatup[/quote]')
events = DiscourseEvent.track_events do
PostAlerter.post_created(post)
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[evil_trout], post])
end
context "with notifications when prioritizing full names" do
before do
SiteSetting.prioritize_username_in_ux = false
SiteSetting.display_name_on_posts = true
end
it 'sends to correct user' do
quote = <<~MD
[quote="#{evil_trout.name}, post:1, username:#{evil_trout.username}"]whatup[/quote]
MD
expect {
create_post_with_alerts(
raw: quote,
topic: topic
)
}.to change(evil_trout.notifications, :count).by(1)
end
it 'sends to correct users when nested quotes with multiple users' do
quote = <<~MD
[quote="#{evil_trout.name}, post:1, username:#{evil_trout.username}"]this [quote="#{walterwhite.name}, post:2, username:#{walterwhite.username}"]whatup[/quote][/quote]
MD
expect {
create_post_with_alerts(
raw: quote,
topic: topic
)
}.to change(evil_trout.notifications, :count).by(1)
.and change(walterwhite.notifications, :count).by(1)
end
it 'sends to correct users when multiple quotes' do
user = Fabricate(:user)
quote = <<~MD
[quote="#{evil_trout.name}, post:1, username:#{evil_trout.username}"]"username:#{user.username}" [/quote]/n [quote="#{walterwhite.name}, post:2, username:#{walterwhite.username}"]whatup[/quote]
MD
expect {
create_post_with_alerts(
raw: quote,
topic: topic
)
}.to change(evil_trout.notifications, :count).by(1)
.and change(walterwhite.notifications, :count).by(1)
.and not_change(user.notifications, :count)
end
it "sends to correct user when user has a full name that matches another user's username" do
user_with_matching_full_name = Fabricate(:user, name: evil_trout.username)
quote = <<~MD
[quote="#{user_with_matching_full_name.name}, post:1, username:#{user_with_matching_full_name.username}"]this [/quote]
MD
expect {
create_post_with_alerts(
raw: quote,
topic: topic
)
}.to change(user_with_matching_full_name.notifications, :count).by(1)
.and not_change(evil_trout.notifications, :count)
end
end
end
context 'with linked' do
let(:post1) { create_post }
let(:user) { post1.user }
let(:linking_post) { create_post(raw: "my magic topic\n##{Discourse.base_url}#{post1.url}") }
before do
Jobs.run_immediately!
end
it "will notify correctly on linking" do
linking_post
expect(user.notifications.count).to eq(1)
watcher = Fabricate(:user)
TopicUser.create!(user_id: watcher.id, topic_id: topic.id, notification_level: TopicUser.notification_levels[:watching])
create_post(topic_id: topic.id, user: user, raw: "my magic topic\n##{Discourse.base_url}#{post1.url}")
user.reload
expect(user.notifications.where(notification_type: Notification.types[:linked]).count).to eq(1)
expect(watcher.notifications.count).to eq(1)
# don't notify on reflection
post1.reload
expect(PostAlerter.new.extract_linked_users(post1).length).to eq(0)
end
it "triggers :before_create_notifications_for_users" do
events = DiscourseEvent.track_events do
linking_post
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user], linking_post])
end
it "doesn't notify the linked user if the user is staged and the category is restricted and allows strangers" do
staged_user = Fabricate(:staged)
group_member = Fabricate(:user)
group.add(group_member)
private_category = Fabricate(
:private_category, group: group,
email_in: 'test@test.com', email_in_allow_strangers: true
)
staged_user_post = create_post(user: staged_user, category: private_category)
linking = create_post(
user: group_member,
category: private_category,
raw: "my magic topic\n##{Discourse.base_url}#{staged_user_post.url}")
staged_user.reload
expect(staged_user.notifications.where(notification_type: Notification.types[:linked]).count).to eq(0)
end
end
context 'with @here' do
let(:post) { create_post_with_alerts(raw: "Hello @here how are you?", user: tl2_user, topic: topic) }
fab!(:other_post) { Fabricate(:post, topic: topic) }
before do
Jobs.run_immediately!
end
it 'does not notify unrelated users' do
expect { post }.not_to change(evil_trout.notifications, :count)
end
it 'does not work if user here exists' do
Fabricate(:user, username: SiteSetting.here_mention)
expect { post }.not_to change(other_post.user.notifications, :count)
end
it 'notifies users who replied' do
post2 = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
post3 = Fabricate(:post, topic: topic)
expect { post }
.to change(other_post.user.notifications, :count).by(1)
.and not_change(post2.user.notifications, :count)
.and change(post3.user.notifications, :count).by(1)
end
it 'notifies users who whispered' do
post2 = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
post3 = Fabricate(:post, topic: topic)
tl2_user.grant_admin!
expect { post }
.to change(other_post.user.notifications, :count).by(1)
.and change(post2.user.notifications, :count).by(1)
.and change(post3.user.notifications, :count).by(1)
end
it 'notifies only last max_here_mentioned users' do
SiteSetting.max_here_mentioned = 2
3.times { Fabricate(:post, topic: topic) }
expect { post }.to change { Notification.count }.by(2)
end
end
context 'with @group mentions' do
fab!(:group) { Fabricate(:group, name: 'group', mentionable_level: Group::ALIAS_LEVELS[:everyone]) }
let(:post) { create_post_with_alerts(raw: "Hello @group how are you?") }
before { group.add(evil_trout) }
it 'notifies users correctly' do
expect {
post
}.to change(evil_trout.notifications, :count).by(1)
expect(GroupMention.count).to eq(1)
Fabricate(:group, name: 'group-alt', mentionable_level: Group::ALIAS_LEVELS[:everyone])
expect {
create_post_with_alerts(raw: "Hello, @group-alt should not trigger a notification?")
}.not_to change(evil_trout.notifications, :count)
expect(GroupMention.count).to eq(2)
group.update_columns(mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins])
expect {
create_post_with_alerts(raw: "Hello @group you are not mentionable")
}.not_to change(evil_trout.notifications, :count)
expect(GroupMention.count).to eq(3)
group.update_columns(mentionable_level: Group::ALIAS_LEVELS[:owners_mods_and_admins])
group.add_owner(user)
expect {
create_post_with_alerts(raw: "Hello @group the owner can mention you", user: user)
}.to change(evil_trout.notifications, :count).by(1)
expect(GroupMention.count).to eq(4)
end
it 'takes private mention as precedence' do
expect {
create_post_with_alerts(raw: "Hello @group and @eviltrout, nice to meet you")
}.to change(evil_trout.notifications, :count).by(1)
expect(evil_trout.notifications.last.notification_type).to eq(Notification.types[:mentioned])
end
it "triggers :before_create_notifications_for_users" do
events = DiscourseEvent.track_events do
post
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[evil_trout], post])
end
end
context 'with @mentions' do
let(:mention_post) { create_post_with_alerts(user: user, raw: 'Hello @eviltrout') }
let(:topic) { mention_post.topic }
before do
Jobs.run_immediately!
end
it 'notifies a user' do
expect {
mention_post
}.to change(evil_trout.notifications, :count).by(1)
end
it "won't notify the user a second time on revision" do
mention_post
expect {
mention_post.revise(mention_post.user, raw: "New raw content that still mentions @eviltrout")
}.not_to change(evil_trout.notifications, :count)
end
it "doesn't notify the user who created the topic in regular mode" do
topic.notify_regular!(user)
mention_post
expect {
create_post_with_alerts(user: user, raw: 'second post', topic: topic)
}.not_to change(user.notifications, :count)
end
it "triggers :before_create_notifications_for_users" do
events = DiscourseEvent.track_events do
mention_post
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[evil_trout], mention_post])
end
it "notification comes from editor if mention is added later" do
post = create_post_with_alerts(user: user, raw: 'No mention here.')
expect {
post.revise(admin, raw: "Mention @eviltrout in this edit.")
}.to change(evil_trout.notifications, :count)
n = evil_trout.notifications.last
expect(n.data_hash["original_username"]).to eq(admin.username)
end
it "doesn't notify the last post editor if they mention themselves" do
post = create_post_with_alerts(user: user, raw: 'Post without a mention.')
expect {
post.revise(evil_trout, raw: "O hai, @eviltrout!")
}.not_to change(evil_trout.notifications, :count)
end
fab!(:alice) { Fabricate(:user, username: 'alice') }
fab!(:bob) { Fabricate(:user, username: 'bob') }
fab!(:carol) { Fabricate(:admin, username: 'carol') }
fab!(:dave) { Fabricate(:user, username: 'dave') }
fab!(:eve) { Fabricate(:user, username: 'eve') }
fab!(:group) { Fabricate(:group, name: 'group', mentionable_level: Group::ALIAS_LEVELS[:everyone]) }
before do
group.bulk_add([alice.id, eve.id])
end
def create_post_with_alerts(args = {})
post = Fabricate(:post, args)
PostAlerter.post_created(post)
end
def set_topic_notification_level(user, topic, level_name)
TopicUser.change(user.id, topic.id, notification_level: TopicUser.notification_levels[level_name])
end
context "with topic" do
fab!(:topic) { Fabricate(:topic, user: alice) }
[:watching, :tracking, :regular].each do |notification_level|
context "when notification level is '#{notification_level}'" do
before do
set_topic_notification_level(alice, topic, notification_level)
end
it "notifies about @username mention" do
args = { user: bob, topic: topic, raw: 'Hello @alice' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :mentioned)
end
end
end
context "when notification level is 'muted'" do
before do
set_topic_notification_level(alice, topic, :muted)
end
it "does not notify about @username mention" do
args = { user: bob, topic: topic, raw: 'Hello @alice' }
expect { create_post_with_alerts(args) }.to_not add_notification(alice, :mentioned)
end
end
end
context "with message to users" do
fab!(:pm_topic) do
Fabricate(:private_message_topic,
user: alice,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: alice),
Fabricate.build(:topic_allowed_user, user: bob),
Fabricate.build(:topic_allowed_user, user: Discourse.system_user)
]
)
end
context "when user is part of conversation" do
[:watching, :tracking, :regular].each do |notification_level|
context "when notification level is '#{notification_level}'" do
before do
set_topic_notification_level(alice, pm_topic, notification_level)
end
it "notifies about @username mention" do
args = { user: bob, topic: pm_topic, raw: 'Hello @alice' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :mentioned)
end
it "notifies about @username mentions by non-human users" do
args = { user: Discourse.system_user, topic: pm_topic, raw: 'Hello @alice' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :mentioned)
end
it "notifies about @group mention when allowed user is part of group" do
args = { user: bob, topic: pm_topic, raw: 'Hello @group' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :group_mentioned)
end
end
end
context "when notification level is 'muted'" do
before do
set_topic_notification_level(alice, pm_topic, :muted)
end
it "does not notify about @username mention" do
args = { user: bob, topic: pm_topic, raw: 'Hello @alice' }
expect { create_post_with_alerts(args) }.to_not add_notification(alice, :mentioned)
end
end
end
context "when user is not part of conversation" do
it "does not notify about @username mention even though mentioned user is an admin" do
args = { user: bob, topic: pm_topic, raw: 'Hello @carol' }
expect { create_post_with_alerts(args) }.to_not add_notification(carol, :mentioned)
end
it "does not notify about @username mention by non-human user even though mentioned user is an admin" do
args = { user: Discourse.system_user, topic: pm_topic, raw: 'Hello @carol' }
expect { create_post_with_alerts(args) }.to_not add_notification(carol, :mentioned)
end
it "does not notify about @username mention when mentioned user is not allowed to see message" do
args = { user: bob, topic: pm_topic, raw: 'Hello @dave' }
expect { create_post_with_alerts(args) }.to_not add_notification(dave, :mentioned)
end
it "does not notify about @group mention when user is not an allowed user" do
args = { user: bob, topic: pm_topic, raw: 'Hello @group' }
expect { create_post_with_alerts(args) }.to_not add_notification(eve, :group_mentioned)
end
end
end
context "with message to group" do
fab!(:some_group) { Fabricate(:group, name: 'some_group', mentionable_level: Group::ALIAS_LEVELS[:everyone]) }
fab!(:pm_topic) do
Fabricate(:private_message_topic,
user: alice,
topic_allowed_groups: [
Fabricate.build(:topic_allowed_group, group: group)
],
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: Discourse.system_user)
]
)
end
before do
some_group.bulk_add([alice.id, carol.id])
end
context "when group is part of conversation" do
[:watching, :tracking, :regular].each do |notification_level|
context "when notification level is '#{notification_level}'" do
before do
set_topic_notification_level(alice, pm_topic, notification_level)
end
it "notifies about @group mention" do
args = { user: bob, topic: pm_topic, raw: 'Hello @group' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :group_mentioned)
end
it "notifies about @group mentions by non-human users" do
args = { user: Discourse.system_user, topic: pm_topic, raw: 'Hello @group' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :group_mentioned)
end
it "notifies about @username mention when user belongs to allowed group" do
args = { user: bob, topic: pm_topic, raw: 'Hello @alice' }
expect { create_post_with_alerts(args) }.to add_notification(alice, :mentioned)
end
end
end
context "when notification level is 'muted'" do
before do
set_topic_notification_level(alice, pm_topic, :muted)
end
it "does not notify about @group mention" do
args = { user: bob, topic: pm_topic, raw: 'Hello @group' }
expect { create_post_with_alerts(args) }.to_not add_notification(alice, :group_mentioned)
end
end
end
context "when group is not part of conversation" do
it "does not notify about @group mention even though mentioned user is an admin" do
args = { user: bob, topic: pm_topic, raw: 'Hello @some_group' }
expect { create_post_with_alerts(args) }.to_not add_notification(carol, :group_mentioned)
end
it "does not notify about @group mention by non-human user even though mentioned user is an admin" do
args = { user: Discourse.system_user, topic: pm_topic, raw: 'Hello @some_group' }
expect { create_post_with_alerts(args) }.to_not add_notification(carol, :group_mentioned)
end
it "does not notify about @username mention when user doesn't belong to allowed group" do
args = { user: bob, topic: pm_topic, raw: 'Hello @dave' }
expect { create_post_with_alerts(args) }.to_not add_notification(dave, :mentioned)
end
end
end
end
describe ".create_notification" do
fab!(:topic) { Fabricate(:private_message_topic, user: user, created_at: 1.hour.ago) }
fab!(:post) { Fabricate(:post, topic: topic, created_at: 1.hour.ago) }
let(:type) { Notification.types[:private_message] }
it "creates a notification for PMs" do
post.revise(user, { raw: 'This is the revised post' }, revised_at: Time.zone.now)
expect {
PostAlerter.new.create_notification(user, type, post)
}.to change { user.notifications.count }.by(1)
expect(user.notifications.last.data_hash["topic_title"]).to eq(topic.title)
end
it "keeps the original title for PMs" do
original_title = topic.title
post.revise(user, { title: "This is the revised title" }, revised_at: Time.now)
expect {
PostAlerter.new.create_notification(user, type, post)
}.to change { user.notifications.count }.by(1)
expect(user.notifications.last.data_hash["topic_title"]).to eq(original_title)
end
it "triggers :pre_notification_alert" do
events = DiscourseEvent.track_events do
PostAlerter.new.create_notification(user, type, post)
end
payload = {
notification_type: type,
post_number: post.post_number,
topic_title: post.topic.title,
topic_id: post.topic.id,
excerpt: post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true),
username: post.username,
post_url: post.url
}
expect(events).to include(event_name: :pre_notification_alert, params: [user, payload])
end
it "does not alert when revising and changing notification type" do
PostAlerter.new.create_notification(user, type, post)
post.revise(user, { raw: "Editing post to fake include a mention of @eviltrout" }, revised_at: Time.now)
events = DiscourseEvent.track_events do
PostAlerter.new.create_notification(user, Notification.types[:mentioned], post)
end
payload = {
notification_type: type,
post_number: post.post_number,
topic_title: post.topic.title,
topic_id: post.topic.id,
excerpt: post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true),
username: post.username,
post_url: post.url
}
expect(events).not_to include(event_name: :pre_notification_alert, params: [user, payload])
end
it "triggers :before_create_notification" do
type = Notification.types[:private_message]
events = DiscourseEvent.track_events do
PostAlerter.new.create_notification(user, type, post, {})
end
expect(events).to include(event_name: :before_create_notification, params: [user, type, post, {}])
end
end
describe ".push_notification" do
let(:mention_post) { create_post_with_alerts(user: user, raw: 'Hello @eviltrout :heart:') }
let(:topic) { mention_post.topic }
before do
SiteSetting.allowed_user_api_push_urls = "https://site.com/push|https://site2.com/push"
2.times do |i|
UserApiKey.create!(user_id: evil_trout.id,
client_id: "xxx#{i}",
application_name: "iPhone#{i}",
scopes: ['notifications'].map { |name| UserApiKeyScope.new(name: name) },
push_url: "https://site2.com/push")
end
end
describe "DiscoursePluginRegistry#push_notification_filters" do
it "sends push notifications when all filters pass" do
Plugin::Instance.new.register_push_notification_filter do |user, payload|
true
end
expect { mention_post }.to change { Jobs::PushNotification.jobs.count }.by(1)
DiscoursePluginRegistry.reset!
end
it "does not send push notifications when a filters returns false" do
Plugin::Instance.new.register_push_notification_filter do |user, payload|
false
end
expect { mention_post }.not_to change { Jobs::PushNotification.jobs.count }
DiscoursePluginRegistry.reset!
end
end
it "pushes nothing to suspended users" do
evil_trout.update_columns(suspended_till: 1.year.from_now)
expect { mention_post }.to_not change { Jobs::PushNotification.jobs.count }
end
it "pushes nothing when the user is in 'do not disturb'" do
Fabricate(:do_not_disturb_timing, user: evil_trout, starts_at: Time.zone.now, ends_at: 1.day.from_now)
expect { mention_post }.to_not change { Jobs::PushNotification.jobs.count }
end
it "correctly pushes notifications if configured correctly" do
Jobs.run_immediately!
body = nil
headers = nil
stub_request(:post, "https://site2.com/push")
.to_return do |request|
body = request.body
headers = request.headers
{ status: 200, body: "OK" }
end
set_subfolder "/subpath"
payload = {
"secret_key" => SiteSetting.push_api_secret_key,
"url" => Discourse.base_url,
"title" => SiteSetting.title,
"description" => SiteSetting.site_description,
"notifications" => [
{
'notification_type' => 1,
'post_number' => 1,
'topic_title' => topic.title,
'topic_id' => topic.id,
'excerpt' => 'Hello @eviltrout ❤',
'username' => user.username,
'url' => UrlHelper.absolute(Discourse.base_path + mention_post.url),
'client_id' => 'xxx0'
},
{
'notification_type' => 1,
'post_number' => 1,
'topic_title' => topic.title,
'topic_id' => topic.id,
'excerpt' => 'Hello @eviltrout ❤',
'username' => user.username,
'url' => UrlHelper.absolute(Discourse.base_path + mention_post.url),
'client_id' => 'xxx1'
}
]
}
post = mention_post
expect(JSON.parse(body)).to eq(payload)
expect(headers["Content-Type"]).to eq('application/json')
TopicUser.change(evil_trout.id, topic.id, notification_level: TopicUser.notification_levels[:watching])
post = Fabricate(:post, topic: post.topic, user_id: evil_trout.id)
user2 = Fabricate(:user)
# if we collapse a reply notification we should get notified on the correct post
new_post = create_post_with_alerts(topic: post.topic, user_id: user.id, reply_to_post_number: post.post_number, raw: 'this is my first reply')
changes = {
"notification_type" => Notification.types[:posted],
"post_number" => new_post.post_number,
"username" => new_post.user.username,
"excerpt" => new_post.raw,
"url" => UrlHelper.absolute(Discourse.base_path + new_post.url)
}
payload["notifications"][0].merge! changes
payload["notifications"][1].merge! changes
expect(JSON.parse(body)).to eq(payload)
new_post = create_post_with_alerts(topic: post.topic, user_id: user2.id, reply_to_post_number: post.post_number, raw: 'this is my second reply')
changes = {
"post_number" => new_post.post_number,
"username" => new_post.user.username,
"excerpt" => new_post.raw,
"url" => UrlHelper.absolute(Discourse.base_path + new_post.url)
}
payload["notifications"][0].merge! changes
payload["notifications"][1].merge! changes
expect(JSON.parse(body)).to eq(payload)
end
end
describe ".create_notification_alert" do
it "does nothing for suspended users" do
evil_trout.update_columns(suspended_till: 1.year.from_now)
events = nil
messages = MessageBus.track_publish do
events = DiscourseEvent.track_events do
PostAlerter.create_notification_alert(
user: evil_trout,
post: post,
notification_type: Notification.types[:custom],
excerpt: "excerpt",
username: "username"
)
end
end
expect(events.size).to eq(0)
expect(messages.size).to eq(0)
expect(Jobs::PushNotification.jobs.size).to eq(0)
end
it "does not publish to MessageBus /notification-alert if the user has not been seen for > 30 days, but still sends a push notification" do
evil_trout.update_columns(last_seen_at: 31.days.ago)
SiteSetting.allowed_user_api_push_urls = "https://site2.com/push"
UserApiKey.create!(user_id: evil_trout.id,
client_id: "xxx#1",
application_name: "iPhone1",
scopes: ['notifications'].map { |name| UserApiKeyScope.new(name: name) },
push_url: "https://site2.com/push")
events = nil
messages = MessageBus.track_publish do
events = DiscourseEvent.track_events do
PostAlerter.create_notification_alert(
user: evil_trout,
post: post,
notification_type: Notification.types[:custom],
excerpt: "excerpt",
username: "username"
)
end
end
expect(events.size).to eq(2)
expect(messages.size).to eq(0)
expect(Jobs::PushNotification.jobs.size).to eq(1)
end
end
describe "watching_first_post" do
fab!(:user) { Fabricate(:user) }
fab!(:category) { Fabricate(:category) }
fab!(:tag) { Fabricate(:tag) }
fab!(:topic) { Fabricate(:topic, category: category, tags: [tag]) }
fab!(:post) { Fabricate(:post, topic: topic) }
it "doesn't notify people who aren't watching" do
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(0)
end
it "notifies the user who is following the first post category" do
level = CategoryUser.notification_levels[:watching_first_post]
CategoryUser.set_notification_level_for_category(user, level, category.id)
PostAlerter.new.after_save_post(post, true)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(1)
end
it "doesn't notify when the record is not new" do
level = CategoryUser.notification_levels[:watching_first_post]
CategoryUser.set_notification_level_for_category(user, level, category.id)
PostAlerter.new.after_save_post(post, false)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(0)
end
it "notifies the user who is following the first post tag" do
level = TagUser.notification_levels[:watching_first_post]
TagUser.change(user.id, tag.id, level)
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(1)
end
it "notifies the user who is following the first post group" do
GroupUser.create(group_id: group.id, user_id: user.id)
GroupUser.create(group_id: group.id, user_id: post.user.id)
topic.topic_allowed_groups.create(group_id: group.id)
level = GroupUser.notification_levels[:watching_first_post]
GroupUser.where(user_id: user.id, group_id: group.id).update_all(notification_level: level)
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(1)
end
it "triggers :before_create_notifications_for_users" do
level = CategoryUser.notification_levels[:watching_first_post]
CategoryUser.set_notification_level_for_category(user, level, category.id)
events = DiscourseEvent.track_events do
PostAlerter.new.after_save_post(post, true)
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user], post])
end
end
context "with replies" do
it "triggers :before_create_notifications_for_users" do
_post = Fabricate(:post, user: user, topic: topic)
reply = Fabricate(:post, topic: topic, reply_to_post_number: 1)
events = DiscourseEvent.track_events do
PostAlerter.post_created(reply)
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user], reply])
end
it "notifies about regular reply" do
_post = Fabricate(:post, user: user, topic: topic)
reply = Fabricate(:post, topic: topic, reply_to_post_number: 1)
PostAlerter.post_created(reply)
expect(user.notifications.where(notification_type: Notification.types[:replied]).count).to eq(1)
end
it "doesn't notify regular user about whispered reply" do
_post = Fabricate(:post, user: user, topic: topic)
whispered_reply = Fabricate(:post, user: admin, topic: topic, post_type: Post.types[:whisper], reply_to_post_number: 1)
PostAlerter.post_created(whispered_reply)
expect(user.notifications.where(notification_type: Notification.types[:replied]).count).to eq(0)
end
it "notifies staff user about whispered reply" do
admin1 = Fabricate(:admin)
admin2 = Fabricate(:admin)
_post = Fabricate(:post, user: user, topic: topic)
whispered_reply1 = Fabricate(:post, user: admin1, topic: topic, post_type: Post.types[:whisper], reply_to_post_number: 1)
whispered_reply2 = Fabricate(:post, user: admin2, topic: topic, post_type: Post.types[:whisper], reply_to_post_number: 2)
PostAlerter.post_created(whispered_reply1)
PostAlerter.post_created(whispered_reply2)
expect(admin1.notifications.where(notification_type: Notification.types[:replied]).count).to eq(1)
TopicUser.change(admin1.id, topic.id, notification_level: TopicUser.notification_levels[:watching])
# this should change nothing cause the moderator post has an action code
# if we have an action code then we should never have notifications, this is rare but
# assign whispers are like this
whispered_reply3 = topic.add_moderator_post(admin2, "i am a reply", post_type: Post.types[:whisper], action_code: 'moderator_thing')
PostAlerter.post_created(whispered_reply3)
# if this whisper is not ignored like it should we would see a posted notification and no replied notifications
notifications = admin1.notifications.where(topic_id: topic.id).to_a
expect(notifications.first.notification_type).to eq(Notification.types[:replied])
expect(notifications.length).to eq(1)
expect(notifications.first.post_number).to eq(whispered_reply2.post_number)
end
it "sends email notifications only to users not on CC list of incoming email" do
alice = Fabricate(:user, username: "alice", email: "alice@example.com")
bob = Fabricate(:user, username: "bob", email: "bob@example.com")
carol = Fabricate(:user, username: "carol", email: "carol@example.com", staged: true)
dave = Fabricate(:user, username: "dave", email: "dave@example.com", staged: true)
erin = Fabricate(:user, username: "erin", email: "erin@example.com")
topic = Fabricate(:private_message_topic, topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: alice),
Fabricate.build(:topic_allowed_user, user: bob),
Fabricate.build(:topic_allowed_user, user: carol),
Fabricate.build(:topic_allowed_user, user: dave),
Fabricate.build(:topic_allowed_user, user: erin)
])
_post = Fabricate(:post, user: alice, topic: topic)
TopicUser.change(alice.id, topic.id, notification_level: TopicUser.notification_levels[:watching])
TopicUser.change(bob.id, topic.id, notification_level: TopicUser.notification_levels[:watching])
TopicUser.change(erin.id, topic.id, notification_level: TopicUser.notification_levels[:watching])
email = Fabricate(:incoming_email,
raw: <<~RAW,
Return-Path: <bob@example.com>
From: Bob <bob@example.com>
To: meta+1234@discoursemail.com, dave@example.com
CC: carol@example.com, erin@example.com
Subject: Hello world
Date: Fri, 15 Jan 2016 00:12:43 +0100
Message-ID: <12345@example.com>
Mime-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable
This post was created by email.
RAW
from_address: "bob@example.com",
to_addresses: "meta+1234@discoursemail.com;dave@example.com",
cc_addresses: "carol@example.com;erin@example.com")
reply = Fabricate(:post_via_email, user: bob, topic: topic, incoming_email: email, reply_to_post_number: 1)
NotificationEmailer.expects(:process_notification).with { |n| n.user_id == alice.id }.once
NotificationEmailer.expects(:process_notification).with { |n| n.user_id == bob.id }.never
NotificationEmailer.expects(:process_notification).with { |n| n.user_id == carol.id }.never
NotificationEmailer.expects(:process_notification).with { |n| n.user_id == dave.id }.never
NotificationEmailer.expects(:process_notification).with { |n| n.user_id == erin.id }.never
PostAlerter.post_created(reply)
expect(alice.notifications.count).to eq(1)
expect(bob.notifications.count).to eq(0)
expect(carol.notifications.count).to eq(1)
expect(dave.notifications.count).to eq(1)
expect(erin.notifications.count).to eq(1)
end
it "does not send email notifications to staged users when notification originates in mailinglist mirror category" do
category = Fabricate(:mailinglist_mirror_category)
topic = Fabricate(:topic, category: category)
user = Fabricate(:staged)
_post = Fabricate(:post, user: user, topic: topic)
reply = Fabricate(:post, topic: topic, reply_to_post_number: 1)
NotificationEmailer.expects(:process_notification).never
expect { PostAlerter.post_created(reply) }.not_to change(user.notifications, :count)
category.mailinglist_mirror = false
NotificationEmailer.expects(:process_notification).once
expect { PostAlerter.post_created(reply) }.to change(user.notifications, :count).by(1)
end
it "creates a notification of type `replied` instead of `posted` for the topic author if they're watching the topic" do
Jobs.run_immediately!
u1 = Fabricate(:admin)
u2 = Fabricate(:admin)
topic = create_topic(user: u1)
u1.notifications.destroy_all
expect do
create_post(topic: topic, user: u2)
end.to change { u1.reload.notifications.count }.by(1)
expect(u1.notifications.exists?(
topic_id: topic.id,
notification_type: Notification.types[:replied],
post_number: 1,
read: false
)).to eq(true)
end
end
context "with category" do
context "with watching" do
it "triggers :before_create_notifications_for_users" do
topic = Fabricate(:topic, category: category)
post = Fabricate(:post, topic: topic)
level = CategoryUser.notification_levels[:watching]
CategoryUser.set_notification_level_for_category(user, level, category.id)
events = DiscourseEvent.track_events do
PostAlerter.post_created(post)
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user], post])
end
it "notifies staff about whispered post" do
topic = Fabricate(:topic, category: category)
level = CategoryUser.notification_levels[:watching]
CategoryUser.set_notification_level_for_category(admin, level, category.id)
CategoryUser.set_notification_level_for_category(user, level, category.id)
whispered_post = Fabricate(:post, user: Fabricate(:admin), topic: topic, post_type: Post.types[:whisper])
expect {
PostAlerter.post_created(whispered_post)
}.to add_notification(admin, :posted)
expect {
PostAlerter.post_created(whispered_post)
}.not_to add_notification(user, :posted)
end
it "notifies a staged user about a private post, but only if the user has access" do
staged_member = Fabricate(:staged)
staged_non_member = Fabricate(:staged)
group_member = Fabricate(:user)
group.add(group_member)
group.add(staged_member)
private_category = Fabricate(
:private_category, group: group,
email_in: 'test@test.com', email_in_allow_strangers: false
)
level = CategoryUser.notification_levels[:watching]
CategoryUser.set_notification_level_for_category(group_member, level, private_category.id)
CategoryUser.set_notification_level_for_category(staged_member, level, private_category.id)
CategoryUser.set_notification_level_for_category(staged_non_member, level, private_category.id)
topic = Fabricate(:topic, category: private_category, user: group_member)
post = Fabricate(:post, topic: topic)
expect {
PostAlerter.post_created(post)
}.to add_notification(staged_member, :posted)
.and not_add_notification(staged_non_member, :posted)
end
it "does not update existing unread notification" do
CategoryUser.set_notification_level_for_category(user, CategoryUser.notification_levels[:watching], category.id)
topic = Fabricate(:topic, category: category)
post = Fabricate(:post, topic: topic)
PostAlerter.post_created(post)
notification = Notification.last
expect(notification.topic_id).to eq(topic.id)
expect(notification.post_number).to eq(1)
post = Fabricate(:post, topic: topic)
PostAlerter.post_created(post)
notification = Notification.last
expect(notification.topic_id).to eq(topic.id)
expect(notification.post_number).to eq(1)
notification_data = JSON.parse(notification.data)
expect(notification_data["display_username"]).to eq(I18n.t("embed.replies", count: 2))
end
end
end
context "with tags" do
context "with watching" do
it "triggers :before_create_notifications_for_users" do
tag = Fabricate(:tag)
topic = Fabricate(:topic, tags: [tag])
post = Fabricate(:post, topic: topic)
level = TagUser.notification_levels[:watching]
TagUser.change(user.id, tag.id, level)
events = DiscourseEvent.track_events do
PostAlerter.post_created(post)
end
expect(events).to include(event_name: :before_create_notifications_for_users, params: [[user], post])
end
it "does not update existing unread notification" do
tag = Fabricate(:tag)
TagUser.change(user.id, tag.id, TagUser.notification_levels[:watching])
topic = Fabricate(:topic, tags: [tag])
post = Fabricate(:post, topic: topic)
PostAlerter.post_created(post)
notification = Notification.last
expect(notification.topic_id).to eq(topic.id)
expect(notification.post_number).to eq(1)
post = Fabricate(:post, topic: topic)
PostAlerter.post_created(post)
notification = Notification.last
expect(notification.topic_id).to eq(topic.id)
expect(notification.post_number).to eq(1)
notification_data = JSON.parse(notification.data)
expect(notification_data["display_username"]).to eq(I18n.t("embed.replies", count: 2))
end
it "does not add notification if user does not belong to tag group with permissions" do
tag = Fabricate(:tag)
topic = Fabricate(:topic, tags: [tag])
post = Fabricate(:post, topic: topic)
tag_group = TagGroup.new(name: 'Only visible to group', tag_names: [tag.name])
tag_group.permissions = [[group.id, TagGroupPermission.permission_types[:full]]]
tag_group.save!
TagUser.change(user.id, tag.id, TagUser.notification_levels[:watching])
expect { PostAlerter.post_created(post) }.not_to change { Notification.count }
end
it "adds notification if user belongs to tag group with permissions" do
tag = Fabricate(:tag)
topic = Fabricate(:topic, tags: [tag])
post = Fabricate(:post, topic: topic)
tag_group = Fabricate(:tag_group, tags: [tag])
Fabricate(:group_user, group: group, user: user)
Fabricate(:tag_group_permission, tag_group: tag_group, group: group)
TagUser.change(user.id, tag.id, TagUser.notification_levels[:watching])
expect { PostAlerter.post_created(post) }.to change { Notification.count }.by(1)
end
end
context "with on change" do
fab!(:user) { Fabricate(:user) }
fab!(:other_tag) { Fabricate(:tag) }
fab!(:watched_tag) { Fabricate(:tag) }
before do
SiteSetting.tagging_enabled = true
Jobs.run_immediately!
TagUser.change(user.id, watched_tag.id, TagUser.notification_levels[:watching_first_post])
TopicUser.change(Fabricate(:user).id, post.topic.id, notification_level: TopicUser.notification_levels[:watching])
end
it "triggers a notification" do
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(0)
expect { PostRevisor.new(post).revise!(Fabricate(:user), tags: [other_tag.name, watched_tag.name]) }.to change { Notification.where(user_id: user.id).count }.by(1)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(1)
expect { PostRevisor.new(post).revise!(Fabricate(:user), tags: [watched_tag.name, other_tag.name]) }.not_to change { Notification.count }
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(1)
end
it "doesn't trigger a notification if topic is unlisted" do
post.topic.update_column(:visible, false)
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(0)
PostRevisor.new(post).revise!(Fabricate(:user), tags: [other_tag.name, watched_tag.name])
expect(user.notifications.where(notification_type: Notification.types[:watching_first_post]).count).to eq(0)
end
end
context "with private message" do
fab!(:post) { Fabricate(:private_message_post) }
fab!(:other_tag) { Fabricate(:tag) }
fab!(:other_tag2) { Fabricate(:tag) }
fab!(:other_tag3) { Fabricate(:tag) }
fab!(:user) { Fabricate(:user) }
fab!(:staged) { Fabricate(:staged) }
before do
SiteSetting.tagging_enabled = true
SiteSetting.pm_tags_allowed_for_groups = "1|2|3"
Jobs.run_immediately!
TopicUser.change(user.id, post.topic.id, notification_level: TopicUser.notification_levels[:watching])
TopicUser.change(staged.id, post.topic.id, notification_level: TopicUser.notification_levels[:watching])
TopicUser.change(admin.id, post.topic.id, notification_level: TopicUser.notification_levels[:watching])
TagUser.change(staged.id, other_tag.id, TagUser.notification_levels[:watching])
TagUser.change(admin.id, other_tag3.id, TagUser.notification_levels[:watching])
post.topic.allowed_users << user
post.topic.allowed_users << staged
end
it "only notifies staff watching added tag" do
expect(PostRevisor.new(post).revise!(Fabricate(:admin), tags: [other_tag.name])).to be true
expect(Notification.where(user_id: staged.id).count).to eq(0)
expect(PostRevisor.new(post).revise!(Fabricate(:admin), tags: [other_tag2.name])).to be true
expect(Notification.where(user_id: admin.id).count).to eq(0)
expect(PostRevisor.new(post).revise!(Fabricate(:admin), tags: [other_tag3.name])).to be true
expect(Notification.where(user_id: admin.id).count).to eq(1)
end
end
context "with tag groups" do
fab!(:tag) { Fabricate(:tag) }
fab!(:user) { Fabricate(:user) }
fab!(:topic) { Fabricate(:topic, tags: [tag]) }
fab!(:post) { Fabricate(:post, topic: topic) }
shared_examples "tag user with notification level" do |notification_level, notification_type|
it "notifies a user who is watching a tag that does not belong to a tag group" do
TagUser.change(user.id, tag.id, TagUser.notification_levels[notification_level])
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[notification_type]).count).to eq(1)
end
it "does not notify a user watching a tag with tag group permissions that he does not belong to" do
tag_group = Fabricate(:tag_group, tags: [tag], permissions: { group.name => 1 })
TagUser.change(user.id, tag.id, TagUser.notification_levels[notification_level])
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[notification_type]).count).to eq(0)
end
it "notifies a user watching a tag with tag group permissions that he belongs to" do
Fabricate(:group_user, group: group, user: user)
TagUser.change(user.id, tag.id, TagUser.notification_levels[notification_level])
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[notification_type]).count).to eq(1)
end
it "notifies a staff watching a tag with tag group permissions that he does not belong to" do
tag_group = Fabricate(:tag_group, tags: [tag])
Fabricate(:tag_group_permission, tag_group: tag_group, group: group)
staff_group = Group.find(Group::AUTO_GROUPS[:staff])
Fabricate(:group_user, group: staff_group, user: user)
TagUser.change(user.id, tag.id, TagUser.notification_levels[notification_level])
PostAlerter.post_created(post)
expect(user.notifications.where(notification_type: Notification.types[notification_type]).count).to eq(1)
end
end
context "with :watching notification level" do
include_examples "tag user with notification level", :watching, :posted
end
context "with :watching_first_post notification level" do
include_examples "tag user with notification level", :watching_first_post, :watching_first_post
end
end
end
describe '#extract_linked_users' do
fab!(:post) { Fabricate(:post, topic: topic) }
fab!(:post2) { Fabricate(:post) }
describe 'when linked post has been deleted' do
let(:topic_link) do
TopicLink.create!(
url: "/t/#{topic.id}",
topic_id: topic.id,
link_topic_id: post2.topic.id,
link_post_id: nil,
post_id: post.id,
user: user,
domain: 'test.com'
)
end
it 'should use the first post of the topic' do
topic_link
expect(PostAlerter.new.extract_linked_users(post.reload)).to eq([post2.user])
end
end
end
describe '#notify_post_users' do
fab!(:post) { Fabricate(:post, topic: topic) }
fab!(:last_editor) { Fabricate(:user) }
fab!(:tag) { Fabricate(:tag) }
fab!(:category) { Fabricate(:category) }
it 'creates single edit notification when post is modified' do
TopicUser.create!(
user_id: user.id,
topic_id: topic.id,
notification_level: TopicUser.notification_levels[:watching],
last_read_post_number: post.post_number
)
PostRevisor.new(post).revise!(last_editor, tags: [tag.name])
PostAlerter.new.notify_post_users(post, [])
expect(Notification.count).to eq(1)
expect(Notification.last.notification_type).to eq(Notification.types[:edited])
expect(JSON.parse(Notification.last.data)["display_username"]).to eq(last_editor.username)
PostAlerter.new.notify_post_users(post, [])
expect(Notification.count).to eq(1)
end
it 'creates posted notification when Sidekiq is slow' do
CategoryUser.set_notification_level_for_category(user, CategoryUser.notification_levels[:watching], category.id)
post = PostCreator.create!(
Fabricate(:user),
title: "one of my first topics",
raw: "one of my first posts",
category: category.id
)
TopicUser.change(user, post.topic_id, last_read_post_number: post.post_number)
# Manually run job after the user read the topic to simulate a slow
# Sidekiq.
job_args = Jobs::PostAlert.jobs[0]['args'][0]
expect { Jobs::PostAlert.new.execute(job_args.with_indifferent_access) }
.to change { Notification.count }.by(1)
expect(Notification.last.notification_type).to eq(Notification.types[:posted])
end
end
context "with SMTP (group_smtp_email)" do
before do
SiteSetting.enable_smtp = true
SiteSetting.email_in = true
Jobs.run_immediately!
end
fab!(:group) do
Fabricate(
:group,
smtp_server: "smtp.gmail.com",
smtp_port: 587,
smtp_ssl: true,
imap_server: "imap.gmail.com",
imap_port: 993,
imap_ssl: true,
email_username: "discourse@example.com",
email_password: "password",
smtp_enabled: true,
imap_enabled: true
)
end
def create_post_with_incoming
raw_mail = <<~EMAIL
From: Foo <foo@discourse.org>
To: discourse@example.com
Cc: bar@discourse.org, jim@othersite.com
Subject: Full email group username flow
Date: Fri, 15 Jan 2021 00:12:43 +0100
Message-ID: <u4w8c9r4y984yh98r3h69873@example.com.mail>
Mime-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
This is the first email.
EMAIL
Email::Receiver.new(raw_mail, {}).process!
end
it "does not error if SMTP is enabled and the topic has no incoming email or allowed groups" do
expect { PostAlerter.new.after_save_post(post, true) }.not_to raise_error
end
it "does not error if SMTP is enabled and the topic has no incoming email but does have an allowed group" do
TopicAllowedGroup.create(topic: private_message_topic, group: group)
expect { PostAlerter.new.after_save_post(post, true) }.not_to raise_error
end
it "does not error if SMTP is enabled and the topic has no incoming email but has multiple allowed groups" do
TopicAllowedGroup.create(topic: private_message_topic, group: group)
TopicAllowedGroup.create(topic: private_message_topic, group: Fabricate(:group))
expect { PostAlerter.new.after_save_post(post, true) }.not_to raise_error
end
it "sends a group smtp email because SMTP is enabled for the site and the group" do
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic)
expect { PostAlerter.new.after_save_post(post, true) }.to change { ActionMailer::Base.deliveries.size }.by(1)
email = ActionMailer::Base.deliveries.last
expect(email.from).to include(group.email_username)
expect(email.to).to contain_exactly(topic.reload.topic_allowed_users.order(:created_at).first.user.email)
expect(email.cc).to match_array(["bar@discourse.org", "jim@othersite.com"])
expect(email.subject).to eq("Re: #{topic.title}")
end
it "sends a group smtp email when the original group has had SMTP disabled and there is an additional topic allowed group" do
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
other_allowed_group = Fabricate(:smtp_group)
TopicAllowedGroup.create(group: other_allowed_group, topic: topic)
post = Fabricate(:post, topic: topic)
group.update!(smtp_enabled: false)
expect { PostAlerter.new.after_save_post(post, true) }.to change { ActionMailer::Base.deliveries.size }.by(1)
email = ActionMailer::Base.deliveries.last
expect(email.from).to include(other_allowed_group.email_username)
expect(email.to).to contain_exactly(topic.reload.topic_allowed_users.order(:created_at).first.user.email)
expect(email.cc).to match_array(["bar@discourse.org", "jim@othersite.com"])
expect(email.subject).to eq("Re: #{topic.title}")
end
it "does not send a group smtp email if smtp is not enabled for the group" do
group.update!(smtp_enabled: false)
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic)
expect { PostAlerter.new.after_save_post(post, true) }.not_to change { ActionMailer::Base.deliveries.size }
end
it "does not send a group smtp email if SiteSetting.enable_smtp is false" do
SiteSetting.enable_smtp = false
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic)
expect { PostAlerter.new.after_save_post(post, true) }.not_to change { ActionMailer::Base.deliveries.size }
end
it "does not send group smtp emails for a whisper" do
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic, post_type: Post.types[:whisper])
expect { PostAlerter.new.after_save_post(post, true) }.not_to change { ActionMailer::Base.deliveries.size }
end
it "sends the group smtp email job with a delay of personal_email_time_window_seconds" do
freeze_time
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic)
PostAlerter.new.after_save_post(post, true)
job_enqueued?(
job: :group_smtp_email,
args: {
group_id: group.id,
post_id: post.id,
email: topic.reload.topic_allowed_users.order(:created_at).first.user.email,
cc_emails: ["bar@discourse.org", "jim@othersite.com"]
},
at: Time.zone.now + SiteSetting.personal_email_time_window_seconds.seconds
)
end
it "does not send a group smtp email for anyone if the reply post originates from an incoming email that is auto generated" do
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic)
Fabricate(:incoming_email, post: post, topic: topic, is_auto_generated: true)
expect_not_enqueued_with(job: :group_smtp_email) do
expect { PostAlerter.new.after_save_post(post, true) }.not_to change { ActionMailer::Base.deliveries.size }
end
end
it "skips sending a notification email to the group and all other email addresses that are _not_ members of the group,
sends a group_smtp_email instead" do
NotificationEmailer.enable
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
group_user1 = Fabricate(:group_user, group: group)
group_user2 = Fabricate(:group_user, group: group)
TopicUser.create(user: group_user1.user, notification_level: TopicUser.notification_levels[:watching], topic: topic)
post = Fabricate(:post, topic: topic.reload)
# Sends an email for:
#
# 1. the group user that is watching the post (but does not send this email with group SMTO)
# 2. the group smtp email to notify all topic_users not in the group
expect { PostAlerter.new.after_save_post(post, true) }.to change {
ActionMailer::Base.deliveries.size
}.by(2).and change { Notification.count }.by(2)
# The group smtp email
email = ActionMailer::Base.deliveries.first
expect(email.from).to eq([group.email_username])
expect(email.to).to contain_exactly("foo@discourse.org")
expect(email.cc).to match_array(["bar@discourse.org", "jim@othersite.com"])
expect(email.subject).to eq("Re: #{topic.title}")
# The watching group user notification email
email = ActionMailer::Base.deliveries.last
expect(email.from).to eq([SiteSetting.notification_email])
expect(email.to).to contain_exactly(group_user1.user.email)
expect(email.cc).to eq(nil)
expect(email.subject).to eq("[Discourse] [PM] #{topic.title}")
end
it "skips sending a notification email to the cc address that was added on the same post with an incoming email" do
NotificationEmailer.enable
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic.reload)
expect { PostAlerter.new.after_save_post(post, true) }.to change {
ActionMailer::Base.deliveries.size
}.by(1).and change { Notification.count }.by(1)
email = ActionMailer::Base.deliveries.last
# the reply post from someone who was emailed
reply_raw_mail = <<~EMAIL
From: Bar <bar@discourse.org>
To: discourse@example.com
Cc: someothernewcc@baz.com, finalnewcc@doom.com
Subject: #{email.subject}
Date: Fri, 16 Jan 2021 00:12:43 +0100
Message-ID: <sdugj3o4iyu4832x3487@discourse.org.mail>
In-Reply-To: #{email.message_id}
Mime-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
Hey here is my reply!
EMAIL
reply_post_from_email = nil
expect {
reply_post_from_email = Email::Receiver.new(reply_raw_mail, {}).process!
}.to change {
User.count # the two new cc addresses have users created
}.by(2).and change {
TopicAllowedUser.where(topic: topic).count # and they are added as topic allowed users
}.by(2).and change {
# but they are not sent emails because they were cc'd on an email, only jim@othersite.com
# is emailed because he is a topic allowed user cc'd on the _original_ email and he is not
# the one creating the post, and foo@discourse.org, who is the OP of the topic
ActionMailer::Base.deliveries.size
}.by(1).and change {
Notification.count # and they are still sent their normal discourse notification
}.by(2)
email = ActionMailer::Base.deliveries.last
expect(email.to).to eq(["foo@discourse.org"])
expect(email.cc).to eq(["jim@othersite.com"])
expect(email.from).to eq([group.email_username])
expect(email.subject).to eq("Re: #{topic.title}")
end
it "handles the OP of the topic replying by email and sends a group email to the other topic allowed users successfully" do
NotificationEmailer.enable
incoming_email_post = create_post_with_incoming
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic.reload)
expect { PostAlerter.new.after_save_post(post, true) }.to change {
ActionMailer::Base.deliveries.size
}.by(1).and change { Notification.count }.by(1)
email = ActionMailer::Base.deliveries.last
# the reply post from someone who was emailed
reply_raw_mail = <<~EMAIL
From: Foo <foo@discourse.org>
To: discourse@example.com
Cc: someothernewcc@baz.com, finalnewcc@doom.com
Subject: #{email.subject}
Date: Fri, 16 Jan 2021 00:12:43 +0100
Message-ID: <sgk094238uc0348c334483@discourse.org.mail>
In-Reply-To: #{email.message_id}
Mime-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
I am ~~Commander Shepherd~~ the OP and I approve of this message.
EMAIL
reply_post_from_email = nil
expect {
reply_post_from_email = Email::Receiver.new(reply_raw_mail, {}).process!
}.to change {
User.count # the two new cc addresses have users created
}.by(2).and change {
TopicAllowedUser.where(topic: topic).count # and they are added as topic allowed users
}.by(2).and change {
# but they are not sent emails because they were cc'd on an email, only jim@othersite.com
# is emailed because he is a topic allowed user cc'd on the _original_ email and he is not
# the one creating the post
ActionMailer::Base.deliveries.size
}.by(1).and change {
Notification.count # and they are still sent their normal discourse notification
}.by(2)
email = ActionMailer::Base.deliveries.last
expect(email.to).to eq(["bar@discourse.org"])
expect(email.cc).to eq(["jim@othersite.com"])
expect(email.from).to eq([group.email_username])
expect(email.subject).to eq("Re: #{topic.title}")
end
it "handles the OP of the topic replying by email and cc'ing new people, and does not send a group SMTP email to those newly cc'd users" do
NotificationEmailer.enable
# this is a special case where we are not CC'ing on the original email,
# only on the follow up email
raw_mail = <<~EMAIL
From: Foo <foo@discourse.org>
To: discourse@example.com
Subject: Full email group username flow
Date: Fri, 14 Jan 2021 00:12:43 +0100
Message-ID: <f4832ujfc3498u398i3@example.com.mail>
Mime-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
This is the first email.
EMAIL
incoming_email_post = Email::Receiver.new(raw_mail, {}).process!
topic = incoming_email_post.topic
post = Fabricate(:post, topic: topic.reload)
expect { PostAlerter.new.after_save_post(post, true) }.to change {
ActionMailer::Base.deliveries.size
}.by(1).and change { Notification.count }.by(1)
email = ActionMailer::Base.deliveries.last
# the reply post from the OP, cc'ing new people in
reply_raw_mail = <<~EMAIL
From: Foo <foo@discourse.org>
To: discourse@example.com
Cc: someothernewcc@baz.com, finalnewcc@doom.com
Subject: #{email.subject}
Date: Fri, 16 Jan 2021 00:12:43 +0100
Message-ID: <3849cu9843yncr9834yr9348x934@discourse.org.mail>
In-Reply-To: #{email.message_id}
Mime-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: 7bit
I am inviting my mates to this email party.
EMAIL
reply_post_from_email = nil
expect {
reply_post_from_email = Email::Receiver.new(reply_raw_mail, {}).process!
}.to change {
User.count # the two new cc addresses have users created
}.by(2).and change {
TopicAllowedUser.where(topic: topic).count # and they are added as topic allowed users
}.by(2).and not_change {
# but they are not sent emails because they were cc'd on an email.
# no group smtp message is sent because the OP is not sent an email,
# they made this post.
ActionMailer::Base.deliveries.size
}.and change {
Notification.count # and they are still sent their normal discourse notification
}.by(2)
last_email = ActionMailer::Base.deliveries.last
expect(email).to eq(last_email)
end
end
describe 'storing custom data' do
let(:custom_data) { 'custom_string' }
it 'stores custom data inside a notification' do
PostAlerter.new.create_notification(
admin,
Notification.types[:liked],
post,
custom_data: { custom_key: custom_data }
)
liked_notification = Notification.where(notification_type: Notification.types[:liked]).last
expect(liked_notification.data_hash[:custom_key]).to eq(custom_data)
end
end
it "does not create notifications for PMs if not invited" do
SiteSetting.pm_tags_allowed_for_groups = "#{Group::AUTO_GROUPS[:everyone]}"
watching_first_post_tag = Fabricate(:tag)
TagUser.change(admin.id, watching_first_post_tag.id, TagUser.notification_levels[:watching_first_post])
watching_tag = Fabricate(:tag)
TagUser.change(admin.id, watching_tag.id, TagUser.notification_levels[:watching])
post = create_post(tags: [watching_first_post_tag.name, watching_tag.name], archetype: Archetype.private_message, target_usernames: "#{evil_trout.username}")
expect { PostAlerter.new.after_save_post(post, true) }.to change { Notification.count }.by(1)
notification = Notification.last
expect(notification.user).to eq(evil_trout)
expect(notification.notification_type).to eq(Notification.types[:private_message])
expect(notification.topic).to eq(post.topic)
expect(notification.post_number).to eq(1)
end
it "does not create multiple notifications for same post" do
category = Fabricate(:category)
CategoryUser.set_notification_level_for_category(
user,
NotificationLevels.all[:tracking],
category.id,
)
watching_first_post_tag = Fabricate(:tag)
TagUser.change(user.id, watching_first_post_tag.id, TagUser.notification_levels[:watching_first_post])
watching_tag = Fabricate(:tag)
TagUser.change(user.id, watching_tag.id, TagUser.notification_levels[:watching])
post = create_post(category: category, tags: [watching_first_post_tag.name, watching_tag.name])
expect { PostAlerter.new.after_save_post(post, true) }.to change { Notification.count }.by(1)
notification = Notification.last
expect(notification.user).to eq(user)
expect(notification.notification_type).to eq(Notification.types[:posted])
expect(notification.topic).to eq(post.topic)
expect(notification.post_number).to eq(1)
end
it 'triggers all discourse events' do
expected_events = [
:post_alerter_before_mentions,
:post_alerter_before_replies,
:post_alerter_before_quotes,
:post_alerter_before_linked,
:post_alerter_before_post,
:post_alerter_before_first_post,
:post_alerter_after_save_post,
]
events = DiscourseEvent.track_events do
PostAlerter.new.after_save_post(post, true)
end
# Expect all the notification events are called
# There are some other events triggered from outside after_save_post
expect(events.map { |e| e[:event_name] }).to include(*expected_events)
# Expect each notification event is called with the right parameters
events.each do |event|
if expected_events.include?(event[:event_name])
expect(event[:params]).to eq([post, true, [post.user]])
end
end
end
end