package api
import (
"path/filepath"
"strconv"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// A poll's ballots belong to the post, not the version they were cast on
// (#375): editing text outside the poll keeps every ballot, a ballot may
// still be cast through an older version's hash, and an edit that changes
// the options starts a fresh poll.
func TestAPollKeepsItsBallotsThroughAnEdit(t *testing.T) {
ix, z, _ := attachStore(t)
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z, Sync: zitSync(z, ix)}}),
Removals: memRemovals{}, Forum: db}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
poll := "```poll\nquestion: lunch?\n- pizza\n- ramen\n- tacos\n```\n"
out, err := r.Invoke(as("jva", "3"), "forum.post", forumPostIn{Repo: "test", Title: "lunch", Body: "Vote below.\n\n" + poll})
if err != nil {
t.Fatal(err)
}
first := out.(forumPostOut).Hash
vote := func(who, post string, choice int) {
t.Helper()
if _, err := r.Invoke(as(who, "3"), "forum.poll", pollVoteIn{Repo: "test", Post: post, Choice: choice}); err != nil {
t.Fatalf("FAIL: %s voting through %.10s: %v", who, post, err)
}
}
tally := func(who string) pollRendered {
t.Helper()
g, err := r.Invoke(as(who, "2"), "forum.get", forumGetIn{Repo: "test", Root: first})
if err != nil {
t.Fatal(err)
}
return g.(forumGetOut).Posts[0].Poll
}
vote("jva", first, 1)
vote("jan", first, 2)
if _, err := r.Invoke(as("jva", "3"), "forum.edit", forumEditIn{Repo: "test", Post: first, Body: "Vote below, by Friday.\n\n" + poll}); err != nil {
t.Fatal(err)
}
p := tally("jva")
if p.Total != 2 || p.Counts[1] != 1 || p.Counts[2] != 1 || p.YourChoice != 1 {
t.Fatalf("FAIL: after an edit outside the poll: %+v", p)
}
t.Log("ok: an edit outside the poll keeps its ballots")
// A reader whose page still names the first version votes, and
// changes their mind, on the one poll.
vote("jan", first, 0)
if p := tally("jan"); p.Total != 2 || p.Counts[0] != 1 || p.Counts[2] != 0 || p.YourChoice != 0 {
t.Fatalf("FAIL: a ballot cast through the older version: %+v", p)
}
t.Log("ok: an older version's hash still reaches the poll, one ballot per account")
if _, err := r.Invoke(as("jva", "3"), "forum.edit", forumEditIn{Repo: "test", Post: first,
Body: "New options.\n\n```poll\nquestion: lunch?\n- soup\n- salad\n```\n"}); err != nil {
t.Fatal(err)
}
if p := tally("jva"); p.Total != 0 || p.YourChoice != -1 || len(p.Counts) != 2 {
t.Fatalf("FAIL: changed options kept ballots cast on other answers: %+v", p)
}
t.Log("ok: changing the options starts a fresh poll")
// History shows each version's poll with its ballots: the two
// versions asking pizza/ramen/tacos carry that poll's 2, the one
// asking soup/salad its own 0 (#376's leftover).
h, err := r.Invoke(as("jan", "2"), "forum.history", forumHistoryIn{Repo: "test", Root: first, Post: first})
if err != nil {
t.Fatal(err)
}
vs := h.(forumHistoryOut).Versions
if len(vs) != 3 {
t.Fatalf("FAIL: history has %d versions, want 3", len(vs))
}
for _, v := range vs {
want := 2
if len(v.Poll.Options) == 2 {
want = 0
}
if !v.Poll.Present || v.Poll.Total != want {
t.Errorf("FAIL: version %.10s poll %+v, want present with %d ballots", v.Hash, v.Poll, want)
}
if !strings.Contains(string(v.HTML), strconv.Itoa(want)+" ballots") {
t.Errorf("FAIL: version %.10s draws no %d ballots", v.Hash, want)
}
}
}