0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/spec/models/user_summary_spec.rb
discoursebot d32c1381a5
SECURITY: Enforce first-post visibility for topic bookmarks [backport 2026.6] (#42354)
Backport of #42321 to release/2026.6.

---

## Summary

Topic bookmark creation, listing, search, and reminder eligibility now
require the first post to be visible to the user. The fix adds an inner
join on the first post with hidden-post filtering to the list query and
delegates creation and visibility checks to `guardian.can_see_post?` on
the first post, preventing an authenticated user from bookmarking a
topic or searching its metadata after the first post is hidden.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1530

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>

---------

Co-authored-by: Chris Alberti <christo@discourse.org>
2026-08-05 12:53:18 -05:00

166 lines
5.5 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe UserSummary do
it "produces secure summaries" do
topic = create_post.topic
user = topic.user
_reply = create_post(user: topic.user, topic: topic)
summary = UserSummary.new(user, Guardian.new)
expect(summary.topics.length).to eq(1)
expect(summary.replies.length).to eq(1)
expect(summary.top_categories.length).to eq(1)
expect(summary.top_categories.first[:topic_count]).to eq(1)
expect(summary.top_categories.first[:post_count]).to eq(1)
topic.update_columns(deleted_at: Time.now)
expect(summary.topics.length).to eq(0)
expect(summary.replies.length).to eq(0)
expect(summary.top_categories.length).to eq(0)
topic.update_columns(deleted_at: nil, visible: false)
expect(summary.topics.length).to eq(0)
expect(summary.replies.length).to eq(0)
expect(summary.top_categories.length).to eq(0)
category = Fabricate(:category)
topic.update_columns(category_id: category.id, deleted_at: nil, visible: true)
category.set_permissions(staff: :full)
category.save
expect(summary.topics.length).to eq(0)
expect(summary.replies.length).to eq(0)
expect(summary.top_categories.length).to eq(0)
end
it "is robust enough to handle bad data" do
UserActionManager.enable
liked_post = create_post
user = Fabricate(:user)
PostActionCreator.like(user, liked_post)
users = UserSummary.new(user, Guardian.new).most_liked_users
expect(users.map(&:id)).to eq([liked_post.user_id])
# really we should not be corrupting stuff like this
# but in production dbs this can happens sometimes I guess
liked_post.user.delete
users = UserSummary.new(user, Guardian.new).most_liked_users
expect(users).to eq([])
end
it "includes ordered top categories" do
u = Fabricate(:user)
UserSummary::MAX_SUMMARY_RESULTS.times do
c = Fabricate(:category)
t = Fabricate(:topic, category: c, user: u)
Fabricate(:post, user: u, topic: t)
end
top_category = Fabricate(:category)
t = Fabricate(:topic, category: top_category, user: u)
Fabricate(:post, user: u, topic: t)
Fabricate(:post, user: u, topic: t)
summary = UserSummary.new(u, Guardian.new)
expect(summary.top_categories.length).to eq(UserSummary::MAX_SUMMARY_RESULTS)
expect(summary.top_categories.first[:id]).to eq(top_category.id)
end
it "excludes moderator action posts" do
topic = create_post.topic
user = topic.user
create_post(user: user, topic: topic)
Fabricate(:small_action, topic: topic, user: user)
summary = UserSummary.new(user, Guardian.new)
expect(summary.topics.length).to eq(1)
expect(summary.replies.length).to eq(1)
expect(summary.top_categories.length).to eq(1)
expect(summary.top_categories.first[:topic_count]).to eq(1)
expect(summary.top_categories.first[:post_count]).to eq(1)
end
describe "#bookmark_count" do
fab!(:user)
fab!(:post_bookmark) { Fabricate(:bookmark, user:, bookmarkable: Fabricate(:post)) }
fab!(:topic_bookmark) { Fabricate(:bookmark, user:, bookmarkable: Fabricate(:topic_with_op)) }
let(:summary) { UserSummary.new(user, Guardian.new(user)) }
before do
Fabricate(:topic_user, user:, topic: post_bookmark.bookmarkable.topic)
Fabricate(:topic_user, user:, topic: topic_bookmark.bookmarkable)
end
it "counts accessible bookmarks" do
expect(summary.bookmark_count).to eq(2)
end
it "excludes bookmarks in inaccessible categories" do
post_bookmark.bookmarkable.topic.update!(
category: Fabricate(:private_category, group: Fabricate(:group)),
)
expect(summary.bookmark_count).to eq(1)
end
it "excludes bookmarks for deleted topics" do
post_bookmark.bookmarkable.topic.trash!
expect(summary.bookmark_count).to eq(1)
end
end
it "returns the most replied to users" do
topic1 = create_post.topic
topic1_post = create_post(topic: topic1)
topic1_reply =
create_post(topic: topic1, reply_to_post_number: topic1_post.post_number, user: topic1.user)
# Create a second topic by the same user as topic1
topic2 = create_post(user: topic1.user).topic
topic2_post = create_post(topic: topic2)
topic2_reply =
create_post(topic: topic2, reply_to_post_number: topic2_post.post_number, user: topic2.user)
# Don't include replies to whispers
topic3 = create_post(user: topic1.user).topic
topic3_post = create_post(topic: topic3, post_type: Post.types[:whisper])
topic3_reply = create_post(topic: topic3, user: topic1.user)
topic3_reply.update_columns(
reply_to_post_number: topic3_post.post_number,
post_type: Post.types[:whisper],
)
# Don't include replies to private messages
replied_to_user = Fabricate(:user)
topic4 =
create_post(
user: topic1.user,
archetype: Archetype.private_message,
target_usernames: [replied_to_user.username],
).topic
topic4_post = create_post(topic: topic4, user: replied_to_user)
topic4_reply =
create_post(topic: topic4, reply_to_post_number: topic4_post.post_number, user: topic4.user)
user_summary = UserSummary.new(topic1.user, Guardian.new(topic1.user))
most_replied_to_users = user_summary.most_replied_to_users
counts =
most_replied_to_users
.index_by { |user_with_count| user_with_count[:id] }
.transform_values { |c| c[:count] }
expect(counts).to eq({ topic1_post.user_id => 1, topic2_post.user_id => 1 })
end
end