discourse/spec/models/post_timing_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

231 lines
7.4 KiB
Ruby

# frozen_string_literal: true
RSpec.describe PostTiming do
fab!(:post)
it { is_expected.to validate_presence_of :post_number }
it { is_expected.to validate_presence_of :msecs }
describe "pretend_read" do
fab!(:p1) { Fabricate(:post) }
fab!(:p2) { Fabricate(:post, topic: p1.topic, user: p1.user) }
fab!(:p3) { Fabricate(:post, topic: p1.topic, user: p1.user) }
let :topic_id do
p1.topic_id
end
def timing(user_id, post_number)
PostTiming.create!(topic_id: topic_id, user_id: user_id, post_number: post_number, msecs: 0)
end
def topic_user(user_id, last_read_post_number)
TopicUser.create!(
topic_id: topic_id,
user_id: user_id,
last_read_post_number: last_read_post_number,
)
end
it "works correctly" do
timing(1, 1)
timing(2, 1)
timing(2, 2)
timing(3, 1)
timing(3, 2)
timing(3, 3)
_tu_one = topic_user(1, 1)
_tu_two = topic_user(2, 2)
_tu_three = topic_user(3, 3)
PostTiming.pretend_read(topic_id, 2, 3)
expect(PostTiming.where(topic_id: topic_id, user_id: 1, post_number: 3).count).to eq(0)
expect(PostTiming.where(topic_id: topic_id, user_id: 2, post_number: 3).count).to eq(1)
expect(PostTiming.where(topic_id: topic_id, user_id: 3, post_number: 3).count).to eq(1)
tu = TopicUser.find_by(topic_id: topic_id, user_id: 1)
expect(tu.last_read_post_number).to eq(1)
tu = TopicUser.find_by(topic_id: topic_id, user_id: 2)
expect(tu.last_read_post_number).to eq(3)
tu = TopicUser.find_by(topic_id: topic_id, user_id: 3)
expect(tu.last_read_post_number).to eq(3)
end
end
describe "safeguard" do
it "doesn't store timings that are larger than the account lifetime" do
user = Fabricate(:user, created_at: 3.minutes.ago)
PostTiming.process_timings(user, post.topic_id, 1, [[post.post_number, 123]])
msecs = PostTiming.where(post_number: post.post_number, user_id: user.id).pluck(:msecs)[0]
expect(msecs).to eq(123)
PostTiming.process_timings(
user,
post.topic_id,
1,
[[post.post_number, 10.minutes.to_i * 1000]],
)
msecs = PostTiming.where(post_number: post.post_number, user_id: user.id).pluck(:msecs)[0]
expect(msecs).to eq(123 + PostTiming::MAX_READ_TIME_PER_BATCH)
end
end
describe "process_timings" do
# integration tests
it "processes timings correctly" do
PostActionNotifier.enable
(2..5).each { |i| Fabricate(:post, topic: post.topic, post_number: i) }
user2 = Fabricate(:coding_horror, created_at: 1.day.ago)
Topic.reset_highest(post.topic.id)
PostActionCreator.like(user2, post)
expect(post.user.unread_notifications).to eq(1)
PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 100]])
post.user.reload
expect(post.user.unread_notifications).to eq(0)
PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 1.day]])
user_visit = post.user.user_visits.order("id DESC").first
expect(user_visit.posts_read).to eq(1)
# Skip to bottom
PostTiming.process_timings(post.user, post.topic_id, 1, [[5, 100]])
expect(user_visit.reload.posts_read).to eq(2)
# Scroll up
PostTiming.process_timings(post.user, post.topic_id, 1, [[4, 100]])
expect(user_visit.reload.posts_read).to eq(3)
PostTiming.process_timings(post.user, post.topic_id, 1, [[2, 100], [3, 100]])
expect(user_visit.reload.posts_read).to eq(5)
end
it "does not count private message posts read" do
pm = Fabricate(:private_message_topic, user: Fabricate(:admin))
user1, user2 = pm.topic_allowed_users.map(&:user)
(1..3).each { |i| Fabricate(:post, topic: pm, user: user1) }
PostTiming.process_timings(user2, pm.id, 10, [[1, 100]])
user_visit = user2.user_visits.last
expect(user_visit.posts_read).to eq(0)
PostTiming.process_timings(user2, pm.id, 10, [[2, 100], [3, 100]])
expect(user_visit.reload.posts_read).to eq(0)
end
end
describe "recording" do
before do
@topic = post.topic
@coding_horror = Fabricate(:coding_horror)
@timing_attrs = {
msecs: 1234,
topic_id: post.topic_id,
user_id: @coding_horror.id,
post_number: post.post_number,
}
end
it "adds a view to the post" do
expect {
PostTiming.record_timing(@timing_attrs)
post.reload
}.to change(post, :reads).by(1)
end
it "doesn't update the posts read count if the topic is a PM" do
pm = Fabricate(:private_message_post).topic
@timing_attrs = @timing_attrs.merge(topic_id: pm.id)
PostTiming.record_timing(@timing_attrs)
expect(@coding_horror.user_stat.posts_read_count).to eq(0)
end
describe "multiple calls" do
it "correctly works" do
PostTiming.record_timing(@timing_attrs)
PostTiming.record_timing(@timing_attrs)
timing =
PostTiming.find_by(
topic_id: post.topic_id,
user_id: @coding_horror.id,
post_number: post.post_number,
)
expect(timing).to be_present
expect(timing.msecs).to eq(2468)
expect(@coding_horror.user_stat.posts_read_count).to eq(1)
end
end
end
describe "decrementing posts read count when destroying post timings" do
let(:initial_read_count) { 0 }
let(:post) { Fabricate(:post, reads: initial_read_count) }
before { PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 100]]) }
it "#destroy_last_for decrements the reads count for a post" do
PostTiming.destroy_last_for(post.user, topic_id: post.topic_id)
expect(post.reload.reads).to eq initial_read_count
end
it "#destroy_for decrements the reads count for a post" do
PostTiming.destroy_for(post.user, [post.topic_id])
expect(post.reload.reads).to eq initial_read_count
end
end
describe ".destroy_last_for" do
it "updates first unread for a user correctly when topic is public" do
post.topic.update!(updated_at: 10.minutes.ago)
PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 100]])
PostTiming.destroy_last_for(post.user, topic_id: post.topic_id)
expect(post.user.user_stat.reload.first_unread_at).to eq_time(post.topic.updated_at)
end
it "updates first unread for a user correctly when topic is a pm" do
post = Fabricate(:private_message_post)
post.topic.update!(updated_at: 10.minutes.ago)
PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 100]])
PostTiming.destroy_last_for(post.user, topic_id: post.topic_id)
expect(post.user.user_stat.reload.first_unread_pm_at).to eq_time(post.topic.updated_at)
end
it "updates first unread for a user correctly when topic is a group pm" do
topic = Fabricate(:private_message_topic, updated_at: 10.minutes.ago)
post = Fabricate(:post, topic: topic)
user = Fabricate(:user)
group = Fabricate(:group)
group.add(user)
topic.allowed_groups << group
PostTiming.process_timings(user, topic.id, 1, [[post.post_number, 100]])
PostTiming.destroy_last_for(user, topic_id: topic.id)
expect(GroupUser.find_by(user: user, group: group).first_unread_pm_at).to eq_time(
post.topic.updated_at,
)
end
end
end