vouch_test.gno
8.01 Kb · 243 lines
1package vouch
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10)
11
12var (
13 alice = testutils.TestAddress("alice")
14 bob = testutils.TestAddress("bob")
15 carol = testutils.TestAddress("carol")
16 dave = testutils.TestAddress("dave")
17)
18
19const realmPath = "gno.land/r/moul/x/social/vouch/v0"
20
21func TestValidReason(t *testing.T) {
22 tests := []struct {
23 name string
24 reason string
25 want bool
26 }{
27 {"plain", "worked with them for a year", true},
28 {"markdown is allowed and escaped later", "[x](y) | z", true},
29 {"at the limit", strings.Repeat("x", MaxReasonLen), true},
30 {"empty", "", false},
31 {"blank", " ", false},
32 {"too long", strings.Repeat("x", MaxReasonLen+1), false},
33 {"a reason is one line", "met them\nat a conference", false},
34 {"no tabs either", "met them\there", false},
35 {"control character", "a\x01b", false},
36 {"delete character", "a\x7fb", false},
37 }
38 for _, tt := range tests {
39 uassert.Equal(t, tt.want, ValidReason(tt.reason), tt.name)
40 }
41}
42
43func TestRecordRefusesWhatItCannotStore(t *testing.T) {
44 g := NewGraph()
45
46 _, err := g.Record(alice, alice, "myself", 0, 100)
47 uassert.ErrorIs(t, err, ErrSelfVouch)
48
49 _, err = g.Record(alice, bob, "", 0, 100)
50 uassert.ErrorIs(t, err, ErrBadReason)
51
52 _, err = g.Record(alice, bob, "fine", -1, 100)
53 uassert.ErrorIs(t, err, ErrBadBond)
54
55 uassert.Equal(t, 0, g.Count())
56 uassert.Equal(t, 0, g.People())
57 uassert.Equal(t, 0, g.ScoreOf(alice))
58}
59
60// A score counts people. This is the property every caller of IsTrusted is
61// relying on, so it is the one worth pinning hardest.
62func TestScoreCountsPeopleAndNotVouches(t *testing.T) {
63 g := NewGraph()
64
65 updated, err := g.Record(bob, alice, "known them for years", 0, 100)
66 urequire.NoError(t, err)
67 uassert.False(t, updated, "the first vouch is not an update")
68 uassert.Equal(t, 1, g.ScoreOf(alice))
69
70 // Saying it again is an edit, not a second voice.
71 updated, err = g.Record(bob, alice, "still true", 0, 101)
72 urequire.NoError(t, err)
73 uassert.True(t, updated, "the second vouch replaced the first")
74 uassert.Equal(t, 1, g.ScoreOf(alice), "one person is one point, however often they say it")
75 uassert.Equal(t, "still true", g.ReasonFrom(bob, alice), "the reason was updated")
76 uassert.Equal(t, 1, g.Count())
77
78 // A different person does move it.
79 _, err = g.Record(carol, alice, "we shipped together", 0, 102)
80 urequire.NoError(t, err)
81 uassert.Equal(t, 2, g.ScoreOf(alice))
82 uassert.Equal(t, 2, g.Count())
83 uassert.Equal(t, 1, g.People(), "one address has been vouched for")
84
85 // And revoking lowers it.
86 _, err = g.Revoke(carol, alice)
87 urequire.NoError(t, err)
88 uassert.Equal(t, 1, g.ScoreOf(alice))
89 uassert.Equal(t, 1, g.Count())
90}
91
92func TestIsTrusted(t *testing.T) {
93 g := NewGraph()
94 g.Record(bob, alice, "one", 0, 100)
95 g.Record(carol, alice, "two", 0, 100)
96
97 tests := []struct {
98 name string
99 addr address
100 min int
101 want bool
102 }{
103 {"below the score", alice, 1, true},
104 {"at the score", alice, 2, true},
105 {"above the score", alice, 3, false},
106 {"zero is read as one", alice, 0, true},
107 {"zero does not open the gate for nobody", dave, 0, false},
108 {"a negative min is read as one too", dave, -5, false},
109 {"nobody vouches for dave", dave, 1, false},
110 }
111 for _, tt := range tests {
112 uassert.Equal(t, tt.want, g.IsTrusted(tt.addr, tt.min), tt.name)
113 }
114}
115
116func TestTheBondFollowsTheVouchInBothDirections(t *testing.T) {
117 g := NewGraph()
118
119 _, err := g.Record(bob, alice, "staking 1 GNOT on this", 1_000_000, 100)
120 urequire.NoError(t, err)
121 uassert.Equal(t, int64(1_000_000), g.BondedFor(alice))
122 uassert.Equal(t, int64(1_000_000), g.TotalBonded())
123 uassert.Equal(t, int64(0), g.Owed(bob), "nothing is owed while the vouch stands")
124
125 // A repeated vouch adds to the bond rather than replacing it.
126 _, err = g.Record(bob, alice, "doubling down", 500_000, 101)
127 urequire.NoError(t, err)
128 uassert.Equal(t, int64(1_500_000), g.BondedFor(alice))
129 uassert.Equal(t, int64(1_500_000), g.BondFrom(bob, alice))
130
131 // A second voucher's bond piles on top.
132 g.Record(carol, alice, "and me", 250_000, 102)
133 uassert.Equal(t, int64(1_750_000), g.BondedFor(alice))
134
135 // Revoking returns exactly what that voucher put in, and nothing else.
136 refund, err := g.Revoke(bob, alice)
137 urequire.NoError(t, err)
138 uassert.Equal(t, int64(1_500_000), refund)
139 uassert.Equal(t, int64(250_000), g.BondedFor(alice), "carol's bond stays")
140 uassert.Equal(t, int64(250_000), g.TotalBonded())
141 uassert.Equal(t, int64(1_500_000), g.Owed(bob))
142 uassert.Equal(t, int64(1_500_000), g.TotalOwed())
143
144 // Withdrawing zeroes the credit before the realm sends anything.
145 got, err := g.Withdraw(bob)
146 urequire.NoError(t, err)
147 uassert.Equal(t, int64(1_500_000), got)
148 uassert.Equal(t, int64(0), g.Owed(bob))
149 uassert.Equal(t, int64(0), g.TotalOwed())
150
151 // A reentrant withdrawal finds nothing.
152 _, err = g.Withdraw(bob)
153 uassert.ErrorIs(t, err, ErrNothingOwed)
154}
155
156func TestRevokeRefusesWhatWasNeverThere(t *testing.T) {
157 g := NewGraph()
158 _, err := g.Revoke(alice, bob)
159 uassert.ErrorIs(t, err, ErrNoVouch)
160
161 g.Record(alice, bob, "fine", 0, 100)
162 _, err = g.Revoke(bob, alice)
163 uassert.ErrorIs(t, err, ErrNoVouch, "a vouch is directed, so the other way round is not one")
164}
165
166func TestTheTwoDirections(t *testing.T) {
167 g := NewGraph()
168 g.Record(alice, bob, "a to b", 0, 100)
169 g.Record(alice, carol, "a to c", 0, 100)
170 g.Record(bob, carol, "b to c", 0, 100)
171
172 uassert.Equal(t, 0, len(g.VouchedBy(alice)), "nobody vouches for alice")
173 uassert.Equal(t, 2, len(g.VouchesOf(alice)))
174 uassert.Equal(t, 2, len(g.VouchedBy(carol)))
175 uassert.Equal(t, 0, len(g.VouchesOf(carol)))
176
177 // Sorted by address, so two identical calls render identically.
178 in := g.VouchedBy(carol)
179 uassert.True(t, in[0].String() < in[1].String(), "inbound is sorted")
180
181 uassert.Equal(t, "a to c", g.ReasonFrom(alice, carol))
182 uassert.Equal(t, "", g.ReasonFrom(carol, alice), "no vouch, no reason")
183}
184
185func TestMutual(t *testing.T) {
186 g := NewGraph()
187 g.Record(alice, bob, "a to b", 0, 100)
188
189 uassert.False(t, g.Mutual(alice, bob), "one direction is not mutual")
190 uassert.False(t, g.Mutual(alice, alice), "an address is never mutual with itself")
191
192 g.Record(bob, alice, "b to a", 0, 101)
193 uassert.True(t, g.Mutual(alice, bob))
194 uassert.True(t, g.Mutual(bob, alice), "mutual is symmetric")
195 uassert.False(t, g.Mutual(alice, carol))
196
197 // The cheapest sybil shape: two addresses, two transactions, both pass a
198 // gate set at one. That is the whole reason Mutual is exported.
199 uassert.True(t, g.IsTrusted(alice, 1))
200 uassert.True(t, g.IsTrusted(bob, 1))
201 uassert.False(t, g.IsTrusted(alice, 2))
202}
203
204func TestLeaderboardIsATotalOrder(t *testing.T) {
205 g := NewGraph()
206 g.Record(alice, carol, "one", 0, 100)
207 g.Record(bob, carol, "two", 0, 100)
208 g.Record(alice, bob, "three", 100, 100)
209
210 rows := g.Leaderboard(10)
211 urequire.Equal(t, 2, len(rows))
212 uassert.Equal(t, carol.String(), rows[0].Addr.String(), "highest score first")
213 uassert.Equal(t, 2, rows[0].Score)
214 uassert.Equal(t, int64(100), rows[1].Bonded)
215
216 uassert.Equal(t, 1, len(g.Leaderboard(1)), "limit truncates")
217 uassert.Equal(t, 0, len(g.Leaderboard(0)))
218 uassert.Equal(t, 0, len(NewGraph().Leaderboard(10)), "an empty graph ranks nobody")
219
220 // Revoking the last vouch for an address drops it off the board.
221 g.Revoke(alice, bob)
222 uassert.Equal(t, 1, len(g.Leaderboard(10)))
223 uassert.Equal(t, 0, g.ScoreOf(bob))
224}
225
226func TestBadge(t *testing.T) {
227 g := NewGraph()
228 uassert.True(t, strings.Contains(Badge(realmPath, alice, g.ScoreOf(alice), 0), "not vouched for"))
229
230 g.Record(bob, alice, "one", 2_000_000, 100)
231 got := Badge(realmPath, alice, g.ScoreOf(alice), g.BondedFor(alice))
232 uassert.True(t, strings.Contains(got, "1 voucher]"), "singular: "+got)
233 uassert.True(t, strings.Contains(got, "2 GNOT bonded"), got)
234 uassert.True(t, strings.Contains(got, AddrURL(realmPath, alice)), got)
235
236 g.Record(carol, alice, "two", 0, 101)
237 uassert.True(t, strings.Contains(Badge(realmPath, alice, g.ScoreOf(alice), 0), "2 vouchers]"))
238}
239
240func TestURLsStripTheChainDomain(t *testing.T) {
241 uassert.Equal(t, "/r/moul/x/social/vouch/v0", RealmURL(realmPath))
242 uassert.Equal(t, "/r/a/b:addr/"+alice.String(), AddrURL("gno.land/r/a/b", alice))
243}