discourse/spec/models/post_timing_spec.rb
Guo Xiang Tan 9b75d95fc6 PERF: Keep track of first unread PM and first unread group PM for user.
This optimization helps to filter away topics so that the joins on
related tables when querying for unread messages is not expensive.
2020-09-09 14:05:41 +08:00

225 lines
7.4 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
describe PostTiming do
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, highest_seen_post_number)
TopicUser.create!(
topic_id: topic_id,
user_id: user_id,
last_read_post_number: last_read_post_number,
highest_seen_post_number: highest_seen_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, 1)
_tu_two = topic_user(2, 2, 2)
_tu_three = topic_user(3, 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)
expect(tu.highest_seen_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)
expect(tu.highest_seen_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)
expect(tu.highest_seen_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)
post = Fabricate(:post)
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
post = Fabricate(:post)
(2..5).each do |i|
Fabricate(:post, topic: post.topic, post_number: i)
end
user2 = Fabricate(:coding_horror, created_at: 1.day.ago)
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 do |i|
Fabricate(:post, topic: pm, user: user1)
end
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
@post = Fabricate(:post)
@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
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 do
PostTiming.process_timings(post.user, post.topic_id, 1, [[post.post_number, 100]])
end
it '#destroy_last_for decrements the reads count for a post' do
PostTiming.destroy_last_for(post.user, 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 = Fabricate(: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, 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, 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)
expect(GroupUser.find_by(user: user, group: group).first_unread_pm_at)
.to eq_time(post.topic.updated_at)
end
end
end