jury_test.gno
6.63 Kb · 175 lines
1package jury
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nym-moultest123/agents/commit/v0"
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/ufmt/v0"
10 "gno.land/p/nt/uassert/v0"
11)
12
13var (
14 opener = testutils.TestAddress("opener")
15 j1 = testutils.TestAddress("juror1")
16 j2 = testutils.TestAddress("juror2")
17 j3 = testutils.TestAddress("juror3")
18 nobody = testutils.TestAddress("nobody")
19)
20
21// Full happy path: 3 jurors commit blind, then reveal; majority upholds.
22func TestCommitRevealMajority(cur realm, t *testing.T) {
23 testing.SetRealm(testing.NewUserRealm(opener))
24 id := OpenCase(cross(cur), "receipt#7", j1.String()+","+j2.String()+", "+j3.String())
25 uassert.Equal(t, string(PhaseCommit), string(Get(id).Phase))
26
27 // Each juror commits a hidden verdict (hash computed off-chain).
28 commitAs(cur, j1, id, true, "s1")
29 commitAs(cur, j2, id, false, "s2")
30 uassert.Equal(t, string(PhaseCommit), string(Get(id).Phase)) // not all in yet
31 commitAs(cur, j3, id, true, "s3")
32 uassert.Equal(t, string(PhaseReveal), string(Get(id).Phase)) // auto-advanced
33
34 revealAs(cur, j1, id, true, "s1")
35 revealAs(cur, j2, id, false, "s2")
36 revealAs(cur, j3, id, true, "s3")
37
38 c := Get(id)
39 uassert.Equal(t, string(PhaseClosed), string(c.Phase))
40 uassert.Equal(t, "upheld", c.Outcome) // 2 yes vs 1 no
41 uassert.Equal(t, 2, c.Yes)
42 uassert.Equal(t, 1, c.No)
43
44 uassert.True(t, len(Render("")) > 0)
45 uassert.True(t, len(Render("1")) > 0)
46}
47
48// A juror cannot change their vote at reveal time: the commitment binds it.
49func TestRevealMustMatchCommitment(cur realm, t *testing.T) {
50 testing.SetRealm(testing.NewUserRealm(opener))
51 id := OpenCase(cross(cur), "claim#3", j1.String())
52 commitAs(cur, j1, id, true, "salt")
53
54 testing.SetRealm(testing.NewUserRealm(j1))
55 uassert.AbortsWithMessage(t, cur, "reveal does not match commitment", func(cur realm) {
56 Reveal(cur, id, false, "salt") // flipped verdict
57 })
58}
59
60func TestOnlyPanelJurors(cur realm, t *testing.T) {
61 testing.SetRealm(testing.NewUserRealm(opener))
62 id := OpenCase(cross(cur), "x", j1.String())
63 testing.SetRealm(testing.NewUserRealm(nobody))
64 uassert.AbortsWithMessage(t, cur, "caller is not on this jury", func(cur realm) {
65 Commit(cur, id, commit.Verdict(true, "s"))
66 })
67}
68
69func TestNoDuplicateJurors(cur realm, t *testing.T) {
70 testing.SetRealm(testing.NewUserRealm(opener))
71 uassert.AbortsWithMessage(t, cur, "duplicate juror: "+j1.String(), func(cur realm) {
72 OpenCase(cur, "x", j1.String()+","+j1.String())
73 })
74}
75
76// helpers
77
78func commitAs(cur realm, juror address, id uint64, verdict bool, salt string) {
79 testing.SetRealm(testing.NewUserRealm(juror))
80 Commit(cross(cur), id, commit.Verdict(verdict, salt))
81}
82
83func revealAs(cur realm, juror address, id uint64, verdict bool, salt string) {
84 testing.SetRealm(testing.NewUserRealm(juror))
85 Reveal(cross(cur), id, verdict, salt)
86}
87
88// Commitments are public, so a juror who copies another's hash would reveal
89// the same (verdict, salt) once the first reveal lands. The copy is refused.
90func TestCopiedCommitmentIsRefused(cur realm, t *testing.T) {
91 testing.SetRealm(testing.NewUserRealm(opener))
92 id := OpenCase(cross(cur), "copy", j1.String()+","+j2.String())
93 commitAs(cur, j1, id, true, "s1")
94 copied := Get(id).Jurors[0].Commitment
95 testing.SetRealm(testing.NewUserRealm(j2))
96 uassert.AbortsContains(t, cur, "commitment already used", func() { Commit(cross(cur), id, copied) })
97}
98
99// The panel arrives as one string, which is what a wallet can send.
100func TestOpenCaseRejectsAnInvalidJuror(cur realm, t *testing.T) {
101 testing.SetRealm(testing.NewUserRealm(opener))
102 uassert.AbortsContains(t, cur, "invalid juror address", func() {
103 OpenCase(cross(cur), "x", j1.String()+",notanaddress")
104 })
105 uassert.AbortsContains(t, cur, "invalid juror address", func() { OpenCase(cross(cur), "x", "") })
106}
107
108// The subject is wallet-controlled now that OpenCase is callable, so it is
109// escaped on both pages that show it.
110func TestSubjectIsEscaped(cur realm, t *testing.T) {
111 testing.SetRealm(testing.NewUserRealm(opener))
112 id := OpenCase(cross(cur), "x | [claim](https://evil.example)", j1.String())
113 idx, page := Render(""), Render(ufmt.Sprintf("%d", id))
114 uassert.False(t, strings.Contains(idx, "[claim](https://evil.example)"), idx)
115 uassert.False(t, strings.Contains(page, "[claim](https://evil.example)"), page)
116}
117
118// An even panel is allowed; a split vote closes as a tie rather than
119// stalling or picking a side.
120func TestEvenPanelSplitIsATie(cur realm, t *testing.T) {
121 testing.SetRealm(testing.NewUserRealm(opener))
122 id := OpenCase(cross(cur), "even", j1.String()+","+j2.String())
123 commitAs(cur, j1, id, true, "t1")
124 commitAs(cur, j2, id, false, "t2")
125 revealAs(cur, j1, id, true, "t1")
126 revealAs(cur, j2, id, false, "t2")
127 uassert.Equal(t, "tie", Outcome(id))
128}
129
130// OpenCase is callable by any wallet, so what it admits is bounded, and the
131// index renders a fixed window whatever the case count.
132func TestOpenCaseIsBounded(cur realm, t *testing.T) {
133 testing.SetRealm(testing.NewUserRealm(opener))
134 uassert.AbortsContains(t, cur, "subject too long", func() {
135 OpenCase(cross(cur), strings.Repeat("s", MaxSubject+1), j1.String())
136 })
137 panel := ""
138 for i := 0; i <= MaxJurors; i++ {
139 if i > 0 {
140 panel += ","
141 }
142 panel += testutils.TestAddress(ufmt.Sprintf("juror%d", i+10)).String()
143 }
144 uassert.AbortsContains(t, cur, "panel too large", func() { OpenCase(cross(cur), "big", panel) })
145
146 for len(cases) < IndexRecent+5 {
147 OpenCase(cross(cur), "filler", j1.String())
148 }
149 idx := Render("")
150 uassert.True(t, strings.Contains(idx, ufmt.Sprintf("The %d most recent of %d cases", IndexRecent, len(cases))), idx)
151 uassert.Equal(t, IndexRecent, strings.Count(idx, "](/r/nym-moultest123/agents/jury/v0:"))
152}
153
154// The case count is bounded, and a subject that sanitizes to nothing is empty.
155func TestCaseCountIsBoundedAndBlankSubjectRefused(cur realm, t *testing.T) {
156 testing.SetRealm(testing.NewUserRealm(opener))
157 uassert.AbortsContains(t, cur, "empty subject", func() { OpenCase(cross(cur), "", j1.String()) })
158 before := len(cases)
159 for len(cases) < MaxCases {
160 cases = append(cases, &Case{ID: uint64(len(cases) + 1), Subject: "filler", Phase: PhaseCommit})
161 }
162 defer func() { cases = cases[:before] }()
163 uassert.AbortsContains(t, cur, "the jury is full", func() { OpenCase(cross(cur), "one more", j1.String()) })
164}
165
166// A commitment is a hash, and a juror cannot store an arbitrary blob as one.
167func TestCommitmentIsBounded(cur realm, t *testing.T) {
168 testing.SetRealm(testing.NewUserRealm(opener))
169 id := OpenCase(cross(cur), "sized", j1.String())
170 testing.SetRealm(testing.NewUserRealm(j1))
171 uassert.AbortsContains(t, cur, "commitment too long", func() {
172 Commit(cross(cur), id, strings.Repeat("a", MaxCommitment+1))
173 })
174 uassert.Equal(t, 64, len(commit.Verdict(true, "s")))
175}