0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 00:15:24 +08:00
discourse/plugins/poll/spec/integration/ranked_choice_result_visibility_spec.rb
Gabriel Grubba 1e48a5d535
FIX: Respect poll result visibility for ranked-choice outcomes (#40578)
Ranked-choice polls always serialized `ranked_choice_outcome` (the
computed winner and round-by-round activity) regardless of the poll's
`results` setting, unlike vote counts and voter lists, which are gated
by `Poll#can_see_results?`.


The outcome was therefore displayed to users who should not yet see
results on `on_vote`, `on_close`, and `staff_only` polls — through the
topic view, the vote/remove-vote responses, and the MessageBus
broadcast.

- Gate `ranked_choice_outcome` in `PollSerializer` behind
`can_see_results?`, matching the other result fields.
- Stop manually re-appending the outcome in `vote`/`remove_vote` so the
serializer's gating is authoritative.
- Serialize the MessageBus payload as an anonymous user so the
topic-wide broadcast only carries data any viewer may see, matching
`PollsUpdater.publish_changes`.
- Don't render an empty results panel to a non-staff voter on a
`staff_only` poll; show the ballot and the staff-only notice instead.



Relates to /t/-/185090
And Patch 998

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:04:38 -03:00

138 lines
4 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe "DiscoursePoll ranked choice result visibility" do
fab!(:voter) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:non_voter) { Fabricate(:user, refresh_auto_groups: true) }
fab!(:author, :admin)
def create_ranked_choice_post(results:)
topic = Fabricate(:topic, user: author)
Fabricate(:post, user: author, topic: topic, raw: <<~RAW)
[poll type=ranked_choice results=#{results}]
- Red
- Blue
- Yellow
[/poll]
RAW
end
def ranked_choice_vote_options(poll)
{
"0" => {
digest: poll.poll_options.first.digest,
rank: "1",
},
"1" => {
digest: poll.poll_options.second.digest,
rank: "2",
},
"2" => {
digest: poll.poll_options.third.digest,
rank: "0",
},
}
end
def vote_in_ranked_choice_poll(user, post)
DiscoursePoll::Poll.vote(
user,
post.id,
DiscoursePoll::DEFAULT_POLL_NAME,
ranked_choice_vote_options(post.polls.first),
)
end
def topic_view_poll(post)
get "/t/#{post.topic.slug}/#{post.topic.id}.json"
expect(response.status).to eq(200)
response.parsed_body["post_stream"]["posts"]
.find { |serialized_post| serialized_post["id"] == post.id }
.fetch("polls")
.first
end
it "omits the outcome from vote responses when staff-only results are hidden from the voter" do
post = create_ranked_choice_post(results: "staff_only")
sign_in(voter)
put "/polls/vote.json",
params: {
post_id: post.id,
poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
options: ranked_choice_vote_options(post.polls.first),
}
expect(response.status).to eq(200)
expect(response.parsed_body["poll"]).not_to have_key("ranked_choice_outcome")
delete "/polls/vote.json",
params: {
post_id: post.id,
poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
}
expect(response.status).to eq(200)
expect(response.parsed_body["poll"]).not_to have_key("ranked_choice_outcome")
end
it "keeps a staff-visible outcome out of shared vote broadcasts" do
post = create_ranked_choice_post(results: "staff_only")
sign_in(author)
messages =
MessageBus.track_publish("/polls/#{post.topic.id}") do
put "/polls/vote.json",
params: {
post_id: post.id,
poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
options: ranked_choice_vote_options(post.polls.first),
}
end
expect(response.status).to eq(200)
expect(response.parsed_body["poll"]).to have_key("ranked_choice_outcome")
expect(messages.size).to eq(1)
published_poll = messages.first.data.deep_stringify_keys["polls"].first
expect(published_poll).not_to have_key("ranked_choice_outcome")
end
it "shows on-vote outcomes to voters without leaking them to non-voters" do
post = create_ranked_choice_post(results: "on_vote")
sign_in(voter)
messages =
MessageBus.track_publish("/polls/#{post.topic.id}") do
put "/polls/vote.json",
params: {
post_id: post.id,
poll_name: DiscoursePoll::DEFAULT_POLL_NAME,
options: ranked_choice_vote_options(post.polls.first),
}
end
expect(response.status).to eq(200)
expect(response.parsed_body["poll"]).to have_key("ranked_choice_outcome")
expect(messages.size).to eq(1)
published_poll = messages.first.data.deep_stringify_keys["polls"].first
expect(published_poll).not_to have_key("ranked_choice_outcome")
sign_in(non_voter)
expect(topic_view_poll(post)).not_to have_key("ranked_choice_outcome")
sign_in(voter)
expect(topic_view_poll(post)).to have_key("ranked_choice_outcome")
end
it "omits on-close outcomes from anonymous topic views while the poll is open" do
post = create_ranked_choice_post(results: "on_close")
vote_in_ranked_choice_poll(voter, post)
expect(topic_view_poll(post)).not_to have_key("ranked_choice_outcome")
end
end