mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 00:15:24 +08:00
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>
138 lines
4 KiB
Ruby
Vendored
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
|