discourse/spec/mailers/user_notifications_spec.rb
Martin Brennan c4ea158656
FIX: Improve tags in email subjects and add filter headers (#19760)
This commit does a couple of things:

1. Changes the limit of tags to include a subject for a
   notification email to the `max_tags_per_topic` setting
   instead of the arbitrary 3 limit
2. Adds both an X-Discourse-Tags and X-Discourse-Category
   custom header to outbound emails containing the tags
   and category from the subject, so people on mail clients
   that allow advanced filtering (i.e. not Gmail) can filter
   mail by tags and category, which is useful for mailing
   list mode users

c.f. https://meta.discourse.org/t/headers-for-email-notifications-so-that-gmail-users-can-filter-on-tags/249982/17
2023-01-06 10:03:02 +10:00

1264 lines
46 KiB
Ruby

# frozen_string_literal: true
RSpec.describe UserNotifications do
let(:user) { Fabricate(:admin) }
describe "#get_context_posts" do
it "does not include hidden/deleted/user_deleted posts in context" do
post1 = create_post
_post2 = Fabricate(:post, topic: post1.topic, deleted_at: 1.day.ago)
_post3 = Fabricate(:post, topic: post1.topic, user_deleted: true)
_post4 = Fabricate(:post, topic: post1.topic, hidden: true)
_post5 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:moderator_action])
_post6 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:small_action])
_post7 = Fabricate(:post, topic: post1.topic, post_type: Post.types[:whisper])
last = Fabricate(:post, topic: post1.topic)
post1.user.user_option.email_previous_replies = UserOption.previous_replies_type[:always]
# default is only post #1
expect(UserNotifications.get_context_posts(last, nil, post1.user).count).to eq(1)
# staff members can also see the whisper
moderator = build(:moderator)
moderator.user_option = UserOption.new
moderator.user_option.email_previous_replies = UserOption.previous_replies_type[:always]
tu = TopicUser.new(topic: post1.topic, user: moderator)
expect(UserNotifications.get_context_posts(last, tu, tu.user).count).to eq(2)
end
it "allows users to control context" do
post1 = create_post
_post2 = Fabricate(:post, topic: post1.topic)
post3 = Fabricate(:post, topic: post1.topic)
user = Fabricate(:user)
TopicUser.change(user.id, post1.topic_id, last_emailed_post_number: 1)
topic_user = TopicUser.find_by(user_id: user.id, topic_id: post1.topic_id)
# to avoid reloads after update_columns
user = topic_user.user
user.user_option.update_columns(email_previous_replies: UserOption.previous_replies_type[:unless_emailed])
expect(UserNotifications.get_context_posts(post3, topic_user, user).count).to eq(1)
user.user_option.update_columns(email_previous_replies: UserOption.previous_replies_type[:never])
expect(UserNotifications.get_context_posts(post3, topic_user, user).count).to eq(0)
user.user_option.update_columns(email_previous_replies: UserOption.previous_replies_type[:always])
expect(UserNotifications.get_context_posts(post3, topic_user, user).count).to eq(2)
SiteSetting.private_email = true
expect(UserNotifications.get_context_posts(post3, topic_user, user).count).to eq(0)
end
end
describe ".signup" do
subject { UserNotifications.signup(user) }
it "works" do
expect(subject.to).to eq([user.email])
expect(subject.subject).to be_present
expect(subject.from).to eq([SiteSetting.notification_email])
expect(subject.body).to be_present
end
end
describe ".forgot_password" do
subject { UserNotifications.forgot_password(user) }
it "works" do
expect(subject.to).to eq([user.email])
expect(subject.subject).to be_present
expect(subject.from).to eq([SiteSetting.notification_email])
expect(subject.body).to be_present
end
end
describe '.post_approved' do
fab!(:post) { Fabricate(:post) }
it 'works' do
subject = UserNotifications.post_approved(user, { notification_data_hash: { post_url: post.url } })
expect(subject.to).to eq([user.email])
expect(subject.subject).to be_present
expect(subject.from).to eq([SiteSetting.notification_email])
expect(subject.body).to be_present
end
end
describe ".confirm_new_email" do
let(:opts) do
{ requested_by_admin: requested_by_admin, email_token: token }
end
let(:token) { "test123" }
context "when requested by admin" do
let(:requested_by_admin) { true }
it "uses the requested by admin template" do
expect(UserNotifications.confirm_new_email(user, opts).body).to include(
"This email change was requested by a site admin."
)
end
end
context "when not requested by admin" do
let(:requested_by_admin) { false }
it "uses the normal template" do
expect(UserNotifications.confirm_new_email(user, opts).body).not_to include(
"This email change was requested by a site admin."
)
end
end
end
describe '.email_login' do
let(:email_token) { Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login]).token }
subject { UserNotifications.email_login(user, email_token: email_token) }
it "generates the right email" do
expect(subject.to).to eq([user.email])
expect(subject.from).to eq([SiteSetting.notification_email])
expect(subject.subject).to eq(I18n.t(
'user_notifications.email_login.subject_template',
email_prefix: SiteSetting.title
))
expect(subject.body.to_s).to match(I18n.t(
'user_notifications.email_login.text_body_template',
site_name: SiteSetting.title,
base_url: Discourse.base_url,
email_token: email_token
))
end
end
describe '.digest' do
subject { UserNotifications.digest(user) }
after do
Discourse.redis.keys('summary-new-users:*').each { |key| Discourse.redis.del(key) }
end
context "without new topics" do
it "doesn't send the email" do
expect(subject.to).to be_blank
end
end
context "with topics only from new users" do
let!(:new_today) { Fabricate(:topic, user: Fabricate(:user, trust_level: TrustLevel[0], created_at: 10.minutes.ago), title: "Hey everyone look at me") }
let!(:new_yesterday) { Fabricate(:topic, user: Fabricate(:user, trust_level: TrustLevel[0], created_at: 25.hours.ago), created_at: 25.hours.ago, title: "This topic is of interest to you") }
it "returns topics from new users if they're more than 24 hours old" do
expect(subject.to).to eq([user.email])
html = subject.html_part.body.to_s
expect(html).to include(new_yesterday.title)
expect(html).to_not include(new_today.title)
end
end
context "with new topics" do
let!(:popular_topic) { Fabricate(:topic, user: Fabricate(:coding_horror), created_at: 1.hour.ago) }
it "works" do
expect(subject.to).to eq([user.email])
expect(subject.subject).to be_present
expect(subject.from).to eq([SiteSetting.notification_email])
expect(subject.html_part.body.to_s).to be_present
expect(subject.text_part.body.to_s).to be_present
expect(subject.header["List-Unsubscribe"].to_s).to match(/\/email\/unsubscribe\/\h{64}/)
expect(subject.html_part.body.to_s).to include('New Users')
end
it "doesn't include new user count if digest_after_minutes is low" do
user.user_option.digest_after_minutes = 60
expect(subject.html_part.body.to_s).to_not include('New Users')
end
it "works with min_date string" do
digest = UserNotifications.digest(user, since: 1.month.ago.to_date.to_s)
expect(digest.html_part.body.to_s).to be_present
expect(digest.text_part.body.to_s).to be_present
expect(digest.html_part.body.to_s).to include('New Users')
end
it "includes email_prefix in email subject instead of site title" do
SiteSetting.email_prefix = "Try Discourse"
SiteSetting.title = "Discourse Meta"
expect(subject.subject).to match(/Try Discourse/)
expect(subject.subject).not_to match(/Discourse Meta/)
end
it "excludes deleted topics and their posts" do
deleted = Fabricate(:topic, user: Fabricate(:user), title: "Delete this topic plz", created_at: 1.hour.ago)
post = Fabricate(:post, topic: deleted, score: 100.0, post_number: 2, raw: "Your wish is my command", created_at: 1.hour.ago)
deleted.trash!
html = subject.html_part.body.to_s
expect(html).to_not include deleted.title
expect(html).to_not include post.raw
end
it "excludes shared drafts" do
cat = Fabricate(:category)
SiteSetting.shared_drafts_category = cat.id
topic = Fabricate(:topic, title: "This is a draft", category_id: cat.id, created_at: 1.hour.ago)
post = Fabricate(
:post,
topic: topic,
score: 100.0,
post_number: 2,
raw: "secret draft content",
created_at: 1.hour.ago
)
html = subject.html_part.body.to_s
expect(html).to_not include topic.title
expect(html).to_not include post.raw
end
it "excludes whispers and other post types that don't belong" do
t = Fabricate(:topic, user: Fabricate(:user), title: "Who likes the same stuff I like?", created_at: 1.hour.ago)
whisper = Fabricate(:post, topic: t, score: 100.0, post_number: 2, raw: "You like weird stuff", post_type: Post.types[:whisper], created_at: 1.hour.ago)
mod_action = Fabricate(:post, topic: t, score: 100.0, post_number: 3, raw: "This topic unlisted", post_type: Post.types[:moderator_action], created_at: 1.hour.ago)
small_action = Fabricate(:post, topic: t, score: 100.0, post_number: 4, raw: "A small action", post_type: Post.types[:small_action], created_at: 1.hour.ago)
html = subject.html_part.body.to_s
expect(html).to_not include whisper.raw
expect(html).to_not include mod_action.raw
expect(html).to_not include small_action.raw
end
it "excludes deleted and hidden posts" do
t = Fabricate(:topic, user: Fabricate(:user), title: "Post objectionable stuff here", created_at: 1.hour.ago)
deleted = Fabricate(:post, topic: t, score: 100.0, post_number: 2, raw: "This post is uncalled for", deleted_at: 5.minutes.ago, created_at: 1.hour.ago)
hidden = Fabricate(:post, topic: t, score: 100.0, post_number: 3, raw: "Try to find this post", hidden: true, hidden_at: 5.minutes.ago, hidden_reason_id: Post.hidden_reasons[:flagged_by_tl3_user], created_at: 1.hour.ago)
user_deleted = Fabricate(:post, topic: t, score: 100.0, post_number: 4, raw: "I regret this post", user_deleted: true, created_at: 1.hour.ago)
html = subject.html_part.body.to_s
expect(html).to_not include deleted.raw
expect(html).to_not include hidden.raw
expect(html).to_not include user_deleted.raw
end
it "excludes posts that are newer than editing grace period" do
SiteSetting.editing_grace_period = 5.minutes
too_new = Fabricate(:topic, user: Fabricate(:user), title: "Oops I need to edit this", created_at: 1.minute.ago)
_too_new_post = Fabricate(:post, user: too_new.user, topic: too_new, score: 100.0, post_number: 1, created_at: 1.minute.ago)
html = subject.html_part.body.to_s
expect(html).to_not include too_new.title
end
it "uses theme color" do
cs = Fabricate(:color_scheme, name: 'Fancy', color_scheme_colors: [
Fabricate(:color_scheme_color, name: 'header_primary', hex: 'F0F0F0'),
Fabricate(:color_scheme_color, name: 'header_background', hex: '1E1E1E')
])
theme = Fabricate(:theme,
user_selectable: true,
user: Fabricate(:admin),
color_scheme_id: cs.id
)
theme.set_default!
html = subject.html_part.body.to_s
expect(html).to include 'F0F0F0'
expect(html).to include '1E1E1E'
end
it "supports subfolder" do
set_subfolder "/forum"
html = subject.html_part.body.to_s
text = subject.text_part.body.to_s
expect(html).to be_present
expect(text).to be_present
expect(html).to_not include("/forum/forum")
expect(text).to_not include("/forum/forum")
expect(subject.header["List-Unsubscribe"].to_s).to match(/http:\/\/test.localhost\/forum\/email\/unsubscribe\/\h{64}/)
topic_url = "http://test.localhost/forum/t/#{popular_topic.slug}/#{popular_topic.id}"
expect(html).to include(topic_url)
expect(text).to include(topic_url)
end
it "applies lang/xml:lang html attributes" do
SiteSetting.default_locale = "pl_PL"
html = subject.html_part.to_s
expect(html).to match(' lang="pl-PL"')
expect(html).to match(' xml:lang="pl-PL"')
end
end
end
describe '.user_replied' do
let(:response_by_user) { Fabricate(:user, name: "John Doe") }
let(:category) { Fabricate(:category, name: 'India') }
let(:tag1) { Fabricate(:tag, name: 'Taggo') }
let(:tag2) { Fabricate(:tag, name: 'Taggie') }
let(:hidden_tag) { Fabricate(:tag, name: "hidden") }
let!(:hidden_tag_group) do
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name])
end
let(:topic) { Fabricate(:topic, category: category, tags: [tag1, tag2, hidden_tag], title: "Super cool topic") }
let(:post) { Fabricate(:post, topic: topic, raw: 'This is My super duper cool topic') }
let(:response) { Fabricate(:basic_reply, topic: post.topic, user: response_by_user) }
let(:user) { Fabricate(:user) }
let(:notification) { Fabricate(:replied_notification, user: user, post: response) }
it 'generates a correct email' do
SiteSetting.default_email_in_reply_to = true
# Fabricator is not fabricating this ...
SiteSetting.email_subject = "[%{site_name}] %{optional_pm}%{optional_cat}%{optional_tags}%{topic_title}"
SiteSetting.enable_names = true
SiteSetting.display_name_on_posts = true
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
# from should include full user name
expect(mail[:from].display_names).to eql(['John Doe via Discourse'])
# subject should include category name
expect(mail.subject).to match(/India/)
# subject should include tag names
expect(mail.subject).to match(/Taggo/)
expect(mail.subject).to match(/Taggie/)
mail_html = mail.html_part.body.to_s
expect(mail_html.scan(/My super duper cool topic/).count).to eq(1)
expect(mail_html.scan(/In Reply To/).count).to eq(1)
# 2 "visit topic" link
expect(mail_html.scan(/Visit Topic/).count).to eq(2)
# 2 respond to links cause we have 1 context post
expect(mail_html.scan(/to respond/).count).to eq(2)
# 1 unsubscribe
expect(mail_html.scan(/To unsubscribe/).count).to eq(1)
# side effect, topic user is updated with post number
tu = TopicUser.get(post.topic_id, user)
expect(tu.last_emailed_post_number).to eq(response.post_number)
# no In Reply To if user opts out
user.user_option.email_in_reply_to = false
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.html_part.body.to_s.scan(/In Reply To/).count).to eq(0)
SiteSetting.enable_names = true
SiteSetting.display_name_on_posts = true
SiteSetting.prioritize_username_in_ux = false
response.user.username = "bobmarley"
response.user.name = "Bob Marley"
response.user.save
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
mail_html = mail.html_part.body.to_s
expect(mail_html.scan(/>Bob Marley/).count).to eq(1)
expect(mail_html.scan(/>bobmarley/).count).to eq(0)
expect(mail.subject.scan(/#{tag1.name}/).count).to eq(1)
expect(mail.subject.scan(/#{hidden_tag.name}/).count).to eq(0)
SiteSetting.prioritize_username_in_ux = true
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
mail_html = mail.html_part.body.to_s
expect(mail_html.scan(/>Bob Marley/).count).to eq(0)
expect(mail_html.scan(/>bobmarley/).count).to eq(1)
end
it "the number of tags shown in subject should match max_tags_per_topic" do
SiteSetting.email_subject = "[%{site_name}] %{optional_pm}%{optional_cat}%{optional_tags}%{topic_title}"
SiteSetting.max_tags_per_topic = 1
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to match(/Taggo/)
expect(mail.subject).not_to match(/Taggie/)
end
it "doesn't include details when private_email is enabled" do
SiteSetting.private_email = true
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.html_part.body.to_s).to_not include(response.raw)
expect(mail.html_part.body.to_s).to_not include(topic.url)
expect(mail.text_part.to_s).to_not include(response.raw)
expect(mail.text_part.to_s).to_not include(topic.url)
end
it "includes excerpt when post_excerpts_in_emails is enabled" do
paragraphs = [
"This is the first paragraph, but you should read more.",
"And here is its friend, the second paragraph."
]
SiteSetting.post_excerpts_in_emails = true
SiteSetting.post_excerpt_maxlength = paragraphs.first.length
response.update!(raw: paragraphs.join("\n\n"))
mail = UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
mail_html = mail.html_part.body.to_s
expect(mail_html.scan(/#{paragraphs[0]}/).count).to eq(1)
expect(mail_html.scan(/#{paragraphs[1]}/).count).to eq(0)
end
end
describe '.user_posted' do
let(:response_by_user) { Fabricate(:user, name: "John Doe", username: "john") }
let(:topic) { Fabricate(:topic, title: "Super cool topic") }
let(:post) { Fabricate(:post, topic: topic) }
let(:response) { Fabricate(:post, topic: topic, user: response_by_user) }
let(:user) { Fabricate(:user) }
let(:notification) { Fabricate(:posted_notification, user: user, post: response) }
it 'generates a correct email' do
SiteSetting.enable_names = false
mail = UserNotifications.user_posted(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
# from should not include full user name if "show user full names" is disabled
expect(mail[:from].display_names).to_not eql(['John Doe'])
# from should include username if "show user full names" is disabled
expect(mail[:from].display_names).to eql(['john via Discourse'])
# subject should not include category name
expect(mail.subject).not_to match(/Uncategorized/)
# 1 respond to links as no context by default
expect(mail.html_part.body.to_s.scan(/to respond/).count).to eq(1)
# 1 unsubscribe link
expect(mail.html_part.body.to_s.scan(/To unsubscribe/).count).to eq(1)
# side effect, topic user is updated with post number
tu = TopicUser.get(post.topic_id, user)
expect(tu.last_emailed_post_number).to eq(response.post_number)
end
it "doesn't include details when private_email is enabled" do
SiteSetting.private_email = true
mail = UserNotifications.user_posted(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.html_part.body.to_s).to_not include(response.raw)
expect(mail.text_part.to_s).to_not include(response.raw)
end
it "uses the original subject for staged users" do
incoming_email = Fabricate(
:incoming_email,
subject: "Original Subject",
post: post,
topic: post.topic,
user: user
)
mail = UserNotifications.user_posted(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to match(/Super cool topic/)
user.update!(staged: true)
mail = UserNotifications.user_posted(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to eq("Re: Original Subject")
another_post = Fabricate(:post, topic: topic)
incoming_email.update!(post_id: another_post.id)
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to match(/Super cool topic/)
end
end
describe '.user_private_message' do
let(:response_by_user) { Fabricate(:user, name: "", username: "john") }
let(:topic) { Fabricate(:private_message_topic, title: "Super cool topic") }
let(:post) { Fabricate(:post, topic: topic) }
let(:response) { Fabricate(:post, topic: topic, user: response_by_user) }
let(:user) { Fabricate(:user) }
let(:notification) { Fabricate(:private_message_notification, user: user, post: response) }
it 'generates a correct email' do
SiteSetting.enable_names = true
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
# from should include username if full user name is not provided
expect(mail[:from].display_names).to eql(['john via Discourse'])
# subject should include "[PM]"
expect(mail.subject).to include("[PM] ")
# 1 "visit message" link
expect(mail.html_part.body.to_s.scan(/Visit Message/).count).to eq(1)
# 1 respond to link
expect(mail.html_part.body.to_s.scan(/to respond/).count).to eq(1)
# 1 unsubscribe link
expect(mail.html_part.body.to_s.scan(/To unsubscribe/).count).to eq(1)
# side effect, topic user is updated with post number
tu = TopicUser.get(topic.id, user)
expect(tu.last_emailed_post_number).to eq(response.post_number)
end
it "doesn't include details when private_email is enabled" do
SiteSetting.private_email = true
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.html_part.body.to_s).to_not include(response.raw)
expect(mail.html_part.body.to_s).to_not include(topic.url)
expect(mail.text_part.to_s).to_not include(response.raw)
expect(mail.text_part.to_s).to_not include(topic.url)
end
it "doesn't include group name in subject" do
group = Fabricate(:group)
topic.allowed_groups = [group]
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to include("[PM] ")
end
it "includes a list of participants (except for the destination user), groups first with member lists" do
group1 = Fabricate(:group, name: "group1")
group2 = Fabricate(:group, name: "group2")
user1 = Fabricate(:user, username: "one", groups: [group1, group2])
user2 = Fabricate(:user, username: "two", groups: [group1], staged: true)
topic.allowed_users = [user, user1, user2]
topic.allowed_groups = [group1, group2]
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.body).to include("[group1 (2)](http://test.localhost/g/group1), [group2 (1)](http://test.localhost/g/group2), [one](http://test.localhost/u/one), [two](http://test.localhost/u/two)")
end
context "when SiteSetting.group_name_in_subject is true" do
before do
SiteSetting.group_in_subject = true
end
let(:group) { Fabricate(:group, name: "my_group") }
let(:mail) do
UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
end
shared_examples "includes first group name" do
it "includes first group name in subject" do
expect(mail.subject).to include("[my_group] ")
end
context "when first group has full name" do
it "includes full name in subject" do
group.full_name = "My Group"
group.save
expect(mail.subject).to include("[My Group] ")
end
end
end
context "with one group in pm" do
before do
topic.allowed_groups = [group]
end
include_examples "includes first group name"
end
context "with multiple groups in pm" do
let(:group2) { Fabricate(:group) }
before do
topic.allowed_groups = [group, group2]
end
include_examples "includes first group name"
end
context "with no groups in pm" do
it "includes %{optional_pm} in subject" do
expect(mail.subject).to include("[PM] ")
end
end
end
it "uses the original subject for staged users when topic was started via email" do
incoming_email = Fabricate(
:incoming_email,
subject: "Original Subject",
post: post,
topic: topic,
user: user
)
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to match(/Super cool topic/)
user.update!(staged: true)
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to eq("Re: Original Subject")
another_post = Fabricate(:post, topic: topic)
incoming_email.update!(post_id: another_post.id)
mail = UserNotifications.user_private_message(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
expect(mail.subject).to match(/Super cool topic/)
end
end
it 'adds a warning when mail limit is reached' do
SiteSetting.max_emails_per_day_per_user = 2
user = Fabricate(:user)
user.email_logs.create!(
email_type: 'blah',
to_address: user.email,
user_id: user.id
)
post = Fabricate(:post)
reply = Fabricate(:post, topic_id: post.topic_id)
notification = Fabricate(
:notification,
topic_id: post.topic_id,
post_number: reply.post_number,
user: post.user,
data: { original_username: 'bob' }.to_json
)
mail = UserNotifications.user_replied(
user,
post: reply,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash
)
# WARNING: you reached the limit of 100 email notifications per day. Further emails will be suppressed.
# Consider watching less topics or disabling mailing list mode.
expect(mail.html_part.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
expect(mail.body.to_s).to match(I18n.t("user_notifications.reached_limit", count: 2))
end
def expects_build_with(condition)
UserNotifications.any_instance.expects(:build_email).with(user.email, condition)
mailer = UserNotifications.public_send(
mail_type, user,
notification_type: Notification.types[notification.notification_type],
notification_data_hash: notification.data_hash,
post: notification.post
)
mailer.message
end
shared_examples "supports reply by email" do
context "with reply_by_email" do
it "should have allow_reply_by_email set when that feature is enabled" do
expects_build_with(has_entry(:allow_reply_by_email, true))
end
end
end
shared_examples "no reply by email" do
context "with reply_by_email" do
it "doesn't support reply by email" do
expects_build_with(Not(has_entry(:allow_reply_by_email, true)))
end
end
end
shared_examples "respect for private_email" do
context "with private_email" do
it "doesn't support reply by email" do
SiteSetting.private_email = true
mailer = UserNotifications.public_send(
mail_type,
user,
notification_type: Notification.types[notification.notification_type],
notification_data_hash: notification.data_hash,
post: notification.post
)
message = mailer.message
topic = notification.post.topic
expect(message.html_part.body.to_s).not_to include(topic.title)
expect(message.html_part.body.to_s).not_to include(topic.slug)
expect(message.text_part.body.to_s).not_to include(topic.title)
expect(message.text_part.body.to_s).not_to include(topic.slug)
end
end
end
# The parts of emails that are derived from templates are translated
shared_examples "sets user locale" do
context "with set locale for translating templates" do
it "sets the locale" do
expects_build_with(has_key(:locale))
end
end
end
shared_examples "notification email building" do
let(:post) { Fabricate(:post, user: user) }
let(:mail_type) { "user_#{notification_type}" }
let(:mail_template) { "user_notifications.#{mail_type}" }
let(:username) { "walterwhite" }
let(:notification) do
Fabricate(:notification,
user: user,
topic: post.topic,
notification_type: Notification.types[notification_type],
post_number: post.post_number,
data: { original_username: username }.to_json)
end
describe 'email building' do
it "has a username" do
expects_build_with(has_entry(:username, username))
end
it "has a url" do
expects_build_with(has_key(:url))
end
it "has a template" do
expects_build_with(has_entry(:template, mail_template))
end
it "overrides the html part" do
expects_build_with(has_key(:html_override))
end
it "has a message" do
expects_build_with(has_key(:message))
end
it "has a context" do
expects_build_with(has_key(:context))
end
it "has an unsubscribe link" do
expects_build_with(has_key(:add_unsubscribe_link))
end
it "has an post_id" do
expects_build_with(has_key(:post_id))
end
it "has an topic_id" do
expects_build_with(has_key(:topic_id))
end
it "should have user name as from_alias" do
SiteSetting.enable_names = true
SiteSetting.display_name_on_posts = true
expects_build_with(has_entry(:from_alias, "#{user.name} via Discourse"))
end
it "should not have user name as from_alias if display_name_on_posts is disabled" do
SiteSetting.enable_names = false
SiteSetting.display_name_on_posts = false
expects_build_with(has_entry(:from_alias, "walterwhite via Discourse"))
end
it "should explain how to respond" do
expects_build_with(Not(has_entry(:include_respond_instructions, false)))
end
it "should not explain how to respond if the user is suspended" do
User.any_instance.stubs(:suspended?).returns(true)
expects_build_with(has_entry(:include_respond_instructions, false))
end
context "when customized" do
let(:custom_body) do
body = +<<~BODY
You are now officially notified.
%{header_instructions}
%{message} %{respond_instructions}
%{topic_title_url_encoded}
%{site_title_url_encoded}
BODY
body << "%{context}" if notification_type != :invited_to_topic
body
end
before do
TranslationOverride.upsert!(
SiteSetting.default_locale,
"#{mail_template}.text_body_template",
custom_body
)
end
it "shouldn't use the default html_override" do
expects_build_with(Not(has_key(:html_override)))
end
end
end
end
describe "user mentioned email" do
include_examples "notification email building" do
let(:notification_type) { :mentioned }
include_examples "respect for private_email"
include_examples "supports reply by email"
include_examples "sets user locale"
end
end
describe "group mentioned email" do
include_examples "notification email building" do
let(:notification_type) { :group_mentioned }
let(:post) { Fabricate(:private_message_post) }
let(:user) { post.user }
let(:mail_type) { "group_mentioned" }
let(:mail_template) { "user_notifications.user_#{notification_type}_pm" }
include_examples "respect for private_email"
include_examples "supports reply by email"
include_examples "sets user locale"
end
end
describe "user replied" do
include_examples "notification email building" do
let(:notification_type) { :replied }
include_examples "respect for private_email"
include_examples "supports reply by email"
include_examples "sets user locale"
end
end
describe "user quoted" do
include_examples "notification email building" do
let(:notification_type) { :quoted }
include_examples "respect for private_email"
include_examples "supports reply by email"
include_examples "sets user locale"
end
end
describe "user posted" do
include_examples "notification email building" do
let(:notification_type) { :posted }
include_examples "respect for private_email"
include_examples "supports reply by email"
include_examples "sets user locale"
end
end
describe "user invited to a private message" do
include_examples "notification email building" do
let(:notification_type) { :invited_to_private_message }
let(:post) { Fabricate(:private_message_post) }
let(:user) { post.user }
let(:mail_template) { "user_notifications.user_#{notification_type}_pm" }
include_examples "respect for private_email"
include_examples "no reply by email"
include_examples "sets user locale"
end
end
describe "group invited to a private message" do
include_examples "notification email building" do
let(:notification_type) { :invited_to_private_message }
let(:post) { Fabricate(:private_message_post) }
let(:user) { post.user }
let(:group) { Fabricate(:group) }
let(:mail_template) { "user_notifications.user_#{notification_type}_pm_group" }
before do
notification.data_hash[:group_id] = group.id
notification.save!
end
it "should include the group name" do
expects_build_with(has_entry(:group_name, group.name))
end
include_examples "respect for private_email"
include_examples "no reply by email"
include_examples "sets user locale"
end
end
describe "user invited to a topic" do
let(:notification_type) { :invited_to_topic }
include_examples "notification email building" do
include_examples "respect for private_email"
include_examples "no reply by email"
include_examples "sets user locale"
end
context "when showing the right name in 'From' field" do
let(:inviter) { Fabricate(:user) }
let(:invitee) { Fabricate(:user) }
let(:notification) do
Fabricate(:notification,
notification_type: Notification.types[:invited_to_topic],
user: invitee,
topic: post.topic,
post_number: post.post_number,
data: {
topic_title: post.topic.title,
display_username: inviter.username,
original_user_id: inviter.id,
original_username: inviter.username
}.to_json
)
end
let(:mailer) do
UserNotifications.public_send(
"user_invited_to_topic",
invitee,
notification_type: Notification.types[notification.notification_type],
notification_data_hash: notification.data_hash,
post: notification.post
)
end
it "sends the email as the inviter" do
SiteSetting.enable_names = false
expect(mailer.message.to_s).to include("From: #{inviter.username} via #{SiteSetting.title} <#{SiteSetting.notification_email}>")
end
it "sends the email as the inviter" do
expect(mailer.message.to_s).to include("From: #{inviter.name} via #{SiteSetting.title} <#{SiteSetting.notification_email}>")
end
end
end
describe "watching first post" do
include_examples "notification email building" do
let(:notification_type) { :invited_to_topic }
include_examples "respect for private_email"
include_examples "no reply by email"
include_examples "sets user locale"
end
end
# notification emails derived from templates are translated into the user's locale
shared_context "with notification derived from template" do
let(:user) { Fabricate(:user, locale: locale) }
let(:mail_type) { mail_type }
let(:notification) { Fabricate(:notification, user: user) }
end
describe "notifications from template" do
context "when user locale is allowed" do
before do
SiteSetting.allow_user_locale = true
end
%w(signup signup_after_approval confirm_old_email notify_old_email confirm_new_email
forgot_password admin_login account_created).each do |mail_type|
include_examples "with notification derived from template" do
let(:locale) { "fr" }
let(:mail_type) { mail_type }
it "sets the locale" do
expects_build_with(has_entry(:locale, "fr"))
end
end
end
end
context "when user locale is not allowed" do
before do
SiteSetting.allow_user_locale = false
end
%w(signup signup_after_approval notify_old_email confirm_old_email confirm_new_email
forgot_password admin_login account_created).each do |mail_type|
include_examples "with notification derived from template" do
let(:locale) { "fr" }
let(:mail_type) { mail_type }
it "sets the locale" do
expects_build_with(has_entry(:locale, "en"))
end
end
end
end
end
describe "#participants" do
fab!(:group1) { Fabricate(:group, name: "group1") }
fab!(:group2) { Fabricate(:group, name: "group2") }
fab!(:group3) { Fabricate(:group, name: "group3") }
fab!(:user1) { Fabricate(:user, username: "one", name: nil, groups: [group1, group2]) }
fab!(:user2) { Fabricate(:user, username: "two", name: nil, groups: [group1]) }
fab!(:user3) { Fabricate(:user, username: "three", name: nil, groups: [group3]) }
fab!(:user4) { Fabricate(:user, username: "four", name: nil, groups: [group1, group3]) }
fab!(:admin) { Fabricate(:admin, username: "admin", name: nil) }
fab!(:topic) do
t = Fabricate(:private_message_topic, title: "Super cool topic")
t.allowed_users = [user1, user2, user3, user4, admin]
t.allowed_groups = [group1]
t
end
fab!(:posts) do
[
Fabricate(:post, topic: topic, post_number: 1, user: user2),
Fabricate(:post, topic: topic, post_number: 2, user: user1),
Fabricate(:post, topic: topic, post_number: 3, user: user2),
Fabricate(:small_action, topic: topic, post_number: 4, user: admin),
Fabricate(:post, topic: topic, post_number: 5, user: user4),
Fabricate(:post, topic: topic, post_number: 6, user: user3),
Fabricate(:post, topic: topic, post_number: 7, user: user4)
]
end
it "returns a list of participants (except for the recipient), groups first, followed by users in order of their last reply" do
expect(UserNotifications.participants(posts.last, user3)).to eq("[group1 (3)](http://test.localhost/g/group1), " \
"[four](http://test.localhost/u/four), [two](http://test.localhost/u/two), [one](http://test.localhost/u/one), " \
"[admin](http://test.localhost/u/admin)")
end
it "caps the list according to site setting" do
SiteSetting.max_participant_names = 3
list = "[group1 (3)](http://test.localhost/g/group1), [four](http://test.localhost/u/four), [two](http://test.localhost/u/two)"
expect(UserNotifications.participants(posts.last, user3)).to eq(I18n.t("user_notifications.more_pm_participants", participants: list, count: 2))
end
it "orders groups by user count" do
SiteSetting.max_participant_names = 3
topic.allowed_groups = [group1, group2, group3]
list = "[group1 (3)](http://test.localhost/g/group1), [group3 (2)](http://test.localhost/g/group3), [group2 (1)](http://test.localhost/g/group2)"
expect(UserNotifications.participants(posts.last, user3)).to eq(I18n.t("user_notifications.more_pm_participants", participants: list, count: 4))
end
it "orders users by their last reply and user id" do
expect(UserNotifications.participants(posts[-3], user4)).to eq("[group1 (3)](http://test.localhost/g/group1), " \
"[two](http://test.localhost/u/two), [one](http://test.localhost/u/one), [three](http://test.localhost/u/three), " \
"[admin](http://test.localhost/u/admin)")
end
it "prefers full group names when available" do
SiteSetting.max_participant_names = 2
topic.allowed_groups = [group1, group2]
group2.update!(full_name: "Awesome Group")
list = "[group1 (3)](http://test.localhost/g/group1), [Awesome Group (1)](http://test.localhost/g/group2)"
expect(UserNotifications.participants(posts.last, user3)).to eq(I18n.t("user_notifications.more_pm_participants", participants: list, count: 4))
end
it "always uses usernames when prioritize_username_in_ux is enabled" do
user4.update!(name: "James Bond")
user1.update!(name: "Indiana Jones")
SiteSetting.prioritize_username_in_ux = true
expect(UserNotifications.participants(posts.last, user3)).to eq("[group1 (3)](http://test.localhost/g/group1), " \
"[four](http://test.localhost/u/four), [two](http://test.localhost/u/two), [one](http://test.localhost/u/one), " \
"[admin](http://test.localhost/u/admin)")
SiteSetting.prioritize_username_in_ux = false
expect(UserNotifications.participants(posts.last, user3)).to eq("[group1 (3)](http://test.localhost/g/group1), " \
"[James Bond](http://test.localhost/u/four), [two](http://test.localhost/u/two), [Indiana Jones](http://test.localhost/u/one), " \
"[admin](http://test.localhost/u/admin)")
end
it "reveals the email address of staged users if enabled" do
user4.update!(staged: true, email: "james.bond@mi6.invalid")
user1.update!(staged: true, email: "indiana.jones@example.com")
SiteSetting.prioritize_username_in_ux = true
expect(UserNotifications.participants(posts.last, user3, reveal_staged_email: true)).to eq( \
"[group1 (3)](http://test.localhost/g/group1), james.bond@mi6.invalid, [two](http://test.localhost/u/two), " \
"indiana.jones@example.com, [admin](http://test.localhost/u/admin)")
end
it "does only include human users" do
topic.allowed_users << Discourse.system_user
expect(UserNotifications.participants(posts.last, user3)).to eq("[group1 (3)](http://test.localhost/g/group1), " \
"[four](http://test.localhost/u/four), [two](http://test.localhost/u/two), [one](http://test.localhost/u/one), " \
"[admin](http://test.localhost/u/admin)")
end
end
describe ".account_silenced" do
fab!(:user_history) { Fabricate(:user_history, action: UserHistory.actions[:silence_user]) }
it "adds the silenced_till date in user's timezone" do
user.user_option.timezone = "Asia/Tbilisi" # GMT+4
user.silenced_till = DateTime.parse("May 25, 2020, 12:00pm")
mail = UserNotifications.account_silenced(user, { user_history: user_history })
expect(mail.body).to include("May 25, 2020, 4:00pm")
end
context "when user doesn't have timezone set" do
before do
user.user_option.timezone = nil
end
it "doesn't raise error" do
expect { UserNotifications.account_silenced(user) }.not_to raise_error
end
it "adds the silenced_till date in UTC" do
date = "May 25, 2020, 12:00pm"
user.silenced_till = DateTime.parse(date)
mail = UserNotifications.account_silenced(user, { user_history: user_history })
expect(mail.body).to include(date)
end
end
end
describe ".account_suspended" do
fab!(:user_history) { Fabricate(:user_history, action: UserHistory.actions[:suspend_user]) }
it "adds the suspended_till date in user's timezone" do
user.user_option.timezone = "Asia/Tbilisi" # GMT+4
user.suspended_till = DateTime.parse("May 25, 2020, 12:00pm")
mail = UserNotifications.account_suspended(user, { user_history: user_history })
expect(mail.body).to include("May 25, 2020, 4:00pm")
end
context "when user doesn't have timezone set" do
before do
user.user_option.timezone = nil
end
it "doesn't raise error" do
expect { UserNotifications.account_suspended(user) }.not_to raise_error
end
it "adds the suspended_till date in UTC" do
date = "May 25, 2020, 12:00pm"
user.suspended_till = DateTime.parse(date)
mail = UserNotifications.account_suspended(user, { user_history: user_history })
expect(mail.body).to include(date)
end
end
end
end