2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-03 23:54:20 +08:00
discourse/spec/models/user_summary_spec.rb
Régis Hanol 06975167e5
FIX: Respect permissions when counting bookmarks in user summary (#36856)
Previously, `UserSummary#bookmark_count` used a simple database count
that included all bookmarks regardless of whether the user could still
access the bookmarked content. This led to inflated counts when:

- Topics were moved to private categories the user can't access
- Topics or posts were soft-deleted
- Access was revoked for any other reason

The fix delegates counting to `BookmarkQuery#count_all`, which applies
the same permission checks used when listing bookmarks. This ensures
the count shown on user profiles matches what users can actually see.

To avoid duplicating query logic, the existing `list_all` method was
refactored to extract `build_list_queries` as a shared helper. The
`unread_notifications` method was also cleaned up by breaking out
several smaller private methods for better readability.

Ref - https://meta.discourse.org/t/391822
2026-01-21 16:34:55 +01:00

163 lines
5.4 KiB
Ruby

# 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)) }
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, reply_to_post_number: topic3_post.post_number, user: topic3.user)
# 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