discourse/spec/models/topic_user_spec.rb
Sam 1c67917367
FIX: disable storing invalid post and topic timing when sent from client (#26683)
This ensures we only ever store correct post and topic timing when the client
notifies.

Previous to this change we would blindly trust the client.

Additionally this has error correction code that will correct the last seen
post number when you visit a topic with incorrect timings.
2024-04-19 18:10:50 +10:00

620 lines
20 KiB
Ruby

# frozen_string_literal: true
RSpec.describe TopicUser do
let :watching do
TopicUser.notification_levels[:watching]
end
let :regular do
TopicUser.notification_levels[:regular]
end
let :tracking do
TopicUser.notification_levels[:tracking]
end
describe "#unwatch_categories!" do
it "correctly unwatches categories" do
op_topic = Fabricate(:topic)
another_topic = Fabricate(:topic)
tracked_topic = Fabricate(:topic)
user = op_topic.user
TopicUser.change(user.id, op_topic, notification_level: watching)
TopicUser.change(user.id, another_topic, notification_level: watching)
TopicUser.change(
user.id,
tracked_topic,
notification_level: watching,
total_msecs_viewed: SiteSetting.default_other_auto_track_topics_after_msecs + 1,
)
TopicUser.unwatch_categories!(user, [Fabricate(:category).id, Fabricate(:category).id])
expect(TopicUser.get(another_topic, user).notification_level).to eq(watching)
TopicUser.unwatch_categories!(user, [op_topic.category_id])
expect(TopicUser.get(op_topic, user).notification_level).to eq(watching)
expect(TopicUser.get(another_topic, user).notification_level).to eq(regular)
expect(TopicUser.get(tracked_topic, user).notification_level).to eq(tracking)
end
end
describe "#notification_levels" do
context "when verifying enum sequence" do
before { @notification_levels = TopicUser.notification_levels }
it "'muted' should be at 0 position" do
expect(@notification_levels[:muted]).to eq(0)
end
it "'watching' should be at 3rd position" do
expect(@notification_levels[:watching]).to eq(3)
end
end
end
describe "#notification_reasons" do
context "when verifying enum sequence" do
before { @notification_reasons = TopicUser.notification_reasons }
it "'created_topic' should be at 1st position" do
expect(@notification_reasons[:created_topic]).to eq(1)
end
it "'plugin_changed' should be at 9th position" do
expect(@notification_reasons[:plugin_changed]).to eq(9)
end
end
end
it { is_expected.to belong_to :user }
it { is_expected.to belong_to :topic }
fab!(:user)
let(:topic) do
u = Fabricate(:user, refresh_auto_groups: true)
guardian = Guardian.new(u)
TopicCreator.create(u, guardian, title: "this is my topic title")
end
let(:topic_user) { TopicUser.get(topic, user) }
let(:topic_creator_user) { TopicUser.get(topic, topic.user) }
let(:new_user) do
u = Fabricate(:user)
u.user_option.update_columns(auto_track_topics_after_msecs: 1000)
u
end
let(:topic_new_user) { TopicUser.get(topic, new_user) }
let(:yesterday) { DateTime.now.yesterday }
def ensure_topic_user
TopicUser.change(user, topic, last_emailed_post_number: 1)
end
describe "unpinned" do
it "defaults to blank" do
ensure_topic_user
expect(topic_user.cleared_pinned_at).to be_blank
end
end
describe "notifications" do
it "should trigger the right DiscourseEvent" do
called = false
blk = Proc.new { called = true }
begin
DiscourseEvent.on(:topic_notification_level_changed, &blk)
TopicUser.change(
user.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking],
)
expect(called).to eq(true)
ensure
DiscourseEvent.off(:topic_notification_level_changed, &blk)
end
end
it "should be set to tracking if auto_track_topics is enabled" do
user.user_option.update_column(:auto_track_topics_after_msecs, 0)
ensure_topic_user
expect(TopicUser.get(topic, user).notification_level).to eq(
TopicUser.notification_levels[:tracking],
)
end
it "should reset regular topics to tracking topics if auto track is changed" do
ensure_topic_user
user.user_option.auto_track_topics_after_msecs = 0
user.user_option.save
expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:tracking])
end
it 'should be set to "regular" notifications, by default on non creators' do
ensure_topic_user
expect(TopicUser.get(topic, user).notification_level).to eq(
TopicUser.notification_levels[:regular],
)
end
it "reason should reset when changed" do
topic.notify_muted!(topic.user)
expect(TopicUser.get(topic, topic.user).notifications_reason_id).to eq(
TopicUser.notification_reasons[:user_changed],
)
end
it "should have the correct reason for a user change when watched" do
topic.notify_watch!(user)
expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:watching])
expect(topic_user.notifications_reason_id).to eq(
TopicUser.notification_reasons[:user_changed],
)
expect(topic_user.notifications_changed_at).not_to eq(nil)
end
it "should have the correct reason for a user change when set to regular" do
topic.notify_regular!(user)
expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:regular])
expect(topic_user.notifications_reason_id).to eq(
TopicUser.notification_reasons[:user_changed],
)
expect(topic_user.notifications_changed_at).not_to eq(nil)
end
it "should have the correct reason for a user change when set to regular" do
topic.notify_muted!(user)
expect(topic_user.notification_level).to eq(TopicUser.notification_levels[:muted])
expect(topic_user.notifications_reason_id).to eq(
TopicUser.notification_reasons[:user_changed],
)
expect(topic_user.notifications_changed_at).not_to eq(nil)
end
it "should watch topics a user created" do
expect(topic_creator_user.notification_level).to eq(TopicUser.notification_levels[:watching])
expect(topic_creator_user.notifications_reason_id).to eq(
TopicUser.notification_reasons[:created_topic],
)
end
end
describe "visited at" do
it "set upon initial visit" do
freeze_time yesterday
TopicUser.track_visit!(topic.id, user.id)
expect(topic_user.first_visited_at.to_i).to eq(yesterday.to_i)
expect(topic_user.last_visited_at.to_i).to eq(yesterday.to_i)
end
it "updates upon repeat visit" do
freeze_time yesterday
TopicUser.track_visit!(topic.id, user.id)
freeze_time Time.zone.now
TopicUser.track_visit!(topic.id, user.id)
# reload is a no go
topic_user = TopicUser.get(topic, user)
expect(topic_user.first_visited_at.to_i).to eq(yesterday.to_i)
expect(topic_user.last_visited_at.to_i).to eq(Time.zone.now.to_i)
end
end
describe "read tracking" do
context "without auto tracking" do
let(:topic_user) { TopicUser.get(topic, user) }
it "should create a new record for a visit" do
freeze_time yesterday
TopicUser.update_last_read(user, topic.id, 1, 1, 0)
expect(topic_user.last_read_post_number).to eq(1)
expect(topic_user.last_visited_at.to_i).to eq(yesterday.to_i)
expect(topic_user.first_visited_at.to_i).to eq(yesterday.to_i)
end
it "should update the record for repeat visit" do
today = Time.zone.now
freeze_time Time.zone.now
# ensure data model is correct for the test
# logging an update to a row that does not exist
# is not supported
_post1 = Fabricate(:post, topic: topic)
_post2 = Fabricate(:post, topic: topic)
TopicUser.update_last_read(user, topic.id, 1, 1, 0)
tomorrow = 1.day.from_now
freeze_time tomorrow
Fabricate(:post, topic: topic, user: user)
channel = TopicTrackingState.unread_channel_key(user.id)
messages =
MessageBus.track_publish(channel) { TopicUser.update_last_read(user, topic.id, 2, 1, 0) }
expect(messages.blank?).to eq(false)
topic_user = TopicUser.get(topic, user)
expect(topic_user.last_read_post_number).to eq(2)
expect(topic_user.last_visited_at.to_i).to eq(today.to_i)
expect(topic_user.first_visited_at.to_i).to eq(today.to_i)
end
end
context "with private messages" do
fab!(:target_user) { Fabricate(:user, refresh_auto_groups: true) }
let(:post) do
create_post(archetype: Archetype.private_message, target_usernames: target_user.username)
end
let(:topic) { post.topic }
it "should ensure recipients and senders are watching" do
expect(TopicUser.get(topic, post.user).notification_level).to eq(
TopicUser.notification_levels[:watching],
)
expect(TopicUser.get(topic, target_user).notification_level).to eq(
TopicUser.notification_levels[:watching],
)
end
it "should ensure invited user is watching once visited" do
another_user = Fabricate(:user)
topic.invite(target_user, another_user.username)
TopicUser.track_visit!(topic.id, another_user.id)
expect(TopicUser.get(topic, another_user).notification_level).to eq(
TopicUser.notification_levels[:watching],
)
another_user = Fabricate(:user)
TopicUser.track_visit!(topic.id, another_user.id)
expect(TopicUser.get(topic, another_user).notification_level).to eq(
TopicUser.notification_levels[:regular],
)
end
it "should publish the right message_bus message" do
TopicUser.update_last_read(user, topic.id, 1, 1, 0)
Fabricate(:post, topic: topic, user: user)
channel = PrivateMessageTopicTrackingState.user_channel(user.id)
messages =
MessageBus.track_publish(channel) { TopicUser.update_last_read(user, topic.id, 2, 1, 0) }
expect(messages.blank?).to eq(false)
end
describe "inviting a group" do
let(:group) do
Fabricate(:group, default_notification_level: NotificationLevels.topic_levels[:tracking])
end
it "should use group's default notification level" do
another_user = Fabricate(:user, refresh_auto_groups: true)
group.add(another_user)
Jobs.run_immediately!
topic.invite_group(target_user, group)
expect(TopicUser.get(topic, another_user).notification_level).to eq(
TopicUser.notification_levels[:tracking],
)
another_user = Fabricate(:user)
topic.invite(target_user, another_user.username)
TopicUser.track_visit!(topic.id, another_user.id)
expect(TopicUser.get(topic, another_user).notification_level).to eq(
TopicUser.notification_levels[:watching],
)
end
end
end
context "with auto tracking" do
let(:post_creator) do
PostCreator.new(new_user, raw: Fabricate.build(:post).raw, topic_id: topic.id)
end
before { TopicUser.update_last_read(new_user, topic.id, 2, 2, 0) }
it "should automatically track topics you reply to" do
post_creator.create
expect(topic_new_user.notification_level).to eq(TopicUser.notification_levels[:tracking])
expect(topic_new_user.notifications_reason_id).to eq(
TopicUser.notification_reasons[:created_post],
)
end
it "should update tracking state when you reply" do
new_user.user_option.update_column(:notification_level_when_replying, 3)
post_creator.create
DB.exec(
"UPDATE topic_users set notification_level=2
WHERE topic_id = :topic_id AND user_id = :user_id",
topic_id: topic_new_user.topic_id,
user_id: topic_new_user.user_id,
)
TopicUser.auto_notification(
topic_new_user.user_id,
topic_new_user.topic_id,
TopicUser.notification_reasons[:created_post],
TopicUser.notification_levels[:watching],
)
tu = TopicUser.find_by(user_id: topic_new_user.user_id, topic_id: topic_new_user.topic_id)
expect(tu.notification_level).to eq(TopicUser.notification_levels[:watching])
end
it "should not update tracking state when you reply" do
new_user.user_option.update_column(:notification_level_when_replying, 3)
post_creator.create
DB.exec(
"UPDATE topic_users set notification_level=3
WHERE topic_id = :topic_id AND user_id = :user_id",
topic_id: topic_new_user.topic_id,
user_id: topic_new_user.user_id,
)
TopicUser.auto_notification(
topic_new_user.user_id,
topic_new_user.topic_id,
TopicUser.notification_reasons[:created_post],
TopicUser.notification_levels[:tracking],
)
tu = TopicUser.find_by(user_id: topic_new_user.user_id, topic_id: topic_new_user.topic_id)
expect(tu.notification_level).to eq(TopicUser.notification_levels[:watching])
end
it "should not update tracking state when state manually set to normal you reply" do
new_user.user_option.update_column(:notification_level_when_replying, 3)
post_creator.create
DB.exec(
"UPDATE topic_users set notification_level=1
WHERE topic_id = :topic_id AND user_id = :user_id",
topic_id: topic_new_user.topic_id,
user_id: topic_new_user.user_id,
)
TopicUser.auto_notification(
topic_new_user.user_id,
topic_new_user.topic_id,
TopicUser.notification_reasons[:created_post],
TopicUser.notification_levels[:tracking],
)
tu = TopicUser.find_by(user_id: topic_new_user.user_id, topic_id: topic_new_user.topic_id)
expect(tu.notification_level).to eq(TopicUser.notification_levels[:regular])
end
it "should not update tracking state when state manually set to muted you reply" do
new_user.user_option.update_column(:notification_level_when_replying, 3)
post_creator.create
DB.exec(
"UPDATE topic_users set notification_level=0
WHERE topic_id = :topic_id AND user_id = :user_id",
topic_id: topic_new_user.topic_id,
user_id: topic_new_user.user_id,
)
TopicUser.auto_notification(
topic_new_user.user_id,
topic_new_user.topic_id,
TopicUser.notification_reasons[:created_post],
TopicUser.notification_levels[:tracking],
)
tu = TopicUser.find_by(user_id: topic_new_user.user_id, topic_id: topic_new_user.topic_id)
expect(tu.notification_level).to eq(TopicUser.notification_levels[:muted])
end
it "should not automatically track topics you reply to and have set state manually" do
post_creator.create
TopicUser.change(
new_user,
topic,
notification_level: TopicUser.notification_levels[:regular],
)
expect(topic_new_user.notification_level).to eq(TopicUser.notification_levels[:regular])
expect(topic_new_user.notifications_reason_id).to eq(
TopicUser.notification_reasons[:user_changed],
)
end
it "should automatically track topics after they are read for long enough" do
expect(topic_new_user.notification_level).to eq(TopicUser.notification_levels[:regular])
TopicUser.update_last_read(
new_user,
topic.id,
2,
2,
SiteSetting.default_other_auto_track_topics_after_msecs + 1,
)
expect(TopicUser.get(topic, new_user).notification_level).to eq(
TopicUser.notification_levels[:tracking],
)
end
it "should not automatically track topics after they are read for long enough if changed manually" do
TopicUser.change(
new_user,
topic,
notification_level: TopicUser.notification_levels[:regular],
)
TopicUser.update_last_read(
new_user,
topic,
2,
2,
SiteSetting.default_other_auto_track_topics_after_msecs + 1,
)
expect(topic_new_user.notification_level).to eq(TopicUser.notification_levels[:regular])
end
it "should not automatically track PMs" do
new_user.user_option.update!(auto_track_topics_after_msecs: 0)
another_user = Fabricate(:user, refresh_auto_groups: true)
pm = Fabricate(:private_message_topic, user: another_user)
pm.invite(another_user, new_user.username)
TopicUser.track_visit!(pm.id, new_user.id)
TopicUser.update_last_read(new_user, pm.id, 2, 2, 1000)
expect(TopicUser.get(pm, new_user).notification_level).to eq(
TopicUser.notification_levels[:watching],
)
end
end
end
describe "change a flag" do
it "only inserts a row once, even on repeated calls" do
topic
user
expect {
TopicUser.change(user, topic.id, total_msecs_viewed: 1)
TopicUser.change(user, topic.id, total_msecs_viewed: 2)
TopicUser.change(user, topic.id, total_msecs_viewed: 3)
}.to change(TopicUser, :count).by(1)
end
describe "after creating a row" do
before { ensure_topic_user }
it "has a lookup" do
expect(TopicUser.lookup_for(user, [topic])).to be_present
end
it "has a key in the lookup for this forum topic" do
expect(TopicUser.lookup_for(user, [topic]).has_key?(topic.id)).to eq(true)
end
end
end
it "can scope by tracking" do
TopicUser.create!(
user_id: 1,
topic_id: 1,
notification_level: TopicUser.notification_levels[:tracking],
)
TopicUser.create!(
user_id: 2,
topic_id: 1,
notification_level: TopicUser.notification_levels[:watching],
)
TopicUser.create!(
user_id: 3,
topic_id: 1,
notification_level: TopicUser.notification_levels[:regular],
)
expect(TopicUser.tracking(1).count).to eq(2)
expect(TopicUser.tracking(10).count).to eq(0)
end
it "is able to self heal" do
p1 = Fabricate(:post)
p2 = Fabricate(:post, user: p1.user, topic: p1.topic, post_number: 2)
p1.topic.notifier.watch_topic!(p1.user_id)
DB.exec(
"UPDATE topic_users set last_read_post_number=0
WHERE topic_id = :topic_id AND user_id = :user_id",
topic_id: p1.topic_id,
user_id: p1.user_id,
)
[p1, p2].each do |p|
PostTiming.create(
topic_id: p.topic_id,
post_number: p.post_number,
user_id: p.user_id,
msecs: 100,
)
end
TopicUser.ensure_consistency!
tu = TopicUser.find_by(user_id: p1.user_id, topic_id: p1.topic_id)
expect(tu.last_read_post_number).to eq(p2.post_number)
end
describe "mailing_list_mode" do
it "will receive email notification for every topic" do
user1 = Fabricate(:user)
Jobs.run_immediately!
SiteSetting.disable_mailing_list_mode = false
SiteSetting.default_email_mailing_list_mode = true
SiteSetting.default_email_mailing_list_mode_frequency = 1
user2 = Fabricate(:user)
post = create_post
user3 = Fabricate(:user)
create_post(topic_id: post.topic_id)
# mails posts from earlier topics
tu = TopicUser.find_by(user_id: user3.id, topic_id: post.topic_id)
expect(tu.last_emailed_post_number).to eq(2)
# mails nothing to random users
tu = TopicUser.find_by(user_id: user1.id, topic_id: post.topic_id)
expect(tu).to eq(nil)
# mails other user
tu = TopicUser.find_by(user_id: user2.id, topic_id: post.topic_id)
expect(tu.last_emailed_post_number).to eq(2)
end
end
it "correctly triggers an event on first visit" do
begin
tracked_user = Fabricate(:user)
post = create_post
called = 0
visits = []
user_first_visit = ->(topic_id, user_id) do
visits << "#{topic_id}-#{user_id}"
called += 1
end
DiscourseEvent.on(:topic_first_visited_by_user, &user_first_visit)
expect(called).to eq(0)
TopicUser.change(tracked_user, post.topic.id, total_msecs_viewed: 1)
expect(visits).to eq(["#{post.topic.id}-#{tracked_user.id}"])
expect(called).to eq(1)
TopicUser.change(tracked_user, post.topic.id, total_msecs_viewed: 2)
expect(called).to eq(1)
ensure
DiscourseEvent.off(:topic_first_visited_by_user, &user_first_visit)
end
end
end