Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

curated_test.gno

14.00 Kb · 416 lines
  1package curated
  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
 12const (
 13	deposit = int64(1000)
 14	window  = int64(100)
 15)
 16
 17var (
 18	alice = testutils.TestAddress("alice")
 19	bob   = testutils.TestAddress("bob")
 20	carol = testutils.TestAddress("carol")
 21	dave  = testutils.TestAddress("dave")
 22)
 23
 24// list returns a registry holding one entry owned by alice, listed at height
 25// 100, which is the starting point of most cases below.
 26func list(t *testing.T) *Registry {
 27	t.Helper()
 28	r := New(deposit, window)
 29	urequire.NoError(t, r.Apply("gnoswap", "https://gnoswap.io", "an AMM", alice, deposit, 100))
 30	return r
 31}
 32
 33func TestValidKey(t *testing.T) {
 34	tests := []struct {
 35		key  string
 36		want bool
 37	}{
 38		{"gnoswap", true},
 39		{"a", true},
 40		{"0", true},
 41		{"gno-swap_v2.1", true},
 42		{strings.Repeat("x", MaxKeyLen), true},
 43		{"", false},
 44		{strings.Repeat("x", MaxKeyLen+1), false},
 45		{"GnoSwap", false},
 46		{"gno swap", false},
 47		{"gno/swap", false},
 48		{"gno:swap", false},
 49		{".", false},
 50		{"..", false},
 51		{"-leading", false},
 52		{"_leading", false},
 53		{"émoji", false},
 54	}
 55	for _, tt := range tests {
 56		uassert.Equal(t, tt.want, ValidKey(tt.key), tt.key)
 57	}
 58}
 59
 60func TestValidURL(t *testing.T) {
 61	tests := []struct {
 62		name string
 63		url  string
 64		want bool
 65	}{
 66		{"https", "https://gnoswap.io/pools", true},
 67		{"an on-chain path, which has no scheme to carry", "/r/moul/home", true},
 68		{"query and fragment", "https://x.io/a?b=c#d", true},
 69		{"a pipe is escaped at render time, not refused", "https://x.io/a|b", true},
 70		{"empty", "", false},
 71		{"too long", "https://x.io/" + strings.Repeat("x", MaxURLLen), false},
 72		{"a space", "https://x.io/a b", false},
 73		{"a newline", "https://x.io/a\nb", false},
 74		{"a control character", "https://x.io/a\x01b", false},
 75	}
 76	for _, tt := range tests {
 77		uassert.Equal(t, tt.want, ValidURL(tt.url), tt.name)
 78	}
 79}
 80
 81func TestValidDescription(t *testing.T) {
 82	tests := []struct {
 83		name string
 84		desc string
 85		want bool
 86	}{
 87		{"plain", "an AMM on gno.land", true},
 88		{"markdown is allowed and escaped later", "[x](y) | z", true},
 89		{"empty", "", false},
 90		{"blank", "   ", false},
 91		{"too long", strings.Repeat("x", MaxDescLen+1), false},
 92		{"a newline would break its own table cell", "a\nb", false},
 93		{"a tab, same reason", "a\tb", false},
 94	}
 95	for _, tt := range tests {
 96		uassert.Equal(t, tt.want, ValidDescription(tt.desc), tt.name)
 97	}
 98}
 99
100func TestApplyRefusesWhatItCannotStore(t *testing.T) {
101	tests := []struct {
102		name string
103		key  string
104		url  string
105		desc string
106		paid int64
107		want error
108	}{
109		{"bad key", "Gnoswap", "https://x.io", "an AMM", deposit, ErrBadKey},
110		{"bad url", "gnoswap", "", "an AMM", deposit, ErrBadURL},
111		{"bad description", "gnoswap", "https://x.io", " ", deposit, ErrBadDescription},
112		{"underpaid", "gnoswap", "https://x.io", "an AMM", deposit - 1, ErrWrongDeposit},
113		{"overpaid", "gnoswap", "https://x.io", "an AMM", deposit + 1, ErrWrongDeposit},
114		{"nothing attached", "gnoswap", "https://x.io", "an AMM", 0, ErrWrongDeposit},
115	}
116	for _, tt := range tests {
117		r := New(deposit, window)
118		err := r.Apply(tt.key, tt.url, tt.desc, alice, tt.paid, 100)
119		uassert.ErrorIs(t, err, tt.want, tt.name)
120		uassert.Equal(t, 0, r.Count(), tt.name+": nothing was listed")
121		uassert.Equal(t, int64(0), r.Locked(), tt.name+": nothing was locked")
122	}
123}
124
125func TestApplyListsImmediately(t *testing.T) {
126	r := list(t)
127
128	e, ok := r.Get("gnoswap")
129	urequire.True(t, ok, "the entry exists")
130	uassert.Equal(t, "https://gnoswap.io", e.URL)
131	uassert.Equal(t, "an AMM", e.Description)
132	uassert.Equal(t, alice.String(), e.Owner.String())
133	uassert.Equal(t, deposit, e.Deposit)
134	uassert.Equal(t, int64(100), e.At)
135	uassert.Equal(t, "listed", e.State.String())
136
137	uassert.True(t, r.IsListed("gnoswap"))
138	uassert.False(t, r.IsListed("nothing"))
139	uassert.Equal(t, 1, r.Count())
140	uassert.Equal(t, 1, r.Records())
141	uassert.Equal(t, deposit, r.Locked())
142	uassert.Equal(t, int64(0), r.Owed())
143}
144
145// A key is unique while it is on the list, and free again once it is off it: a
146// challenge removes an entry, it does not burn the name.
147func TestAKeyIsTakenUntilItIsRemoved(t *testing.T) {
148	r := list(t)
149
150	uassert.ErrorIs(t, r.Apply("gnoswap", "https://other.io", "mine now", bob, deposit, 101), ErrTaken)
151	urequire.NoError(t, r.Unlist("gnoswap", alice))
152	uassert.False(t, r.IsListed("gnoswap"))
153
154	urequire.NoError(t, r.Apply("gnoswap", "https://other.io", "mine now", bob, deposit, 102))
155	e, _ := r.Get("gnoswap")
156	uassert.Equal(t, bob.String(), e.Owner.String())
157	uassert.Equal(t, int64(102), e.At, "a re-application is a fresh entry")
158	uassert.Equal(t, 1, r.Count())
159	uassert.Equal(t, 1, r.Records(), "and it reuses its place in the listing order")
160}
161
162func TestListedIsOldestFirstAndSkipsTheRemoved(t *testing.T) {
163	r := New(deposit, window)
164	urequire.NoError(t, r.Apply("one", "https://1.io", "first", alice, deposit, 100))
165	urequire.NoError(t, r.Apply("two", "https://2.io", "second", alice, deposit, 101))
166	urequire.NoError(t, r.Apply("three", "https://3.io", "third", alice, deposit, 102))
167
168	got := r.Listed()
169	urequire.Equal(t, 3, len(got))
170	uassert.Equal(t, "one", got[0].Key)
171	uassert.Equal(t, "two", got[1].Key)
172	uassert.Equal(t, "three", got[2].Key)
173
174	urequire.NoError(t, r.Unlist("two", alice))
175	got = r.Listed()
176	urequire.Equal(t, 2, len(got))
177	uassert.Equal(t, "one", got[0].Key)
178	uassert.Equal(t, "three", got[1].Key)
179	uassert.Equal(t, 0, len(r.Challenged()))
180}
181
182func TestChallengeRefusals(t *testing.T) {
183	tests := []struct {
184		name string
185		key  string
186		who  address
187		bond int64
188		want error
189	}{
190		{"no such entry", "nothing", bob, deposit, ErrNoEntry},
191		{"the bond must match", "gnoswap", bob, deposit - 1, ErrWrongBond},
192		{"and may not exceed it either", "gnoswap", bob, deposit + 1, ErrWrongBond},
193		{"an owner cannot shield their own entry", "gnoswap", alice, deposit, ErrSelfChallenge},
194	}
195	for _, tt := range tests {
196		r := list(t)
197		uassert.ErrorIs(t, r.Challenge(tt.key, tt.who, tt.bond, 200), tt.want, tt.name)
198		uassert.Equal(t, deposit, r.Locked(), tt.name+": no bond was taken")
199		_, open := r.ChallengeOf("gnoswap")
200		uassert.False(t, open, tt.name+": no challenge was opened")
201	}
202}
203
204func TestChallengeOpensOnceAndSetsTheDeadline(t *testing.T) {
205	r := list(t)
206
207	bond, ok := r.BondFor("gnoswap")
208	urequire.True(t, ok, "it is challengeable")
209	uassert.Equal(t, deposit, bond, "the bond matches that entry's own deposit")
210
211	urequire.NoError(t, r.Challenge("gnoswap", bob, bond, 200))
212	c, open := r.ChallengeOf("gnoswap")
213	urequire.True(t, open, "the challenge is readable")
214	uassert.Equal(t, bob.String(), c.Challenger.String())
215	uassert.Equal(t, int64(200+window), c.Deadline)
216	uassert.True(t, c.Open(200+window-1))
217	uassert.False(t, c.Open(200+window), "the deadline is exclusive")
218	uassert.Equal(t, "challenged", mustGet(t, r, "gnoswap").State.String())
219	uassert.True(t, r.IsListed("gnoswap"), "a challenge is an objection, not a verdict")
220	uassert.Equal(t, 2*deposit, r.Locked(), "the deposit and the bond")
221
222	// A second challenge would be a second bond on one entry.
223	uassert.ErrorIs(t, r.Challenge("gnoswap", carol, deposit, 201), ErrChallenged)
224	_, ok = r.BondFor("gnoswap")
225	uassert.False(t, ok, "and BondFor says so before any money is read")
226}
227
228func TestVoteIsOneAddressOneVoteWhileTheChallengeIsOpen(t *testing.T) {
229	r := list(t)
230	uassert.ErrorIs(t, r.Vote("gnoswap", carol, true, 200), ErrNoChallenge)
231	uassert.ErrorIs(t, r.Vote("nothing", carol, true, 200), ErrNoEntry)
232
233	urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))
234	urequire.NoError(t, r.Vote("gnoswap", carol, true, 210))
235	uassert.ErrorIs(t, r.Vote("gnoswap", carol, false, 211), ErrAlreadyVoted)
236	uassert.ErrorIs(t, r.Vote("gnoswap", carol, true, 212), ErrAlreadyVoted)
237	urequire.NoError(t, r.Vote("gnoswap", dave, false, 213))
238
239	c, _ := r.ChallengeOf("gnoswap")
240	uassert.Equal(t, int64(1), c.Keep)
241	uassert.Equal(t, int64(1), c.Remove)
242	uassert.Equal(t, 2, c.Voters())
243	uassert.True(t, c.HasVoted(carol))
244	uassert.False(t, c.HasVoted(alice))
245
246	// Past the deadline the poll is shut, and so is the deadline itself.
247	uassert.ErrorIs(t, r.Vote("gnoswap", alice, true, 200+window), ErrVotingClosed)
248	uassert.Equal(t, 2, c.Voters())
249}
250
251// The outcome table: who wins, and what the loser pays. A tie keeps the entry.
252func TestResolvePaysTheWinnerAndTiesKeep(t *testing.T) {
253	tests := []struct {
254		name       string
255		keep       []address
256		remove     []address
257		wantKept   bool
258		wantWinner address
259		wantAmount int64
260	}{
261		{"nobody voted, the incumbent keeps it", nil, nil, true, alice, deposit},
262		{"a tie keeps it", []address{carol}, []address{dave}, true, alice, deposit},
263		{"a majority to keep", []address{carol, dave}, nil, true, alice, deposit},
264		{"a majority to remove", nil, []address{carol, dave}, false, bob, 2 * deposit},
265	}
266	for _, tt := range tests {
267		r := list(t)
268		urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))
269		for _, who := range tt.keep {
270			urequire.NoError(t, r.Vote("gnoswap", who, true, 210))
271		}
272		for _, who := range tt.remove {
273			urequire.NoError(t, r.Vote("gnoswap", who, false, 210))
274		}
275
276		out, err := r.Resolve("gnoswap", 200+window)
277		urequire.NoError(t, err, tt.name)
278		uassert.Equal(t, tt.wantKept, out.Kept, tt.name)
279		uassert.Equal(t, tt.wantWinner.String(), out.Winner.String(), tt.name)
280		uassert.Equal(t, tt.wantAmount, out.Amount, tt.name)
281		uassert.Equal(t, tt.wantAmount, r.CreditOf(tt.wantWinner), tt.name+": credited, not sent")
282		uassert.Equal(t, tt.wantKept, r.IsListed("gnoswap"), tt.name)
283
284		// Whatever happened, every ugnot is either still backing the entry or
285		// assigned to somebody.
286		uassert.Equal(t, 2*deposit, r.Locked()+r.Owed(), tt.name+": the books balance")
287
288		// The challenge is gone either way, so nothing resolves twice.
289		_, open := r.ChallengeOf("gnoswap")
290		uassert.False(t, open, tt.name)
291		_, err = r.Resolve("gnoswap", 200+window)
292		uassert.ErrorIs(t, err, ErrNoChallenge, tt.name)
293	}
294}
295
296func TestResolveRefusesBeforeTheDeadline(t *testing.T) {
297	r := list(t)
298	_, err := r.Resolve("gnoswap", 200)
299	uassert.ErrorIs(t, err, ErrNoChallenge, "an unchallenged entry has nothing to resolve")
300	_, err = r.Resolve("nothing", 200)
301	uassert.ErrorIs(t, err, ErrNoEntry)
302
303	urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))
304	_, err = r.Resolve("gnoswap", 200+window-1)
305	uassert.ErrorIs(t, err, ErrTooEarly)
306	uassert.Equal(t, int64(0), r.Owed(), "nothing was paid out early")
307	uassert.Equal(t, "challenged", mustGet(t, r, "gnoswap").State.String())
308}
309
310// Losing a challenge frees the key, and the deposit that backed it has already
311// gone to the challenger, so re-listing it costs a fresh deposit.
312func TestAnEntryThatLosesIsRemovedAndItsDepositIsGone(t *testing.T) {
313	r := list(t)
314	urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))
315	urequire.NoError(t, r.Vote("gnoswap", carol, false, 210))
316	_, err := r.Resolve("gnoswap", 200+window)
317	urequire.NoError(t, err)
318
319	uassert.Equal(t, "removed", mustGet(t, r, "gnoswap").State.String())
320	uassert.Equal(t, 0, r.Count())
321	uassert.Equal(t, int64(0), r.Locked())
322	uassert.Equal(t, 2*deposit, r.Owed())
323	uassert.Equal(t, int64(0), r.CreditOf(alice), "the owner lost the deposit")
324	uassert.ErrorIs(t, r.Unlist("gnoswap", alice), ErrNoEntry, "there is nothing left to take down")
325}
326
327func TestUnlistIsTheOwnersAndNotWhileChallenged(t *testing.T) {
328	r := list(t)
329	uassert.ErrorIs(t, r.Unlist("nothing", alice), ErrNoEntry)
330	uassert.ErrorIs(t, r.Unlist("gnoswap", bob), ErrNotOwner)
331
332	urequire.NoError(t, r.Challenge("gnoswap", bob, deposit, 200))
333	uassert.ErrorIs(t, r.Unlist("gnoswap", alice), ErrChallenged,
334		"an owner who could walk away mid-challenge would be risking nothing")
335	uassert.Equal(t, int64(0), r.CreditOf(alice))
336
337	// Once the challenge is behind it, the entry is the owner's to take down.
338	urequire.NoError(t, r.Vote("gnoswap", carol, true, 210))
339	_, err := r.Resolve("gnoswap", 200+window)
340	urequire.NoError(t, err)
341	urequire.NoError(t, r.Unlist("gnoswap", alice))
342	uassert.Equal(t, 2*deposit, r.CreditOf(alice), "the bond she won, plus her own deposit back")
343	uassert.Equal(t, int64(0), r.Locked())
344}
345
346func TestWithdrawZeroesTheCreditBeforeItIsPaid(t *testing.T) {
347	r := list(t)
348	urequire.NoError(t, r.Unlist("gnoswap", alice))
349	uassert.Equal(t, deposit, r.Owed())
350
351	_, err := r.Withdraw(bob)
352	uassert.ErrorIs(t, err, ErrNothingOwed, "bob is owed nothing")
353
354	got, err := r.Withdraw(alice)
355	urequire.NoError(t, err)
356	uassert.Equal(t, deposit, got)
357	uassert.Equal(t, int64(0), r.CreditOf(alice))
358	uassert.Equal(t, int64(0), r.Owed())
359
360	// A second call, which is what a reentrant one would be, finds nothing.
361	_, err = r.Withdraw(alice)
362	uassert.ErrorIs(t, err, ErrNothingOwed)
363}
364
365func TestCreditsAccumulateAcrossEntries(t *testing.T) {
366	r := list(t)
367	urequire.NoError(t, r.Apply("gnoscan", "https://gnoscan.io", "an explorer", alice, deposit, 101))
368	urequire.NoError(t, r.Unlist("gnoswap", alice))
369	urequire.NoError(t, r.Unlist("gnoscan", alice))
370
371	uassert.Equal(t, 2*deposit, r.CreditOf(alice), "crediting twice owes the sum")
372	got, err := r.Withdraw(alice)
373	urequire.NoError(t, err)
374	uassert.Equal(t, 2*deposit, got, "and one withdrawal collects both")
375}
376
377// Everything the registry ever took in is either locked behind something live
378// or owed to somebody, at every step of a full round.
379func TestTheBooksBalanceAtEveryStep(t *testing.T) {
380	r := New(deposit, window)
381	paidIn := int64(0)
382
383	check := func(step string) {
384		t.Helper()
385		uassert.Equal(t, paidIn, r.Locked()+r.Owed(), step)
386	}
387	check("empty")
388
389	urequire.NoError(t, r.Apply("one", "https://1.io", "first", alice, deposit, 100))
390	paidIn += deposit
391	check("after a listing")
392
393	urequire.NoError(t, r.Challenge("one", bob, deposit, 200))
394	paidIn += deposit
395	check("after a challenge")
396
397	urequire.NoError(t, r.Vote("one", carol, false, 210))
398	check("after a vote")
399
400	_, err := r.Resolve("one", 200+window)
401	urequire.NoError(t, err)
402	check("after a resolution")
403
404	_, err = r.Withdraw(bob)
405	urequire.NoError(t, err)
406	paidIn -= 2 * deposit
407	check("after the winner collected")
408	uassert.Equal(t, int64(0), r.Locked()+r.Owed(), "and the realm owes nothing")
409}
410
411func mustGet(t *testing.T, r *Registry, key string) *Entry {
412	t.Helper()
413	e, ok := r.Get(key)
414	urequire.True(t, ok, "entry "+key+" exists")
415	return e
416}