1package vouch23import (4 "strings"5 "testing"67 "gno.land/p/nt/testutils/v0"8 "gno.land/p/nt/uassert/v0"9 "gno.land/p/nt/urequire/v0"10)1112var (13 alice = testutils.TestAddress("alice")14 bob = testutils.TestAddress("bob")15 carol = testutils.TestAddress("carol")16 dave = testutils.TestAddress("dave")17)1819const realmPath = "gno.land/r/moul/x/social/vouch/v0"2021func TestValidReason(t *testing.T) {22 tests := []struct {23 name string24 reason string25 want bool26 }{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}4243func TestRecordRefusesWhatItCannotStore(t *testing.T) {44 g := NewGraph()4546 _, err := g.Record(alice, alice, "myself", 0, 100)47 uassert.ErrorIs(t, err, ErrSelfVouch)4849 _, err = g.Record(alice, bob, "", 0, 100)50 uassert.ErrorIs(t, err, ErrBadReason)5152 _, err = g.Record(alice, bob, "fine", -1, 100)53 uassert.ErrorIs(t, err, ErrBadBond)5455 uassert.Equal(t, 0, g.Count())56 uassert.Equal(t, 0, g.People())57 uassert.Equal(t, 0, g.ScoreOf(alice))58}5960// A score counts people. This is the property every caller of IsTrusted is61// relying on, so it is the one worth pinning hardest.62func TestScoreCountsPeopleAndNotVouches(t *testing.T) {63 g := NewGraph()6465 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))6970 // 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())7778 // 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")8485 // 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}9192func TestIsTrusted(t *testing.T) {93 g := NewGraph()94 g.Record(bob, alice, "one", 0, 100)95 g.Record(carol, alice, "two", 0, 100)9697 tests := []struct {98 name string99 addr address100 min int101 want bool102 }{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}115116func TestTheBondFollowsTheVouchInBothDirections(t *testing.T) {117 g := NewGraph()118119 _, 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")124125 // 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))130131 // 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))134135 // 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())143144 // 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())150151 // A reentrant withdrawal finds nothing.152 _, err = g.Withdraw(bob)153 uassert.ErrorIs(t, err, ErrNothingOwed)154}155156func TestRevokeRefusesWhatWasNeverThere(t *testing.T) {157 g := NewGraph()158 _, err := g.Revoke(alice, bob)159 uassert.ErrorIs(t, err, ErrNoVouch)160161 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}165166func 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)171172 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)))176177 // 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")180181 uassert.Equal(t, "a to c", g.ReasonFrom(alice, carol))182 uassert.Equal(t, "", g.ReasonFrom(carol, alice), "no vouch, no reason")183}184185func TestMutual(t *testing.T) {186 g := NewGraph()187 g.Record(alice, bob, "a to b", 0, 100)188189 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")191192 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))196197 // The cheapest sybil shape: two addresses, two transactions, both pass a198 // 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}203204func 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)209210 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)215216 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")219220 // 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}225226func TestBadge(t *testing.T) {227 g := NewGraph()228 uassert.True(t, strings.Contains(Badge(realmPath, alice, g.ScoreOf(alice), 0), "not vouched for"))229230 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)235236 g.Record(carol, alice, "two", 0, 101)237 uassert.True(t, strings.Contains(Badge(realmPath, alice, g.ScoreOf(alice), 0), "2 vouchers]"))238}239240func 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}244Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.