discourse/spec/lib/topic_view_spec.rb
Alan Guo Xiang Tan f122f24b35
SECURITY: Default tags to show count of topics in unrestricted categories (#19916)
Currently, `Tag#topic_count` is a count of all regular topics regardless of whether the topic is in a read restricted category or not. As a result, any users can technically poll a sensitive tag to determine if a new topic is created in a category which the user has not excess to. We classify this as a minor leak in sensitive information.

The following changes are introduced in this commit:

1. Introduce `Tag#public_topic_count` which only count topics which have been tagged with a given tag in public categories.
2. Rename `Tag#topic_count` to `Tag#staff_topic_count` which counts the same way as `Tag#topic_count`. In other words, it counts all topics tagged with a given tag regardless of the category the topic is in. The rename is also done so that we indicate that this column contains sensitive information. 
3. Change all previous spots which relied on `Topic#topic_count` to rely on `Tag.topic_column_count(guardian)` which will return the right "topic count" column to use based on the current scope. 
4. Introduce `SiteSetting.include_secure_categories_in_tag_counts` site setting to allow site administrators to always display the tag topics count using `Tag#staff_topic_count` instead.
2023-01-20 09:50:24 +08:00

1089 lines
38 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) }
describe "preload" do
it "allows preloading of data" do
preloaded_topic_view = nil
preloader = lambda { |view| preloaded_topic_view = view }
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(1_231_232, 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
describe "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
describe "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") { |posts, topic_view| posts.where(wiki: true) }
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
describe "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
context "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
context "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
context "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
context "when a staff user is ignored" do
let!(:admin) { Fabricate(:user, admin: true) }
let!(:admin_ignored_user) do
Fabricate(:ignored_user, user: evil_trout, ignored_user: admin)
end
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
describe "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 "when 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 "with 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 { TopicView.stubs(:chunk_size).returns(2) }
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
describe ".post_counts_by_user" do
it "returns the two posters with their appropriate counts" do
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
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
describe ".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
describe ".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
describe ".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
describe "#bookmarks" do
let!(:user) { Fabricate(:user) }
let!(:bookmark1) do
Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user)
end
let!(:bookmark2) do
Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user)
end
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
describe "#bookmarks" do
let!(:user) { Fabricate(:user) }
let!(:bookmark1) do
Fabricate(:bookmark_next_business_day_reminder, bookmarkable: topic.first_post, user: user)
end
let!(:bookmark2) do
Fabricate(
:bookmark_next_business_day_reminder,
bookmarkable: topic.posts.order(:post_number)[1],
user: user,
)
end
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
describe ".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
describe "#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
describe "whispers" do
it "handles their visibility properly" do
SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}"
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
describe "#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: 99_999_999)
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
describe "page_title" do
fab!(:tag1) { Fabricate(:tag, staff_topic_count: 0, public_topic_count: 0) }
fab!(:tag2) { Fabricate(:tag, staff_topic_count: 2, public_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 "with 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 "with 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 "with uncategorized topic" do
context "when topic_page_title_includes_category is false" do
before { SiteSetting.topic_page_title_includes_category = false }
it { is_expected.to eq(topic.title) }
end
context "when topic_page_title_includes_category is true" do
before { SiteSetting.topic_page_title_includes_category = true }
it { is_expected.to eq(topic.title) }
context "with tagged topic" do
before { topic.tags << [tag1, tag2] }
context "with tagging enabled" do
before { SiteSetting.tagging_enabled = true }
it { is_expected.to start_with(topic.title) }
it { is_expected.not_to include(tag1.name) }
it { is_expected.to end_with(tag2.name) } # tag2 has higher topic count
end
context "with tagging disabled" do
before { SiteSetting.tagging_enabled = false }
it { is_expected.to start_with(topic.title) }
it { is_expected.not_to include(tag1.name) }
it { is_expected.not_to include(tag2.name) }
end
end
end
end
context "with categorized topic" do
let(:category) { Fabricate(:category) }
before { topic.update(category_id: category.id) }
context "when topic_page_title_includes_category is false" do
before { SiteSetting.topic_page_title_includes_category = false }
it { is_expected.to eq(topic.title) }
end
context "when topic_page_title_includes_category is true" do
before { SiteSetting.topic_page_title_includes_category = true }
it { is_expected.to start_with(topic.title) }
it { is_expected.to end_with(category.name) }
context "with tagged topic" do
before do
SiteSetting.tagging_enabled = true
topic.tags << [tag1, tag2]
end
it { is_expected.to start_with(topic.title) }
it { is_expected.to end_with(category.name) }
it { is_expected.not_to include(tag1.name) }
it { is_expected.not_to 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 { [p1, p2, p3].each_with_index { |post, index| post.update!(sort_order: index + 1) } }
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) do
Fabricate(:post, topic: topic)
.tap { |p| p.update_column(:image_upload_id, post3_upload.id) }
.reload
end
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 for op and posts image when they have one" do
expect(topic_view_for_post(1).image_url).to end_with(op_upload.url)
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
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 { TopicView.reset_custom_default_scopes }
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(:topic) }
let(:user) { Fabricate(:user) }
let(:category) { topic.category }
before { NewPostManager.stubs(:queue_enabled?).returns(queue_enabled) }
context "when queue is enabled globally" do
let(:queue_enabled) { true }
it { expect(topic_view.queued_posts_enabled?).to be(true) }
end
context "when queue is not enabled globally" do
let(:queue_enabled) { false }
context "when category is moderated" do
before { category.custom_fields[Category::REQUIRE_REPLY_APPROVAL] = true }
it { expect(topic_view.queued_posts_enabled?).to be(true) }
end
context "when category is not moderated" do
it { expect(topic_view.queued_posts_enabled?).to be(nil) }
end
end
end
describe "#tags" do
subject(:topic_view_tags) { topic_view.tags }
let(:topic_view) { described_class.new(topic, user) }
let(:topic) { Fabricate.build(:topic, tags: tags) }
let(:tags) { Fabricate.build_times(2, :tag) }
let(:user) { Fabricate(:user) }
it "returns the tags names" do
expect(topic_view_tags).to match tags.map(&:name)
end
end
end