0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-12 04:31:29 +08:00
discourse/spec/services/topic_bookmarkable_spec.rb
Chris Alberti bd15d025b2
SECURITY: Enforce first-post visibility for topic bookmarks (#42321)
## 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>
2026-08-05 12:52:40 -05:00

146 lines
5.6 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe TopicBookmarkable do
subject(:registered_bookmarkable) { RegisteredBookmarkable.new(TopicBookmarkable) }
fab!(:user)
fab!(:private_category) { Fabricate(:private_category, group: Fabricate(:group)) }
let(:guardian) { Guardian.new(user) }
let!(:topic1) { Fabricate(:topic) }
let!(:topic2) { Fabricate(:topic) }
let!(:post) { Fabricate(:post, topic: topic1) }
let!(:post2) { Fabricate(:post, topic: topic2) }
let!(:bookmark1) do
Fabricate(:bookmark, user: user, bookmarkable: topic1, name: "something i gotta do")
end
let!(:bookmark2) { Fabricate(:bookmark, user: user, bookmarkable: topic2) }
let!(:bookmark3) { Fabricate(:bookmark) }
let!(:topic_user1) { Fabricate(:topic_user, user: user, topic: topic1) }
let!(:topic_user2) { Fabricate(:topic_user, user: user, topic: topic2) }
describe "#perform_list_query" do
it "returns all the user's bookmarks" do
expect(registered_bookmarkable.perform_list_query(user, guardian).map(&:id)).to match_array(
[bookmark1.id, bookmark2.id],
)
end
it "does not return bookmarks for posts where the user does not have access to the topic category" do
bookmark1.bookmarkable.update!(category: private_category)
expect(registered_bookmarkable.perform_list_query(user, guardian).map(&:id)).to match_array(
[bookmark2.id],
)
end
it "does not return bookmarks for posts where the user does not have access to the private message" do
bookmark1.update!(bookmarkable: Fabricate(:private_message_topic))
expect(registered_bookmarkable.perform_list_query(user, guardian).map(&:id)).to match_array(
[bookmark2.id],
)
end
end
describe "#perform_search_query" do
before { SearchIndexer.enable }
it "returns bookmarks that match by name" do
ts_query = Search.ts_query(term: "gotta", ts_config: "simple")
expect(
registered_bookmarkable.perform_search_query(
registered_bookmarkable.perform_list_query(user, guardian),
"%gotta%",
ts_query,
).map(&:id),
).to match_array([bookmark1.id])
end
it "returns bookmarks that match by post search data (topic title or post content)" do
post.update(raw: "some post content")
topic1.update(title: "a great topic title")
ts_query = Search.ts_query(term: "post content", ts_config: "simple")
expect(
registered_bookmarkable.perform_search_query(
registered_bookmarkable.perform_list_query(user, guardian),
"%post content%",
ts_query,
).map(&:id),
).to match_array([bookmark1.id])
ts_query = Search.ts_query(term: "great topic", ts_config: "simple")
expect(
registered_bookmarkable.perform_search_query(
registered_bookmarkable.perform_list_query(user, guardian),
"%great topic%",
ts_query,
).map(&:id),
).to match_array([bookmark1.id])
ts_query = Search.ts_query(term: "blah", ts_config: "simple")
expect(
registered_bookmarkable.perform_search_query(
registered_bookmarkable.perform_list_query(user, guardian),
"%blah%",
ts_query,
).map(&:id),
).to eq([])
end
end
describe "#can_send_reminder?" do
it "cannot send reminder if the topic is deleted" do
expect(registered_bookmarkable.can_send_reminder?(bookmark1)).to eq(true)
bookmark1.bookmarkable.trash!
bookmark1.reload
expect(registered_bookmarkable.can_send_reminder?(bookmark1)).to eq(false)
end
it "cannot send a reminder if the first post is hidden" do
expect(registered_bookmarkable.can_send_reminder?(bookmark1)).to eq(true)
bookmark1.bookmarkable.first_post.update!(hidden: true)
expect(registered_bookmarkable.can_send_reminder?(bookmark1)).to eq(false)
end
it "cannot send reminder if the user cannot access the topic" do
expect(registered_bookmarkable.can_send_reminder?(bookmark1)).to eq(true)
bookmark1.bookmarkable.update!(category: private_category)
bookmark1.reload
expect(registered_bookmarkable.can_send_reminder?(bookmark1)).to eq(false)
end
end
describe "#reminder_handler" do
it "creates a notification for the user with the correct details" do
expect { registered_bookmarkable.send_reminder_notification(bookmark1) }.to change {
Notification.count
}.by(1)
notif = user.notifications.last
expect(notif.notification_type).to eq(Notification.types[:bookmark_reminder])
expect(notif.topic_id).to eq(bookmark1.bookmarkable_id)
expect(notif.post_number).to eq(1)
expect(notif.data).to eq(
{
title: bookmark1.bookmarkable.title,
bookmarkable_url: bookmark1.bookmarkable.first_post.url,
display_username: bookmark1.user.username,
bookmark_name: bookmark1.name,
bookmark_id: bookmark1.id,
bookmarkable_type: bookmark1.bookmarkable_type,
bookmarkable_id: bookmark1.bookmarkable_id,
}.to_json,
)
end
end
describe "#can_see?" do
it "returns false if the post is in a private category or private message the user cannot see" do
expect(registered_bookmarkable.can_see?(guardian, bookmark1)).to eq(true)
bookmark1.bookmarkable.update!(category: private_category)
expect(registered_bookmarkable.can_see?(guardian, bookmark1)).to eq(false)
bookmark1.update!(bookmarkable: Fabricate(:private_message_topic))
expect(registered_bookmarkable.can_see?(guardian, bookmark1)).to eq(false)
end
end
end