Fix votersCount inflation in multiple-choice polls

increase_vote_count/3 was incrementing votersCount on every vote
activity, causing inflation when a single voter picks multiple options.
Now only increments when the actor is a new unique voter, and preserves
existing votersCount otherwise.

Also adds is_integer guard to voters_count/1 to handle nil safely, and
adds tests for the voters_count clause ordering and edge cases.
This commit is contained in:
Lain Soykaf 2026-05-06 11:33:34 +04:00
commit 727e9e7749
No known key found for this signature in database
3 changed files with 189 additions and 6 deletions

View file

@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
assert result[:pleroma][:non_anonymous] == true
end
test "prefers votersCount over voters list when both are present" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
assert object.data["votersCount"] == 1
assert length(object.data["voters"]) == 1
object = %{
object
| data: Map.put(object.data, "votersCount", 42)
}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 42
end
test "falls back to voters list when votersCount is absent" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
assert length(object.data["voters"]) == 1
data = Map.delete(object.data, "votersCount")
object = %{object | data: data}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 1
end
test "returns 0 when both votersCount and voters are absent" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
data =
object.data
|> Map.delete("votersCount")
|> Map.delete("voters")
object = %{object | data: data}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 0
end
test "returns 0 when voters list is empty" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
data =
object.data
|> Map.delete("votersCount")
|> Map.put("voters", [])
object = %{object | data: data}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 0
end
test "does not inflate votersCount when same voter picks multiple options" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick several",
poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0, 2])
assert object.data["votersCount"] == 1
assert length(object.data["voters"]) == 1
end
test "preserves votersCount from remote source when existing voter picks another option" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick several",
poll: %{options: ["a", "b"], expires_in: 20, multiple: true}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0, 1])
object = %{object | data: Map.put(object.data, "votersCount", 14)}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 14
end
test "returns 0 when votersCount is explicitly 0" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick one",
poll: %{options: ["a", "b"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
object = %{object | data: Map.put(object.data, "votersCount", 0)}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 0
end
test "falls back to voters list when votersCount is nil" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick one",
poll: %{options: ["a", "b"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
object = %{object | data: Map.put(object.data, "votersCount", nil)}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == length(object.data["voters"])
end
end