discourse/spec/components/topic_view_spec.rb
Sam 94b8ba4f8f FIX: remove slow platform detection from server side
Historically due to https://meta.discourse.org/t/why-is-discourse-so-slow-on-android/8823
we decreased page sizes of both home page and topic page on android by half.

This was done on the server side and as a side effect and caused page sizes on android
to mismatch between Android and non Android.

Unfortunately about a year ago googlebot started pretending it is Android,
this cause Google to start indexing pages as what android would see. So
it saw double the amount of pages in the index as what exists on desktop.
This in turn caused double the amount of indexing work and a large amount
of broken links on long topics.

This fix removes all special behavior which is no longer needed due to
other performance work in Discourse including raw handlebars on home page
and virtual dom on topic pages.

I tested we do not need this on Blu Advance 5.0 it has 1.3 GHZ mediatec mt6580
This phone retails for around $50 USD.

If we decide long term that we want any hacks like this we will shift them
to the client side. It can just hold data in memory without rendering.
2018-12-13 13:57:05 +11:00

668 lines
23 KiB
Ruby

require 'rails_helper'
require 'topic_view'
describe TopicView do
let(:topic) { create_topic }
let(:evil_trout) { Fabricate(:evil_trout) }
let(:first_poster) { topic.user }
let(:topic_view) { TopicView.new(topic.id, evil_trout) }
it "raises a not found error if the topic doesn't exist" do
expect { TopicView.new(1231232, evil_trout) }.to raise_error(Discourse::NotFound)
end
it "accepts a topic or a topic id" do
expect(TopicView.new(topic, evil_trout).topic).to eq(topic)
expect(TopicView.new(topic.id, evil_trout).topic).to eq(topic)
end
# see also spec/controllers/topics_controller_spec.rb TopicsController::show::permission errors
it "raises an error if the user can't see the topic" do
Guardian.any_instance.expects(:can_see?).with(topic).returns(false)
expect { topic_view }.to raise_error(Discourse::InvalidAccess)
end
it "handles deleted topics" do
admin = Fabricate(:admin)
topic.trash!(admin)
expect { TopicView.new(topic.id, Fabricate(:user)) }.to raise_error(Discourse::InvalidAccess)
expect { TopicView.new(topic.id, admin) }.not_to raise_error
end
context "chunk_size" do
it "returns `chunk_size` by default" do
expect(TopicView.new(topic.id, evil_trout).chunk_size).to eq(TopicView.chunk_size)
end
it "returns `print_chunk_size` when print param is true" do
tv = TopicView.new(topic.id, evil_trout, print: true)
expect(tv.chunk_size).to eq(TopicView.print_chunk_size)
end
end
context "with a few sample posts" do
let!(:p1) { Fabricate(:post, topic: topic, user: first_poster, percent_rank: 1) }
let!(:p2) { Fabricate(:post, topic: topic, user: evil_trout, percent_rank: 0.5) }
let!(:p3) { Fabricate(:post, topic: topic, user: first_poster, percent_rank: 0) }
let(:moderator) { Fabricate(:moderator) }
let(:admin) { Fabricate(:admin) }
it "it can find the best responses" do
best2 = TopicView.new(topic.id, evil_trout, best: 2)
expect(best2.posts.count).to eq(2)
expect(best2.posts[0].id).to eq(p2.id)
expect(best2.posts[1].id).to eq(p3.id)
topic.update_status('closed', true, Fabricate(:admin))
expect(topic.posts.count).to eq(4)
# should not get the status post
best = TopicView.new(topic.id, nil, best: 99)
expect(best.posts.count).to eq(2)
expect(best.filtered_post_ids.size).to eq(3)
expect(best.posts.pluck(:id)).to match_array([p2.id, p3.id])
# should get no results for trust level too low
best = TopicView.new(topic.id, nil, best: 99, min_trust_level: evil_trout.trust_level + 1)
expect(best.posts.count).to eq(0)
# should filter out the posts with a score that is too low
best = TopicView.new(topic.id, nil, best: 99, min_score: 99)
expect(best.posts.count).to eq(0)
# should filter out everything if min replies not met
best = TopicView.new(topic.id, nil, best: 99, min_replies: 99)
expect(best.posts.count).to eq(0)
# should punch through posts if the score is high enough
p2.update_column(:score, 100)
best = TopicView.new(topic.id, nil, best: 99, bypass_trust_level_score: 100, min_trust_level: evil_trout.trust_level + 1)
expect(best.posts.count).to eq(1)
# 0 means ignore
best = TopicView.new(topic.id, nil, best: 99, bypass_trust_level_score: 0, min_trust_level: evil_trout.trust_level + 1)
expect(best.posts.count).to eq(0)
# If we restrict to posts a moderator liked, return none
best = TopicView.new(topic.id, nil, best: 99, only_moderator_liked: true)
expect(best.posts.count).to eq(0)
# It doesn't count likes from admins
PostAction.act(admin, p3, PostActionType.types[:like])
best = TopicView.new(topic.id, nil, best: 99, only_moderator_liked: true)
expect(best.posts.count).to eq(0)
# It should find the post liked by the moderator
PostAction.act(moderator, p2, PostActionType.types[:like])
best = TopicView.new(topic.id, nil, best: 99, only_moderator_liked: true)
expect(best.posts.count).to eq(1)
end
it "raises NotLoggedIn if the user isn't logged in and is trying to view a private message" do
Topic.any_instance.expects(:private_message?).returns(true)
expect { TopicView.new(topic.id, nil) }.to raise_error(Discourse::NotLoggedIn)
end
context 'log_check_personal_message is enabled' do
let(:group) { Fabricate(:group) }
let(:private_message) { Fabricate(:private_message_topic, allowed_groups: [group]) }
before do
SiteSetting.log_personal_messages_views = true
evil_trout.admin = true
end
it "logs view if Admin views personal message for other user/group" do
allowed_user = private_message.topic_allowed_users.first.user
TopicView.new(private_message.id, allowed_user)
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(0)
TopicView.new(private_message.id, evil_trout)
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(1)
end
it "does not log personal message view for group he belongs to" do
group.users << evil_trout
TopicView.new(private_message.id, evil_trout)
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(0)
end
it "does not log personal message view for his own personal message" do
private_message.allowed_users << evil_trout
TopicView.new(private_message.id, evil_trout)
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(0)
end
it "does not log personal message view if user can't see the message" do
expect { TopicView.new(private_message.id, Fabricate(:user)) }.to raise_error(Discourse::InvalidAccess)
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(0)
end
it "does not log personal message view if there exists a similar log in previous hour" do
2.times { TopicView.new(private_message.id, evil_trout) }
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(1)
freeze_time (2.hours.from_now)
TopicView.new(private_message.id, evil_trout)
expect(UserHistory.where(action: UserHistory.actions[:check_personal_message]).count).to eq(2)
end
end
it "provides an absolute url" do
expect(topic_view.absolute_url).to eq("http://test.localhost/t/#{topic.slug}/#{topic.id}")
end
context 'subfolder' do
before do
GlobalSetting.stubs(:relative_url_root).returns('/forum')
Discourse.stubs(:base_uri).returns("/forum")
end
it "provides the correct absolute url" do
expect(topic_view.absolute_url).to eq("http://test.localhost/forum/t/#{topic.slug}/#{topic.id}")
end
end
it "provides a summary of the first post" do
expect(topic_view.summary).to be_present
end
describe "#get_canonical_path" do
let(:user) { Fabricate(:user) }
let(:topic) { Fabricate(:topic) }
let(:path) { "/1234" }
before do
topic.stubs(:relative_url).returns(path)
TopicView.any_instance.stubs(:find_topic).with(1234).returns(topic)
end
it "generates canonical path correctly" do
expect(TopicView.new(1234, user).canonical_path).to eql(path)
expect(TopicView.new(1234, user, page: 5).canonical_path).to eql("/1234?page=5")
end
it "generates a canonical correctly for paged results" do
expect(TopicView.new(1234, user, post_number: 10 * TopicView.chunk_size)
.canonical_path).to eql("/1234?page=10")
end
end
describe "#next_page" do
let(:p2) { stub(post_number: 2) }
let(:topic) do
topic = create_topic
topic.stubs(:highest_post_number).returns(5)
topic
end
let(:user) { Fabricate(:user) }
before do
TopicView.any_instance.expects(:find_topic).with(1234).returns(topic)
TopicView.any_instance.stubs(:filter_posts)
TopicView.any_instance.stubs(:last_post).returns(p2)
TopicView.stubs(:chunk_size).returns(2)
end
it "should return the next page" do
expect(TopicView.new(1234, user).next_page).to eql(2)
end
end
context '.post_counts_by_user' do
it 'returns the two posters with their appropriate counts' do
Fabricate(:post, topic: topic, user: evil_trout, post_type: Post.types[:whisper])
expect(topic_view.post_counts_by_user.to_a).to match_array([[first_poster.id, 2], [evil_trout.id, 2]])
expect(TopicView.new(topic.id, first_poster).post_counts_by_user.to_a).to match_array([[first_poster.id, 2], [evil_trout.id, 1]])
end
it "doesn't return counts for posts with authors who have been deleted" do
p2.user_id = nil
p2.save!
expect(topic_view.post_counts_by_user.to_a).to match_array([[first_poster.id, 2]])
end
end
context '.participants' do
it 'returns the two participants hashed by id' do
expect(topic_view.participants.to_a).to match_array([[first_poster.id, first_poster], [evil_trout.id, evil_trout]])
end
end
context '.all_post_actions' do
it 'is blank at first' do
expect(topic_view.all_post_actions).to be_blank
end
it 'returns the like' do
PostAction.act(evil_trout, p1, PostActionType.types[:like])
expect(topic_view.all_post_actions[p1.id][PostActionType.types[:like]]).to be_present
end
end
context '.all_active_flags' do
it 'is blank at first' do
expect(topic_view.all_active_flags).to be_blank
end
it 'returns the active flags' do
PostAction.act(moderator, p1, PostActionType.types[:off_topic])
PostAction.act(evil_trout, p1, PostActionType.types[:off_topic])
expect(topic_view.all_active_flags[p1.id][PostActionType.types[:off_topic]].count).to eq(2)
end
it 'returns only the active flags' do
PostAction.act(moderator, p1, PostActionType.types[:off_topic])
PostAction.act(evil_trout, p1, PostActionType.types[:off_topic])
PostAction.defer_flags!(p1, moderator)
expect(topic_view.all_active_flags[p1.id]).to eq(nil)
end
end
context '.read?' do
it 'tracks correctly' do
# anon is assumed to have read everything
expect(TopicView.new(topic.id).read?(1)).to eq(true)
# random user has nothing
expect(topic_view.read?(1)).to eq(false)
evil_trout.created_at = 2.days.ago
# a real user that just read it should have it marked
PostTiming.process_timings(evil_trout, topic.id, 1, [[1, 1000]])
expect(TopicView.new(topic.id, evil_trout).read?(1)).to eq(true)
expect(TopicView.new(topic.id, evil_trout).topic_user).to be_present
end
end
context '.topic_user' do
it 'returns nil when there is no user' do
expect(TopicView.new(topic.id, nil).topic_user).to be_blank
end
end
context '#recent_posts' do
before do
24.times do |t| # our let()s have already created 3
Fabricate(:post, topic: topic, user: first_poster, created_at: t.seconds.from_now)
end
end
it 'returns at most 25 recent posts ordered newest first' do
recent_posts = topic_view.recent_posts
# count
expect(recent_posts.count).to eq(25)
# ordering
expect(recent_posts.include?(p1)).to eq(false)
expect(recent_posts.include?(p3)).to eq(true)
expect(recent_posts.first.created_at).to be > recent_posts.last.created_at
end
end
end
context 'whispers' do
it "handles their visibility properly" do
p1 = Fabricate(:post, topic: topic, user: evil_trout)
p2 = Fabricate(:post, topic: topic, user: evil_trout, post_type: Post.types[:whisper])
p3 = Fabricate(:post, topic: topic, user: evil_trout)
ch_posts = TopicView.new(topic.id, evil_trout).posts
expect(ch_posts.map(&:id)).to eq([p1.id, p2.id, p3.id])
anon_posts = TopicView.new(topic.id).posts
expect(anon_posts.map(&:id)).to eq([p1.id, p3.id])
admin_posts = TopicView.new(topic.id, Fabricate(:moderator)).posts
expect(admin_posts.map(&:id)).to eq([p1.id, p2.id, p3.id])
end
end
context '#posts' do
# Create the posts in a different order than the sort_order
let!(:p5) { Fabricate(:post, topic: topic, user: evil_trout) }
let!(:p2) { Fabricate(:post, topic: topic, user: evil_trout) }
let!(:p6) { Fabricate(:post, topic: topic, user: Fabricate(:user), deleted_at: Time.now) }
let!(:p4) { Fabricate(:post, topic: topic, user: evil_trout, deleted_at: Time.now) }
let!(:p1) { Fabricate(:post, topic: topic, user: first_poster) }
let!(:p7) { Fabricate(:post, topic: topic, user: evil_trout, deleted_at: Time.now) }
let!(:p3) { Fabricate(:post, topic: topic, user: first_poster) }
before do
TopicView.stubs(:chunk_size).returns(3)
# Update them to the sort order we're checking for
[p1, p2, p3, p4, p5, p6, p7].each_with_index do |p, idx|
p.sort_order = idx + 1
p.save
end
p6.user_id = nil # user got nuked
p6.save!
end
describe "contains_gaps?" do
it "works" do
# does not contain contains_gaps with default filtering
expect(topic_view.contains_gaps?).to eq(false)
# contains contains_gaps when filtered by username" do
expect(TopicView.new(topic.id, evil_trout, username_filters: ['eviltrout']).contains_gaps?).to eq(true)
# contains contains_gaps when filtered by summary
expect(TopicView.new(topic.id, evil_trout, filter: 'summary').contains_gaps?).to eq(true)
# contains contains_gaps when filtered by best
expect(TopicView.new(topic.id, evil_trout, best: 5).contains_gaps?).to eq(true)
end
end
it "#restricts to correct topic" do
t2 = Fabricate(:topic)
category = Fabricate(:category, name: "my test")
category.set_permissions(Group[:admins] => :full)
category.save
topic.category_id = category.id
topic.save!
expect {
TopicView.new(topic.id, evil_trout).posts.count
}.to raise_error(Discourse::InvalidAccess)
expect(TopicView.new(t2.id, evil_trout, post_ids: [p1.id, p2.id]).posts.count).to eq(0)
end
describe '#filter_posts_paged' do
before { TopicView.stubs(:chunk_size).returns(2) }
it 'returns correct posts for all pages' do
expect(topic_view.filter_posts_paged(1)).to eq([p1, p2])
expect(topic_view.filter_posts_paged(2)).to eq([p3, p5])
expect(topic_view.filter_posts_paged(3)).to eq([])
expect(topic_view.filter_posts_paged(100)).to eq([])
end
end
describe '#filter_posts_by_post_number' do
def create_topic_view(post_number)
TopicView.new(
topic.id,
evil_trout,
filter_post_number: post_number,
asc: asc
)
end
describe 'ascending' do
let(:asc) { true }
it 'should return the right posts' do
topic_view = create_topic_view(p3.post_number)
expect(topic_view.posts).to eq([p5])
topic_view = create_topic_view(p6.post_number)
expect(topic_view.posts).to eq([])
end
end
describe 'descending' do
let(:asc) { false }
it 'should return the right posts' do
topic_view = create_topic_view(p7.post_number)
expect(topic_view.posts).to eq([p5, p3, p2])
topic_view = create_topic_view(p2.post_number)
expect(topic_view.posts).to eq([p1])
end
end
end
describe "filter_posts_near" do
def topic_view_near(post, show_deleted = false)
TopicView.new(topic.id, evil_trout, post_number: post.post_number, show_deleted: show_deleted)
end
it "snaps to the lower boundary" do
near_view = topic_view_near(p1)
expect(near_view.desired_post).to eq(p1)
expect(near_view.posts).to eq([p1, p2, p3])
expect(near_view.contains_gaps?).to eq(false)
end
it "snaps to the upper boundary" do
near_view = topic_view_near(p5)
expect(near_view.desired_post).to eq(p5)
expect(near_view.posts).to eq([p2, p3, p5])
expect(near_view.contains_gaps?).to eq(false)
end
it "returns the posts in the middle" do
near_view = topic_view_near(p2)
expect(near_view.desired_post).to eq(p2)
expect(near_view.posts).to eq([p1, p2, p3])
expect(near_view.contains_gaps?).to eq(false)
end
describe 'when post_number is too large' do
it "snaps to the lower boundary" do
near_view = TopicView.new(topic.id, evil_trout,
post_number: 99999999,
)
expect(near_view.desired_post).to eq(p2)
expect(near_view.posts).to eq([p2, p3, p5])
expect(near_view.contains_gaps?).to eq(false)
end
end
it "gaps deleted posts to an admin" do
evil_trout.admin = true
near_view = topic_view_near(p3)
expect(near_view.desired_post).to eq(p3)
expect(near_view.posts).to eq([p2, p3, p5])
expect(near_view.gaps.before).to eq(p5.id => [p4.id])
expect(near_view.gaps.after).to eq(p5.id => [p6.id, p7.id])
end
it "returns deleted posts to an admin with show_deleted" do
evil_trout.admin = true
near_view = topic_view_near(p3, true)
expect(near_view.desired_post).to eq(p3)
expect(near_view.posts).to eq([p2, p3, p4])
expect(near_view.contains_gaps?).to eq(false)
end
it "gaps deleted posts by nuked users to an admin" do
evil_trout.admin = true
near_view = topic_view_near(p5)
expect(near_view.desired_post).to eq(p5)
# note: both p4 and p6 get skipped
expect(near_view.posts).to eq([p2, p3, p5])
expect(near_view.gaps.before).to eq(p5.id => [p4.id])
expect(near_view.gaps.after).to eq(p5.id => [p6.id, p7.id])
end
it "returns deleted posts by nuked users to an admin with show_deleted" do
evil_trout.admin = true
near_view = topic_view_near(p5, true)
expect(near_view.desired_post).to eq(p5)
expect(near_view.posts).to eq([p4, p5, p6])
expect(near_view.contains_gaps?).to eq(false)
end
context "when 'posts per page' exceeds the number of posts" do
before { TopicView.stubs(:chunk_size).returns(100) }
it 'returns all the posts' do
near_view = topic_view_near(p5)
expect(near_view.posts).to eq([p1, p2, p3, p5])
expect(near_view.contains_gaps?).to eq(false)
end
it 'gaps deleted posts to admins' do
evil_trout.admin = true
near_view = topic_view_near(p5)
expect(near_view.posts).to eq([p1, p2, p3, p5])
expect(near_view.gaps.before).to eq(p5.id => [p4.id])
expect(near_view.gaps.after).to eq(p5.id => [p6.id, p7.id])
end
it 'returns deleted posts to admins' do
evil_trout.admin = true
near_view = topic_view_near(p5, true)
expect(near_view.posts).to eq([p1, p2, p3, p4, p5, p6, p7])
expect(near_view.contains_gaps?).to eq(false)
end
end
end
end
context "page_title" do
let(:tag1) { Fabricate(:tag) }
let(:tag2) { Fabricate(:tag, topic_count: 2) }
subject { TopicView.new(topic.id, evil_trout).page_title }
context "uncategorized topic" do
context "topic_page_title_includes_category is false" do
before { SiteSetting.topic_page_title_includes_category = false }
it { should eq(topic.title) }
end
context "topic_page_title_includes_category is true" do
before { SiteSetting.topic_page_title_includes_category = true }
it { should eq(topic.title) }
context "tagged topic" do
before { topic.tags << [tag1, tag2] }
context "tagging enabled" do
before { SiteSetting.tagging_enabled = true }
it { should start_with(topic.title) }
it { should_not include(tag1.name) }
it { should end_with(tag2.name) } # tag2 has higher topic count
end
context "tagging disabled" do
before { SiteSetting.tagging_enabled = false }
it { should start_with(topic.title) }
it { should_not include(tag1.name) }
it { should_not include(tag2.name) }
end
end
end
end
context "categorized topic" do
let(:category) { Fabricate(:category) }
before { topic.update_attributes(category_id: category.id) }
context "topic_page_title_includes_category is false" do
before { SiteSetting.topic_page_title_includes_category = false }
it { should eq(topic.title) }
end
context "topic_page_title_includes_category is true" do
before { SiteSetting.topic_page_title_includes_category = true }
it { should start_with(topic.title) }
it { should end_with(category.name) }
context "tagged topic" do
before do
SiteSetting.tagging_enabled = true
topic.tags << [tag1, tag2]
end
it { should start_with(topic.title) }
it { should end_with(category.name) }
it { should_not include(tag1.name) }
it { should_not include(tag2.name) }
end
end
end
end
describe '#filtered_post_stream' do
let!(:post) { Fabricate(:post, topic: topic, user: first_poster) }
let!(:post2) { Fabricate(:post, topic: topic, user: evil_trout) }
let!(:post3) { Fabricate(:post, topic: topic, user: first_poster) }
it 'should return the right columns' do
expect(topic_view.filtered_post_stream).to eq([
[post.id, 0],
[post2.id, 0],
[post3.id, 0]
])
end
describe 'for mega topics' do
it 'should return the right columns' do
begin
original_const = TopicView::MEGA_TOPIC_POSTS_COUNT
TopicView.send(:remove_const, "MEGA_TOPIC_POSTS_COUNT")
TopicView.const_set("MEGA_TOPIC_POSTS_COUNT", 2)
expect(topic_view.filtered_post_stream).to eq([
post.id,
post2.id,
post3.id
])
ensure
TopicView.send(:remove_const, "MEGA_TOPIC_POSTS_COUNT")
TopicView.const_set("MEGA_TOPIC_POSTS_COUNT", original_const)
end
end
end
end
describe '#filtered_post_id' do
it 'should return the right id' do
post = Fabricate(:post, topic: topic)
expect(topic_view.filtered_post_id(nil)).to eq(nil)
expect(topic_view.filtered_post_id(post.post_number)).to eq(post.id)
end
end
describe '#first_post_id and #last_post_id' do
let!(:p3) { Fabricate(:post, topic: topic) }
let!(:p2) { Fabricate(:post, topic: topic) }
let!(:p1) { Fabricate(:post, topic: topic) }
before do
[p1, p2, p3].each_with_index do |post, index|
post.update!(sort_order: index + 1)
end
end
it 'should return the right id' do
expect(topic_view.first_post_id).to eq(p1.id)
expect(topic_view.last_post_id).to eq(p3.id)
end
end
end