discourse/spec/mailers/user_notifications_spec.rb
Régis Hanol 5f6b6e9818 FIX: correctly compute the window for email summaries
In 958437e7dd we ensured that the email summaries are properly sent based on 'digest_attempted_at' for people who barely/never visit the forum.

This fixed the "frequency" of the email summaries but introduced a bug where the digest would be sent even though there wasn't anything new since for some users.

The logic we use to compute the threshold date for the content to be included in the digest was

```ruby
@since = opts[:since] || user.last_seen_at || user.user_stat&.digest_attempted_at || 1.month.ago
```

It was working as expected for users who haven never been seen but for users who have connected at least once, we would use their "last_seen_at" date as the "threshold date" for the content to be sent in a summary 😬

This fix changes the logic to be the most recent date amongst the `last_seen_at`, `digest_attempted_at` and `1.month.ago` so it's correctly handling cases where

- user has never been seen nor emailed a summary
- user has been seen in a while but has recently been sent a summary
- user has been sent a summary recently but hasn't been seen in a while.
2024-05-27 22:33:51 +02:00

1575 lines
53 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(:email) { UserNotifications.signup(user) }
it "works" do
expect(email.to).to eq([user.email])
expect(email.subject).to be_present
expect(email.from).to eq([SiteSetting.notification_email])
expect(email.body).to be_present
end
end
describe ".forgot_password" do
subject(:email) { UserNotifications.forgot_password(user) }
it "works" do
expect(email.to).to eq([user.email])
expect(email.subject).to be_present
expect(email.from).to eq([SiteSetting.notification_email])
expect(email.body).to be_present
end
end
describe ".post_approved" do
fab!(: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) { { requested_by_admin: requested_by_admin, email_token: token } }
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
subject(:email) { UserNotifications.email_login(user, email_token: email_token) }
let(:email_token) do
Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login]).token
end
it "generates the right email" do
expect(email.to).to eq([user.email])
expect(email.from).to eq([SiteSetting.notification_email])
expect(email.subject).to eq(
I18n.t("user_notifications.email_login.subject_template", email_prefix: SiteSetting.title),
)
expect(email.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(:email) { UserNotifications.digest(user) }
after { Discourse.redis.keys("summary-new-users:*").each { |key| Discourse.redis.del(key) } }
context "without new topics" do
it "doesn't send the email" do
expect(email.to).to be_blank
end
end
context "with topics only from new users" do
let!(:new_today) do
Fabricate(
:topic,
user: Fabricate(:user, trust_level: TrustLevel[0], created_at: 10.minutes.ago),
title: "Hey everyone look at me",
)
end
let!(:new_yesterday) do
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",
)
end
it "returns topics from new users if they're more than 24 hours old" do
expect(email.to).to eq([user.email])
html = email.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
fab!(:coding_horror)
let!(:popular_topic) { Fabricate(:topic, user: coding_horror, created_at: 1.hour.ago) }
let!(:another_popular_topic) do
Fabricate(:topic, user: coding_horror, created_at: 1.hour.ago)
end
let!(:post) { Fabricate(:post, topic: popular_topic, post_number: 1) }
let!(:another_post) { Fabricate(:post, topic: another_popular_topic, post_number: 1) }
it "works" do
expect(email.to).to eq([user.email])
expect(email.subject).to be_present
expect(email.from).to eq([SiteSetting.notification_email])
expect(email.html_part.body.to_s).to be_present
expect(email.text_part.body.to_s).to be_present
expect(email.header["List-Unsubscribe"].to_s).to match(/\/email\/unsubscribe\/\h{64}/)
expect(email.header["X-Discourse-Topic-Ids"].to_s).to eq(
"#{another_popular_topic.id},#{popular_topic.id}",
)
expect(email.header["X-Discourse-Post-Ids"].to_s).to eq("#{another_post.id},#{post.id}")
expect(email.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(email.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(email.subject).to match(/Try Discourse/)
expect(email.subject).not_to match(/Discourse Meta/)
end
it "includes unread likes received count within the since date" do
Fabricate(
:notification,
user: user,
notification_type: Notification.types[:liked],
created_at: 2.months.ago,
)
Fabricate(
:notification,
user: user,
notification_type: Notification.types[:liked],
read: true,
)
Fabricate(:notification, user: user, notification_type: Notification.types[:liked])
Fabricate(:notification, user: user, notification_type: Notification.types[:liked])
digest = UserNotifications.digest(user, since: 1.month.ago.to_date.to_s)
parsed_html = Nokogiri::HTML5.fragment(digest.html_part.body.to_s)
expect(parsed_html.css(".header-stat-count #likes_received_stat_count strong").text).to eq(
"2",
)
expect(
parsed_html.css(".header-stat-description #likes_received_stat_description strong").text,
).to eq("Likes Received")
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 = email.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 = email.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 = email.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 = email.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 = email.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 = email.html_part.body.to_s
expect(html).to include "F0F0F0"
expect(html).to include "1E1E1E"
end
it "supports subfolder" do
set_subfolder "/forum"
html = email.html_part.body.to_s
text = email.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(email.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 = email.html_part.to_s
expect(html).to match(' lang="pl-PL"')
expect(html).to match(' xml:lang="pl-PL"')
end
it "uses digest_attempted_at when user hasn't been seen in a while" do
user.update!(last_seen_at: 7.days.ago)
user.user_stat.update!(digest_attempted_at: 30.minutes.ago)
expect(email.to).to be_nil
end
it "uses last_seen_at when user has been sent a digest in a while" do
user.update!(last_seen_at: 30.minutes.ago)
user.user_stat.update!(digest_attempted_at: 7.days.ago)
expect(email.to).to be_nil
end
it "caps at 1 month when user has never been seen or sent a digest" do
old_topic = Fabricate(:topic, created_at: 2.months.ago)
user.update!(last_seen_at: nil)
user.user_stat.update!(digest_attempted_at: nil)
expect(email.to).to contain_exactly(user.email)
html = email.html_part.body.to_s
expect(html).not_to include(old_topic.title)
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", public_topic_count: 1) }
let(:tag2) { Fabricate(:tag, name: "Taggie", public_topic_count: 3) }
let(:tag3) { Fabricate(:tag, name: "Teggo", public_topic_count: 2) }
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) do
Fabricate(
:topic,
category: category,
tags: [tag1, tag2, tag3, hidden_tag],
title: "Super cool topic",
)
end
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
describe "number of tags shown in subject line" do
describe "max_tags_per_email_subject siteSetting enabled" do
before { SiteSetting.enable_max_tags_per_email_subject = true }
it "should match max_tags_per_email_subject" do
SiteSetting.email_subject =
"[%{site_name}] %{optional_pm}%{optional_cat}%{optional_tags}%{topic_title}"
SiteSetting.max_tags_per_topic = 1
SiteSetting.max_tags_per_email_subject = 2
mail =
UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash,
)
expect(mail.subject).to eq(
"[Discourse] [#{category.name}] #{tag2.name} #{tag3.name} #{topic.title}",
)
end
end
describe "max_tags_per_email_subject siteSetting disabled" do
before { SiteSetting.enable_max_tags_per_email_subject = false }
it "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 = 2
SiteSetting.max_tags_per_email_subject = 1
mail =
UserNotifications.user_replied(
user,
post: response,
notification_type: notification.notification_type,
notification_data_hash: notification.data_hash,
)
expect(mail.subject).to eq(
"[Discourse] [#{category.name}] #{tag2.name} #{tag3.name} #{topic.title}",
)
end
end
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 { SiteSetting.group_in_subject = true }
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 { topic.allowed_groups = [group] }
include_examples "includes first group name"
end
context "with multiple groups in pm" do
let(:group2) { Fabricate(:group) }
before { topic.allowed_groups = [group, group2] }
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 { SiteSetting.allow_user_locale = true }
%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 { SiteSetting.allow_user_locale = false }
%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 { user.user_option.timezone = nil }
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
context "when user timezone is invalid" do
before { user.user_option.timezone = "" }
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 { user.user_option.timezone = nil }
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
context "when user timezone is invalid" do
before { user.user_option.timezone = "" }
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