discourse/spec/lib/topic_view_spec.rb
Krzysztof Kotlarek 09932738e5
FEATURE: whispers available for groups (#17170)
Before, whispers were only available for staff members.

Config has been changed to allow to configure privileged groups with access to whispers. Post migration was added to move from the old setting into the new one.

I considered having a boolean column `whisperer` on user model similar to `admin/moderator` for performance reason. Finally, I decided to keep looking for groups as queries are only done for current user and didn't notice any N+1 queries.
2022-06-30 10:18:12 +10:00

1035 lines
37 KiB
Ruby

# frozen_string_literal: true
require 'topic_view'
RSpec.describe TopicView do
fab!(:user) { Fabricate(:user) }
fab!(:moderator) { Fabricate(:moderator) }
fab!(:admin) { Fabricate(:admin) }
fab!(:topic) { Fabricate(:topic) }
fab!(:evil_trout) { Fabricate(:evil_trout) }
fab!(:first_poster) { topic.user }
fab!(:anonymous) { Fabricate(:anonymous) }
let(:topic_view) { TopicView.new(topic.id, evil_trout) }
context "preload" do
it "allows preloading of data" do
preloaded_topic_view = nil
preloader = lambda do |view|
preloaded_topic_view = view
end
TopicView.on_preload(&preloader)
expect(preloaded_topic_view).to eq(nil)
topic_view
expect(preloaded_topic_view).to eq(topic_view)
TopicView.cancel_preload(&preloader)
end
end
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
topic.trash!(admin)
expect { TopicView.new(topic.id, user) }.to raise_error(Discourse::InvalidAccess)
expect { TopicView.new(topic.id, admin) }.not_to raise_error
end
context "filter options" do
fab!(:p0) { Fabricate(:post, topic: topic) }
fab!(:p1) { Fabricate(:post, topic: topic, post_type: Post.types[:moderator_action]) }
fab!(:p2) { Fabricate(:post, topic: topic, post_type: Post.types[:small_action]) }
it "omits moderator actions and small posts when only_regular is set" do
tv = TopicView.new(topic.id, nil)
expect(tv.filtered_post_ids).to eq([p0.id, p1.id, p2.id])
tv = TopicView.new(topic.id, nil, only_regular: true)
expect(tv.filtered_post_ids).to eq([p0.id])
end
it "omits the first post when exclude_first is set" do
tv = TopicView.new(topic.id, nil, exclude_first: true)
expect(tv.filtered_post_ids).to eq([p0.id, p1.id, p2.id])
end
end
context 'custom filters' do
fab!(:p0) { Fabricate(:post, topic: topic) }
fab!(:p1) { Fabricate(:post, topic: topic, wiki: true) }
it 'allows to register custom filters' do
tv = TopicView.new(topic.id, evil_trout, { filter: 'wiki' })
expect(tv.filter_posts({ filter: "wiki" })).to eq([p0, p1])
TopicView.add_custom_filter("wiki") do |posts, topic_view|
posts.where(wiki: true)
end
tv = TopicView.new(topic.id, evil_trout, { filter: 'wiki' })
expect(tv.filter_posts).to eq([p1])
tv = TopicView.new(topic.id, evil_trout, { filter: 'whatever' })
expect(tv.filter_posts).to eq([p0, p1])
ensure
TopicView.instance_variable_set(:@custom_filters, {})
end
end
context "setup_filtered_posts" do
describe "filters posts with ignored users" do
fab!(:ignored_user) { Fabricate(:ignored_user, user: evil_trout, ignored_user: user) }
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: user) }
it "filters out ignored user posts" do
tv = TopicView.new(topic.id, evil_trout)
expect(tv.filtered_post_ids).to eq([post.id, post2.id])
end
it "returns nil for next_page" do
tv = TopicView.new(topic.id, evil_trout)
expect(tv.next_page).to eq(nil)
end
describe "when an ignored user made the original post" do
let!(:post) { Fabricate(:post, topic: topic, user: user) }
it "filters out ignored user posts only" do
tv = TopicView.new(topic.id, evil_trout)
expect(tv.filtered_post_ids).to eq([post.id, post2.id])
end
end
describe "when an anonymous user made a post" do
let!(:post4) { Fabricate(:post, topic: topic, user: anonymous) }
it "filters out ignored user posts only" do
tv = TopicView.new(topic.id, evil_trout)
expect(tv.filtered_post_ids).to eq([post.id, post2.id, post4.id])
end
end
describe "when an anonymous (non signed-in) user is viewing a Topic" do
let!(:post4) { Fabricate(:post, topic: topic, user: anonymous) }
it "filters out ignored user posts only" do
tv = TopicView.new(topic.id, nil)
expect(tv.filtered_post_ids).to eq([post.id, post2.id, post3.id, post4.id])
end
end
describe "when a staff user is ignored" do
let!(:admin) { Fabricate(:user, admin: true) }
let!(:admin_ignored_user) { Fabricate(:ignored_user, user: evil_trout, ignored_user: admin) }
let!(:post4) { Fabricate(:post, topic: topic, user: admin) }
it "filters out ignored user excluding the staff user" do
tv = TopicView.new(topic.id, evil_trout)
expect(tv.filtered_post_ids).to eq([post.id, post2.id, post4.id])
end
end
end
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
fab!(:p1) { Fabricate(:post, topic: topic, user: first_poster, percent_rank: 1) }
fab!(:p2) { Fabricate(:post, topic: topic, user: evil_trout, percent_rank: 0.5) }
fab!(:p3) { Fabricate(:post, topic: topic, user: first_poster, percent_rank: 0) }
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, 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
PostActionCreator.like(admin, p3)
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
PostActionCreator.like(moderator, p2)
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
fab!(:group) { Fabricate(:group) }
fab!(: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, 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
it "provides the correct absolute url" do
set_subfolder "/forum"
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
fab!(: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
5.times { |i| Fabricate(:post, post_number: i + 1, topic: topic) }
expect(TopicView.new(1234, user, post_number: 5, limit: 2)
.canonical_path).to eql("/1234?page=3")
end
it "generates canonical path correctly by skipping whisper posts" do
2.times { |i| Fabricate(:post, post_number: i + 1, topic: topic) }
2.times { |i| Fabricate(:whisper, post_number: i + 3, topic: topic) }
Fabricate(:post, post_number: 5, topic: topic)
expect(TopicView.new(1234, user, post_number: 5, limit: 2)
.canonical_path).to eql("/1234?page=2")
end
it "generates canonical path correctly for mega topics" do
2.times { |i| Fabricate(:post, post_number: i + 1, topic: topic) }
2.times { |i| Fabricate(:whisper, post_number: i + 3, topic: topic) }
Fabricate(:post, post_number: 5, topic: topic)
expect(TopicView.new(1234, user, post_number: 5, limit: 2, is_mega_topic: true)
.canonical_path).to eql("/1234?page=3")
end
end
describe "#next_page" do
let!(:post) { Fabricate(:post, topic: topic, user: user) }
let!(:post2) { Fabricate(:post, topic: topic, user: user) }
let!(:post3) { Fabricate(:post, topic: topic, user: user) }
let!(:post4) { Fabricate(:post, topic: topic, user: user) }
let!(:post5) { Fabricate(:post, topic: topic, user: user) }
before do
TopicView.stubs(:chunk_size).returns(2)
end
it "should return the next page" do
expect(TopicView.new(topic.id, user, { post_number: post.post_number }).next_page).to eql(3)
end
end
context '.post_counts_by_user' do
it 'returns the two posters with their appropriate counts' do
SiteSetting.enable_whispers = true
Fabricate(:post, topic: topic, user: evil_trout, post_type: Post.types[:whisper])
# Should not be counted
Fabricate(:post, topic: topic, user: evil_trout, post_type: Post.types[:whisper], action_code: 'assign')
expect(TopicView.new(topic.id, admin).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
PostActionCreator.like(evil_trout, p1)
expect(topic_view.all_post_actions[p1.id][PostActionType.types[:like]]).to be_present
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 "#bookmarks" do
let!(:user) { Fabricate(:user) }
let!(:bookmark1) { Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user) }
let!(:bookmark2) { Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user) }
let!(:bookmark3) { Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic)) }
it "returns all the bookmarks in the topic for a user" do
expect(TopicView.new(topic.id, user).bookmarks.pluck(:id)).to match_array(
[bookmark1.id, bookmark2.id]
)
end
it "returns [] for anon users" do
expect(TopicView.new(topic.id, nil).bookmarks.pluck(:id)).to eq([])
end
end
context "#bookmarks" do
let!(:user) { Fabricate(:user) }
let!(:bookmark1) { Fabricate(:bookmark_next_business_day_reminder, bookmarkable: topic.first_post, user: user) }
let!(:bookmark2) { Fabricate(:bookmark_next_business_day_reminder, bookmarkable: topic.posts[1], user: user) }
it "gets the first post bookmark reminder at for the user" do
topic_view = TopicView.new(topic.id, user)
first, second = topic_view.bookmarks.sort_by(&:id)
expect(first[:bookmarkable_id]).to eq(bookmark1.bookmarkable_id)
expect(first[:reminder_at]).to eq_time(bookmark1.reminder_at)
expect(second[:bookmarkable_id]).to eq(bookmark2.bookmarkable_id)
expect(second[:reminder_at]).to eq_time(bookmark2.reminder_at)
end
context "when the topic is deleted" do
it "returns []" do
topic_view = TopicView.new(topic, user)
expect(topic_view.bookmarks).to match_array([bookmark1, bookmark2])
PostDestroyer.new(Fabricate(:admin), topic.first_post).destroy
topic.reload
topic_view.instance_variable_set(:@bookmarks, nil)
expect(topic_view.bookmarks).to eq([])
end
end
context "when one of the posts is deleted" do
it "does not return that post's bookmark" do
topic_view = TopicView.new(topic, user)
PostDestroyer.new(Fabricate(:admin), topic.posts.second).destroy
topic.reload
expect(topic_view.bookmarks.length).to eq(1)
first = topic_view.bookmarks.first
expect(first[:bookmarkable_id]).to eq(bookmark1.bookmarkable_id)
expect(first[:reminder_at]).to eq_time(bookmark1.reminder_at)
end
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
SiteSetting.enable_whispers = true
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, 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: 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
fab!(:tag1) { Fabricate(:tag) }
fab!(:tag2) { Fabricate(:tag, topic_count: 2) }
fab!(:op_post) { Fabricate(:post, topic: topic) }
fab!(:post1) { Fabricate(:post, topic: topic) }
fab!(:whisper) { Fabricate(:post, topic: topic, post_type: Post.types[:whisper]) }
subject { TopicView.new(topic.id, evil_trout).page_title }
context "when a post number is specified" do
context "admins" do
it "see post number and username for all posts" do
title = TopicView.new(topic.id, admin, post_number: 0).page_title
expect(title).to eq(topic.title)
title = TopicView.new(topic.id, admin, post_number: 1).page_title
expect(title).to eq(topic.title)
title = TopicView.new(topic.id, admin, post_number: 2).page_title
expect(title).to eq("#{topic.title} - #2 by #{post1.user.username}")
title = TopicView.new(topic.id, admin, post_number: 3).page_title
expect(title).to eq("#{topic.title} - #3 by #{whisper.user.username}")
end
end
context "regular users" do
it "see post number and username for regular posts" do
title = TopicView.new(topic.id, evil_trout, post_number: 0).page_title
expect(title).to eq(topic.title)
title = TopicView.new(topic.id, evil_trout, post_number: 1).page_title
expect(title).to eq(topic.title)
title = TopicView.new(topic.id, evil_trout, post_number: 2).page_title
expect(title).to eq("#{topic.title} - #2 by #{post1.user.username}")
end
it "see only post number for whisper posts" do
title = TopicView.new(topic.id, evil_trout, post_number: 3).page_title
expect(title).to eq("#{topic.title} - #3")
post2 = Fabricate(:post, topic: topic)
topic.reload
title = TopicView.new(topic.id, evil_trout, post_number: 3).page_title
expect(title).to eq("#{topic.title} - #3")
title = TopicView.new(topic.id, evil_trout, post_number: 4).page_title
expect(title).to eq("#{topic.title} - #4 by #{post2.user.username}")
end
end
end
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(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, created_at: 18.hours.ago) }
let!(:post2) { Fabricate(:post, topic: topic, user: evil_trout, created_at: 6.hours.ago) }
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, 1],
[post2.id, 0],
[post3.id, 0]
])
end
describe 'for mega topics' do
it 'should return the right columns' do
stub_const(TopicView, "MEGA_TOPIC_POSTS_COUNT", 2) do
expect(topic_view.filtered_post_stream).to eq([
post.id,
post2.id,
post3.id
])
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 '#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.last_post_id).to eq(p3.id)
end
end
describe '#read_time' do
let!(:post) { Fabricate(:post, topic: topic) }
before do
PostCreator.create!(Discourse.system_user, topic_id: topic.id, raw: "![image|100x100](upload://upload.png)")
topic_view.topic.reload
end
it 'should return the right read time' do
SiteSetting.read_time_word_count = 500
expect(topic_view.read_time).to eq(1)
SiteSetting.read_time_word_count = 0
expect(topic_view.read_time).to eq(nil)
end
end
describe '#image_url' do
fab!(:op_upload) { Fabricate(:image_upload) }
fab!(:post3_upload) { Fabricate(:image_upload) }
fab!(:post1) { Fabricate(:post, topic: topic) }
fab!(:post2) { Fabricate(:post, topic: topic) }
fab!(:post3) { Fabricate(:post, topic: topic).tap { |p| p.update_column(:image_upload_id, post3_upload.id) }.reload }
def topic_view_for_post(post_number)
TopicView.new(topic.id, evil_trout, post_number: post_number)
end
context "when op has an image" do
before do
topic.update_column(:image_upload_id, op_upload.id)
post1.update_column(:image_upload_id, op_upload.id)
end
it "uses the topic image as a fallback when posts have no image" do
expect(topic_view_for_post(1).image_url).to end_with(op_upload.url)
expect(topic_view_for_post(2).image_url).to end_with(op_upload.url)
expect(topic_view_for_post(3).image_url).to end_with(post3_upload.url)
end
end
context "when op has no image" do
it "returns nil when posts have no image" do
expect(topic_view_for_post(1).image_url).to eq(nil)
expect(topic_view_for_post(2).image_url).to eq(nil)
expect(topic_view_for_post(3).image_url).to end_with(post3_upload.url)
end
end
end
describe '#show_read_indicator?' do
let(:topic) { Fabricate(:topic) }
let(:pm_topic) { Fabricate(:private_message_topic) }
it "shows read indicator for private messages" do
group = Fabricate(:group, users: [admin], publish_read_state: true)
pm_topic.topic_allowed_groups = [Fabricate.build(:topic_allowed_group, group: group)]
topic_view = TopicView.new(pm_topic.id, admin)
expect(topic_view.show_read_indicator?).to be_truthy
end
it "does not show read indicator if groups do not have read indicator enabled" do
topic_view = TopicView.new(pm_topic.id, admin)
expect(topic_view.show_read_indicator?).to be_falsey
end
it "does not show read indicator for topics with allowed groups" do
group = Fabricate(:group, users: [admin], publish_read_state: true)
topic.topic_allowed_groups = [Fabricate.build(:topic_allowed_group, group: group)]
topic_view = TopicView.new(topic.id, admin)
expect(topic_view.show_read_indicator?).to be_falsey
end
end
describe '#reviewable_counts' do
it 'exclude posts queued because the category needs approval' do
category = Fabricate.build(:category, user: admin)
category.custom_fields[Category::REQUIRE_TOPIC_APPROVAL] = true
category.save!
manager = NewPostManager.new(
user,
raw: 'to the handler I say enqueue me!',
title: 'this is the title of the queued post',
category: category.id
)
result = manager.perform
reviewable = result.reviewable
reviewable.perform(admin, :approve_post)
topic_view = TopicView.new(reviewable.topic, admin)
expect(topic_view.reviewable_counts).to be_empty
end
it 'include posts queued for other reasons' do
Fabricate(:watched_word, word: "darn", action: WatchedWord.actions[:require_approval])
manager = NewPostManager.new(
user,
raw: 'this is darn new post content',
title: 'this is the title of the queued post'
)
result = manager.perform
reviewable = result.reviewable
reviewable.perform(admin, :approve_post)
topic_view = TopicView.new(reviewable.topic, admin)
expect(topic_view.reviewable_counts.keys).to contain_exactly(reviewable.target_id)
end
end
describe '.apply_custom_default_scope' do
fab!(:post) { Fabricate(:post, topic: topic, created_at: 2.hours.ago) }
fab!(:post_2) { Fabricate(:post, topic: topic, created_at: 1.hour.ago) }
after do
TopicView.reset_custom_default_scopes
end
it 'allows a custom default scope to be configured' do
topic_view = TopicView.new(topic, admin)
expect(topic_view.filtered_post_ids).to eq([post.id, post_2.id])
TopicView.apply_custom_default_scope do |scope, _|
scope.unscope(:order).order("posts.created_at DESC")
end
topic_view = TopicView.new(topic, admin)
expect(topic_view.filtered_post_ids).to eq([post_2.id, post.id])
end
end
describe "#queued_posts_enabled?" do
subject(:topic_view) { described_class.new(topic, user) }
let(:topic) { Fabricate.build(:topic) }
let(:user) { Fabricate.build(:user, id: 1) }
let(:category) { topic.category }
before do
NewPostManager.stubs(:queue_enabled?).returns(queue_enabled)
end
context "when queue is enabled globally" do
let(:queue_enabled) { true }
it { is_expected.to be_queued_posts_enabled }
end
context "when queue is not enabled globally" do
let(:queue_enabled) { false }
context "when category is moderated" do
before do
category.custom_fields[Category::REQUIRE_REPLY_APPROVAL] = true
end
it { is_expected.to be_queued_posts_enabled }
end
context "when category is not moderated" do
it { is_expected.not_to be_queued_posts_enabled }
end
end
end
end