registry_test.gno
75.25 Kb · 1566 lines
1package zones
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10)
11
12var (
13 alice = testutils.TestAddress("alice")
14 bob = testutils.TestAddress("bob")
15 curator = testutils.TestAddress("curator")
16 carol = testutils.TestAddress("carol")
17)
18
19func onyx() Info {
20 return Info{
21 ChainID: "onyx-1",
22 Title: "Onyx",
23 Description: "The gno.land testnet.",
24 Kind: Testnet,
25 GnowebURL: "https://onyx.testnets.gno.land",
26 RPCURL: "https://rpc.onyx.testnets.gno.land",
27 }
28}
29
30func TestProposeAndRead(t *testing.T) {
31 r := NewRegistry()
32 in := onyx()
33 in.Title = " Onyx "
34 uassert.NoError(t, r.Propose(alice, 10, "onyx", in))
35
36 z, ok := r.Zone("onyx")
37 uassert.True(t, ok)
38 uassert.Equal(t, "Onyx", z.Title) // trimmed
39 uassert.Equal(t, string(Pending), string(z.Status))
40 uassert.Equal(t, alice.String(), z.Proposer.String())
41 uassert.Equal(t, int64(10), z.ProposedAt)
42 uassert.True(t, !z.Reviewed())
43
44 _, ok = r.Zone("nope")
45 uassert.False(t, ok)
46
47 uassert.ErrorContains(t, r.Propose(bob, 11, "onyx", onyx()), "is taken")
48 uassert.ErrorContains(t, r.Propose(bob, 11, "On yx", onyx()), "slug")
49 uassert.ErrorContains(t, r.Propose("", 11, "other", onyx()), "proposer is not a valid lowercase address")
50 uassert.Equal(t, 1, r.Len())
51}
52
53func TestZoneCopiesAreCopies(t *testing.T) {
54 r := NewRegistry()
55 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
56 z, _ := r.Zone("onyx")
57 z.Title = "mutated"
58 z.Status = Approved
59 again, _ := r.Zone("onyx")
60 uassert.Equal(t, "Onyx", again.Title)
61 uassert.Equal(t, string(Pending), string(again.Status))
62}
63
64func TestPendingCapPerProposer(t *testing.T) {
65 r := NewRegistry()
66 for i := 0; i < MaxPendingPerProposer; i++ {
67 uassert.NoError(t, r.Propose(alice, 1, "z"+strconv.Itoa(i), onyx()))
68 }
69 uassert.ErrorContains(t, r.Propose(alice, 1, "one-more", onyx()), "pending proposals, the limit is")
70 // Somebody else is not blocked by alice's spam.
71 uassert.NoError(t, r.Propose(bob, 1, "bobs", onyx()))
72 // A reviewed proposal frees the slot.
73 uassert.NoError(t, r.ReviewZone("z0", Approved, rev(r, "z0"), curator, 2, ""))
74 uassert.NoError(t, r.Propose(alice, 3, "one-more", onyx()))
75 // So does a removed one.
76 uassert.NoError(t, r.RemoveZone("z1", rev(r, "z1")))
77 uassert.NoError(t, r.Propose(alice, 3, "and-another", onyx()))
78}
79
80func TestReviewTransitions(t *testing.T) {
81 cases := []struct {
82 from Status
83 to Status
84 reason string
85 err string
86 }{
87 {Pending, Approved, "", ""},
88 {Pending, Rejected, "duplicate of onyx", ""},
89 {Pending, Rejected, "", "needs a reason"},
90 {Pending, Rejected, "\u200b", "invisible or bidi"},
91 {Approved, Retired, "\u2066\u2069", "invisible or bidi"},
92 {Pending, Retired, "gone", "only an approved zone can be retired"},
93 {Pending, Pending, "", "already pending"},
94 {Approved, Retired, "the RPC stopped resolving", ""},
95 {Approved, Retired, " ", "needs a reason"},
96 {Approved, Rejected, "x", "retire it instead"},
97 {Approved, Approved, "", "already approved"},
98 {Rejected, Approved, "reconsidered", ""},
99 {Rejected, Retired, "x", "only an approved zone"},
100 {Retired, Approved, "it came back", ""},
101 {Retired, Rejected, "x", "only a pending zone can be rejected"},
102 {Pending, "official", "", "cannot set a zone"},
103 }
104 for _, c := range cases {
105 r := NewRegistry()
106 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
107 // Walk the zone to the starting state.
108 switch c.from {
109 case Approved:
110 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, ""))
111 case Rejected:
112 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 2, "setup"))
113 case Retired:
114 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, ""))
115 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 3, "setup"))
116 }
117 label := string(c.from) + " -> " + string(c.to)
118 err := r.ReviewZone("onyx", c.to, rev(r, "onyx"), curator, 9, c.reason)
119 z, _ := r.Zone("onyx")
120 if c.err != "" {
121 uassert.ErrorContains(t, err, c.err, label)
122 uassert.Equal(t, string(c.from), string(z.Status), label)
123 continue
124 }
125 uassert.NoError(t, err, label)
126 uassert.Equal(t, string(c.to), string(z.Status), label)
127 uassert.Equal(t, curator.String(), z.ReviewedBy.String(), label)
128 uassert.Equal(t, int64(9), z.ReviewedAt, label)
129 uassert.Equal(t, c.reason, z.Reason, label)
130 }
131}
132
133func TestEdit(t *testing.T) {
134 r := NewRegistry()
135 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
136 in := onyx()
137 in.GenesisURL = "https://github.com/gnolang/gno/releases/download/chain/onyx/genesis.json"
138 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, "", 2, ""), "editor is not a valid lowercase address")
139 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, alice, 2, ""))
140 z, _ := r.Zone("onyx")
141 uassert.Equal(t, in.GenesisURL, z.GenesisURL)
142 uassert.False(t, z.Reviewed()) // pending: just an edit
143
144 bad := in
145 bad.RPCURL = ""
146 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), bad, alice, 2, ""), "rpc url is empty")
147 uassert.ErrorContains(t, r.Edit("nope", rev(r, "nope"), in, alice, 2, ""), "no zone")
148 z, _ = r.Zone("onyx")
149 uassert.Equal(t, "https://rpc.onyx.testnets.gno.land", z.RPCURL) // unchanged by the refused edit
150
151 // Once reviewed, an edit is a decision: it needs a reason and an editor, and
152 // it replaces the review on record.
153 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, "probed"))
154 in.RPCURL = "https://rpc2.onyx.example.com"
155 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 4, ""), "an edit needs a reason")
156 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, "", 4, "moved"), "editor is not a valid lowercase address")
157 z, _ = r.Zone("onyx")
158 uassert.Equal(t, "https://rpc.onyx.testnets.gno.land", z.RPCURL)
159 uassert.Equal(t, "probed", z.Reason)
160 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, bob, 5, "rpc moved"))
161 z, _ = r.Zone("onyx")
162 uassert.Equal(t, "https://rpc2.onyx.example.com", z.RPCURL)
163 uassert.Equal(t, bob.String(), z.ReviewedBy.String())
164 uassert.Equal(t, int64(5), z.ReviewedAt)
165 uassert.Equal(t, "rpc moved", z.Reason)
166 uassert.Equal(t, string(Approved), string(z.Status))
167
168 // A retired zone is a record: its retirement reason is not editable away.
169 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 6, "testnet ended"))
170 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 7, "typo"), "only a pending or approved zone can be edited")
171 z, _ = r.Zone("onyx")
172 uassert.Equal(t, "testnet ended", z.Reason)
173 // Nor is a rejected one.
174 uassert.NoError(t, r.Propose(alice, 8, "nope", onyx()))
175 uassert.NoError(t, r.ReviewZone("nope", Rejected, rev(r, "nope"), curator, 9, "dup"))
176 uassert.ErrorContains(t, r.Edit("nope", rev(r, "nope"), onyx(), alice, 10, ""), "only a pending or approved zone can be edited")
177}
178
179// What was verified is that an endpoint answered for the zone's chain id, so
180// changing the chain id un-verifies them; changing only the RPC does not.
181func TestChainIDEditUnverifiesEndpoints(t *testing.T) {
182 r := NewRegistry()
183 uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
184 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
185 a, _ := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "")
186 b, _ := r.Register(alice, 2, "onyx", Peer, nodeID+"@b.example.com:26656", "")
187 f, _ := r.Register(alice, 2, "onyx", RPC, "https://f.example.com", "")
188 uassert.NoError(t, r.ReviewEndpoint(a, Verified, zr(r, a), erev(r, a), curator, 3, ""))
189 uassert.NoError(t, r.ReviewEndpoint(b, Verified, zr(r, b), erev(r, b), curator, 3, ""))
190 uassert.NoError(t, r.ReviewEndpoint(f, Flagged, zr(r, f), erev(r, f), curator, 3, "down"))
191
192 in := onyx()
193 in.RPCURL = "https://rpc2.onyx.example.com"
194 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 4, "rpc moved"))
195 uassert.Equal(t, 2, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
196
197 in.ChainID = "onyx-2"
198 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 5, "relaunched as onyx-2"))
199 uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
200 e, _ := r.Endpoint(a)
201 uassert.Equal(t, string(Unverified), string(e.Status))
202 uassert.Equal(t, ChainIDChanged, e.Reason)
203 uassert.Equal(t, int64(5), e.ReviewedAt)
204 e, _ = r.Endpoint(f)
205 uassert.Equal(t, string(Flagged), string(e.Status)) // a flag is not undone by an edit
206 uassert.Equal(t, "down", e.Reason)
207 // And the reset ones count against the review queue again (the flagged
208 // one has its verdict and stays out): verifying one frees it.
209 v, _ := r.Count("onyx")
210 uassert.Equal(t, 0, v)
211 uassert.Equal(t, 2, r.Awaiting("onyx"), "both reset endpoints are back in the queue")
212 uassert.NoError(t, r.ReviewEndpoint(a, Verified, zr(r, a), erev(r, a), curator, 6, "answers onyx-2"))
213 uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
214 v, _ = r.Count("onyx")
215 uassert.Equal(t, 1, v)
216
217 // Retiring resets what was verified, at the moment the network stops: a
218 // retired network's hosts may be somebody else's by the time anyone reads
219 // them. Approving it again does not restore anything.
220 uassert.NoError(t, r.ReviewEndpoint(b, Verified, zr(r, b), erev(r, b), curator, 6, ""))
221 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 6, "stopped"))
222 uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
223 e, _ = r.Endpoint(b)
224 uassert.Equal(t, ZoneRetired, e.Reason)
225 uassert.Equal(t, curator.String(), e.ReviewedBy.String())
226 uassert.ErrorContains(t, r.ReviewEndpoint(b, Verified, zr(r, b), erev(r, b), curator, 6, ""), "nothing on it can be verified")
227 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 7, "back up"))
228 uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
229 e, _ = r.Endpoint(f)
230 uassert.Equal(t, string(Flagged), string(e.Status))
231 // A pending zone is no exception: a proposer collecting verdicts for one
232 // chain id and then switching it gets them all reset.
233 uassert.NoError(t, r.Propose(alice, 7, "pend", onyx()))
234 pid, _ := r.Register(alice, 7, "pend", RPC, "https://p.example.com", "")
235 uassert.NoError(t, r.ReviewEndpoint(pid, Verified, zr(r, pid), erev(r, pid), curator, 8, ""))
236 other := onyx()
237 other.ChainID = "elsewhere-1"
238 uassert.NoError(t, r.Edit("pend", rev(r, "pend"), other, alice, 9, ""))
239 e, _ = r.Endpoint(pid)
240 uassert.Equal(t, string(Unverified), string(e.Status))
241 // The proposer's own edit caused the reset, and the proposer is not a
242 // reviewer, so none is recorded, and the reason says why.
243 uassert.Equal(t, "", e.ReviewedBy.String())
244 uassert.Equal(t, ChainIDChanged, e.Reason)
245 z, _ := r.Zone("pend")
246 uassert.False(t, z.Reviewed()) // still a pending edit: the zone's own review is untouched
247}
248
249func TestRegisterAndFilter(t *testing.T) {
250 r := NewRegistry()
251 uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
252 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
253
254 rpc, err := r.Register(alice, 5, "onyx", RPC, " https://rpc.onyx.testnets.gno.land ", "gno core")
255 uassert.NoError(t, err)
256 peer, err := r.Register(bob, 6, "onyx", Peer, nodeID+"@seed-1.onyx.testnets.gno.land:26656", "")
257 uassert.NoError(t, err)
258 uassert.True(t, rpc > 0 && peer > rpc)
259
260 e, ok := r.Endpoint(rpc)
261 uassert.True(t, ok)
262 uassert.Equal(t, rpc, e.ID)
263 uassert.Equal(t, "https://rpc.onyx.testnets.gno.land", e.Address) // trimmed
264 uassert.Equal(t, string(Unverified), string(e.Status))
265 uassert.Equal(t, alice.String(), e.Registrant.String())
266
267 // Dedup is case-insensitive: the same host under another spelling.
268 _, err = r.Register(bob, 7, "onyx", RPC, "https://RPC.onyx.testnets.gno.land", "")
269 uassert.ErrorContains(t, err, "already lists that rpc")
270 // ...but a path is case-sensitive, so another path is another endpoint.
271 _, err = r.Register(bob, 7, "onyx", Indexer, "https://indexer.example.com/API", "")
272 uassert.NoError(t, err)
273 _, err = r.Register(bob, 7, "onyx", Indexer, "https://INDEXER.example.com/API", "")
274 uassert.ErrorContains(t, err, "already lists that indexer")
275 _, err = r.Register(bob, 7, "onyx", Indexer, "https://indexer.example.com/api", "")
276 uassert.NoError(t, err)
277 // The same address under another kind is a different endpoint.
278 _, err = r.Register(bob, 7, "onyx", Gnoweb, "https://rpc.onyx.testnets.gno.land", "")
279 uassert.NoError(t, err)
280
281 _, err = r.Register(bob, 7, "nope", RPC, "https://x.y", "")
282 uassert.ErrorContains(t, err, "no zone")
283 _, err = r.Register(bob, 7, "onyx", Peer, "https://x.y", "")
284 uassert.ErrorContains(t, err, "<node id>@")
285 _, err = r.Register(bob, 7, "onyx", RPC, "https://x.y", "two\nlines")
286 uassert.ErrorContains(t, err, "control character")
287
288 uassert.Equal(t, 5, len(r.Endpoints(EndpointFilter{Zone: "onyx"})))
289 uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "onyx", Kind: Peer})))
290 uassert.Equal(t, 4, len(r.Endpoints(EndpointFilter{Registrant: bob})))
291 uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})))
292
293 uassert.NoError(t, r.ReviewEndpoint(rpc, Verified, zr(r, rpc), erev(r, rpc), curator, 8, "answers onyx-1"))
294 vs := r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified})
295 uassert.Equal(t, 1, len(vs))
296 uassert.Equal(t, "answers onyx-1", vs[0].Reason)
297 v, total := r.Count("onyx")
298 uassert.Equal(t, 1, v)
299 uassert.Equal(t, 5, total)
300
301 uassert.ErrorContains(t, r.ReviewEndpoint(rpc, Verified, zr(r, rpc), erev(r, rpc), curator, 8, "answers onyx-1"), "already verified")
302 uassert.ErrorContains(t, r.ReviewEndpoint(peer, Flagged, zr(r, peer), erev(r, peer), curator, 8, ""), "needs a reason")
303 uassert.ErrorContains(t, r.ReviewEndpoint(peer, "trusted", zr(r, peer), erev(r, peer), curator, 8, ""), "unknown verification")
304 uassert.ErrorContains(t, r.ReviewEndpoint(peer, "", zr(r, peer), erev(r, peer), curator, 8, ""), "needs a verdict")
305 uassert.ErrorContains(t, r.ReviewEndpoint(peer, " verified ", zr(r, peer), erev(r, peer), curator, 8, ""), "unknown verification")
306 _, err = r.Register(bob, 7, "onyx", " rpc ", "not a url", "")
307 uassert.ErrorContains(t, err, "unknown endpoint kind")
308 // A scheme's case is not a different endpoint.
309 _, err = r.Register(bob, 7, "onyx", Explorer, "HTTPS://Explorer.example.com/x", "")
310 uassert.NoError(t, err)
311 _, err = r.Register(bob, 7, "onyx", Explorer, "https://explorer.example.com/x", "")
312 uassert.ErrorContains(t, err, "already lists that explorer")
313 uassert.ErrorContains(t, r.ReviewEndpoint(999, Verified, zr(r, 999), erev(r, 999), curator, 8, ""), "no endpoint #999")
314 uassert.ErrorContains(t, r.ReviewEndpoint(0, Verified, zr(r, 0), erev(r, 0), curator, 8, ""), "no endpoint #0")
315 uassert.NoError(t, r.ReviewEndpoint(peer, Flagged, zr(r, peer), erev(r, peer), curator, 9, "wrong chain id"))
316 uassert.NoError(t, r.ReviewEndpoint(peer, Unverified, zr(r, peer), erev(r, peer), curator, 10, "fixed, re-checking"))
317
318 // Removing frees the dedup key, so the same address can come back.
319 uassert.NoError(t, r.RemoveEndpoint(rpc, erev(r, rpc)))
320 _, ok = r.Endpoint(rpc)
321 uassert.False(t, ok)
322 uassert.ErrorContains(t, r.RemoveEndpoint(rpc, erev(r, rpc)), "no endpoint")
323 _, err = r.Register(bob, 11, "onyx", RPC, "https://rpc.onyx.testnets.gno.land", "")
324 uassert.NoError(t, err)
325}
326
327func TestRegisterNeedsALiveZone(t *testing.T) {
328 r := NewRegistry()
329 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
330 // Pending takes endpoints: a proposer fills in the peers before review.
331 _, err := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "")
332 uassert.NoError(t, err)
333 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 3, "not a gno chain"))
334 _, err = r.Register(alice, 4, "onyx", RPC, "https://b.example.com", "")
335 uassert.ErrorContains(t, err, "is rejected and takes no endpoints")
336}
337
338func TestEndpointCapPerAddress(t *testing.T) {
339 r := NewRegistry()
340 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
341 for i := 0; i < MaxEndpointsPerAddress; i++ {
342 _, err := r.Register(alice, 2, "onyx", RPC, "https://n"+strconv.Itoa(i)+".example.com", "")
343 uassert.NoError(t, err)
344 }
345 _, err := r.Register(alice, 2, "onyx", RPC, "https://over.example.com", "")
346 uassert.ErrorContains(t, err, "the limit is")
347 // Bob still can.
348 _, err = r.Register(bob, 2, "onyx", RPC, "https://over.example.com", "")
349 uassert.NoError(t, err)
350}
351
352func TestRemoveZone(t *testing.T) {
353 r := NewRegistry()
354 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
355 _, err := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
356 uassert.NoError(t, err)
357 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
358 uassert.ErrorContains(t, r.RemoveZone("onyx", rev(r, "onyx")), "retire it instead")
359 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 4, "testnet ended"))
360 uassert.ErrorContains(t, r.RemoveZone("onyx", rev(r, "onyx")), "kept on record")
361 _, ok := r.Zone("onyx")
362 uassert.True(t, ok)
363
364 // A rejected zone is removable, endpoints and all.
365 uassert.NoError(t, r.Propose(alice, 5, "spam", onyx()))
366 _, err = r.Register(bob, 5, "spam", RPC, "https://a.example.com", "")
367 uassert.NoError(t, err)
368 uassert.NoError(t, r.ReviewZone("spam", Rejected, rev(r, "spam"), curator, 6, "not a network"))
369 uassert.NoError(t, r.RemoveZone("spam", rev(r, "spam")))
370 _, ok = r.Zone("spam")
371 uassert.False(t, ok)
372 uassert.Equal(t, 0, len(r.Endpoints(EndpointFilter{Zone: "spam"})))
373 uassert.Equal(t, 1, r.Len())
374 uassert.ErrorContains(t, r.RemoveZone("spam", rev(r, "spam")), "no zone")
375 // The slug and the address are free again.
376 uassert.NoError(t, r.Propose(bob, 7, "spam", onyx()))
377 _, err = r.Register(bob, 8, "spam", RPC, "https://a.example.com", "")
378 uassert.NoError(t, err)
379}
380
381func TestReviewNeedsAReviewer(t *testing.T) {
382 r := NewRegistry()
383 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
384 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), "", 2, ""), "reviewer is not a valid lowercase address")
385 z, _ := r.Zone("onyx")
386 uassert.Equal(t, string(Pending), string(z.Status))
387 id, err := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "")
388 uassert.NoError(t, err)
389 uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), "", 3, ""), "reviewer is not a valid lowercase address")
390}
391
392// A flood from many addresses fills the review queue and nothing above it.
393func TestFloodCannotCrowdOutReviewedEntries(t *testing.T) {
394 r := NewRegistry()
395 uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
396 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
397 for i := 0; i < MaxPending; i++ {
398 uassert.NoError(t, r.Propose(testAddr(i), 2, "flood-"+strconv.Itoa(i), onyx()))
399 }
400 uassert.ErrorContains(t, r.Propose(bob, 3, "honest", onyx()), "waiting for review")
401 uassert.Equal(t, MaxPending+1, r.Len())
402 // Clearing one makes room.
403 uassert.NoError(t, r.ReviewZone("flood-0", Rejected, rev(r, "flood-0"), curator, 4, "spam"))
404 uassert.NoError(t, r.Propose(bob, 5, "honest", onyx()))
405
406 // Endpoints: those waiting for a verdict have their own gate, and verifying frees it.
407 ids := []int64{}
408 for i := 0; i < MaxUnverifiedPerZone; i++ {
409 id, err := r.Register(testAddr(i/MaxEndpointsPerAddress), 6, "onyx", RPC, "https://n"+strconv.Itoa(i)+".example.com", "")
410 uassert.NoError(t, err)
411 ids = append(ids, id)
412 }
413 _, err := r.Register(bob, 7, "onyx", RPC, "https://honest.example.com", "")
414 uassert.ErrorContains(t, err, "waiting for review")
415 uassert.NoError(t, r.ReviewEndpoint(ids[0], Verified, zr(r, ids[0]), erev(r, ids[0]), curator, 8, ""))
416 _, err = r.Register(bob, 9, "onyx", RPC, "https://honest.example.com", "")
417 uassert.NoError(t, err)
418 // The gate is full again (the honest one is waiting). A flag is a verdict,
419 // so a flagged endpoint leaves the queue: curators keep the warning and
420 // still make room, instead of deleting the warning to make room.
421 _, err = r.Register(bob, 11, "onyx", RPC, "https://honest2.example.com", "")
422 uassert.ErrorContains(t, err, "waiting for review")
423 uassert.NoError(t, r.ReviewEndpoint(ids[1], Flagged, zr(r, ids[1]), erev(r, ids[1]), curator, 12, "spam"))
424 _, err = r.Register(bob, 13, "onyx", RPC, "https://honest2.example.com", "")
425 uassert.NoError(t, err)
426 // Unflagging puts it back in the queue, and a review is never refused for
427 // the queue being full, only a registration is.
428 uassert.NoError(t, r.ReviewEndpoint(ids[1], Unverified, zr(r, ids[1]), erev(r, ids[1]), curator, 14, "re-checking"))
429 _, err = r.Register(bob, 15, "onyx", RPC, "https://honest3.example.com", "")
430 uassert.ErrorContains(t, err, "waiting for review")
431 uassert.NoError(t, r.RemoveEndpoint(ids[2], erev(r, ids[2])))
432 uassert.NoError(t, r.RemoveEndpoint(ids[3], erev(r, ids[3])))
433 _, err = r.Register(bob, 16, "onyx", RPC, "https://honest3.example.com", "")
434 uassert.NoError(t, err)
435}
436
437func testAddr(i int) address { return testutils.TestAddress("flood" + strconv.Itoa(i)) }
438
439func TestZonesFilterAndOrder(t *testing.T) {
440 r := NewRegistry()
441 mainnet := onyx()
442 mainnet.ChainID, mainnet.Kind = "gnoland-1", Mainnet
443 uassert.NoError(t, r.Propose(curator, 1, "mainnet", mainnet))
444 uassert.NoError(t, r.Propose(curator, 1, "onyx", onyx()))
445 uassert.NoError(t, r.Propose(alice, 2, "alices", onyx()))
446 uassert.NoError(t, r.ReviewZone("mainnet", Approved, rev(r, "mainnet"), curator, 3, ""))
447 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
448
449 slugs := func(zs []Zone) string {
450 s := ""
451 for _, z := range zs {
452 s += z.Slug + " "
453 }
454 return s
455 }
456 uassert.Equal(t, "mainnet onyx alices ", slugs(r.Zones(ZoneFilter{})))
457 uassert.Equal(t, "mainnet onyx ", slugs(r.Zones(ZoneFilter{Status: Approved})))
458 uassert.Equal(t, "alices ", slugs(r.Zones(ZoneFilter{Status: Pending})))
459 uassert.Equal(t, "onyx alices ", slugs(r.Zones(ZoneFilter{Kind: Testnet})))
460 uassert.Equal(t, "onyx ", slugs(r.Zones(ZoneFilter{Status: Approved, Kind: Testnet})))
461 uassert.Equal(t, "", slugs(r.Zones(ZoneFilter{Kind: Local})))
462}
463
464func TestPagesAndCounts(t *testing.T) {
465 r := NewRegistry()
466 for i := 0; i < 7; i++ {
467 uassert.NoError(t, r.Propose(testAddr(i), 1, "z"+strconv.Itoa(i), onyx()))
468 }
469 uassert.NoError(t, r.ReviewZone("z1", Approved, rev(r, "z1"), curator, 2, ""))
470 uassert.NoError(t, r.ReviewZone("z3", Approved, rev(r, "z3"), curator, 2, ""))
471 uassert.NoError(t, r.ReviewZone("z5", Approved, rev(r, "z5"), curator, 2, ""))
472 uassert.Equal(t, 3, r.ZoneCount(Approved))
473 uassert.Equal(t, 4, r.ZoneCount(Pending))
474 uassert.Equal(t, 7, r.ZoneCount(""))
475
476 page := func(zs []Zone) string {
477 out := ""
478 for _, z := range zs {
479 out += z.Slug + " "
480 }
481 return out
482 }
483 uassert.Equal(t, "z1 z3 ", page(r.ZonePage(Approved, 1, 2)))
484 uassert.Equal(t, "z5 ", page(r.ZonePage(Approved, 2, 2)))
485 uassert.Equal(t, "", page(r.ZonePage(Approved, 3, 2)))
486 uassert.Equal(t, "", page(r.ZonePage(Approved, 0, 2)))
487 uassert.Equal(t, "", page(r.ZonePage(Approved, 1, 0)))
488 uassert.Equal(t, "", page(r.ZonePage(Approved, 1<<62, 1<<10)), "no overflow into a small offset")
489 uassert.Equal(t, "z0 z2 z4 z6 ", page(r.ZonePage(Pending, 1, 10)))
490 // "" is every status, consistent with ZoneCount("") and ZoneFilter{}.
491 uassert.Equal(t, "z0 z1 z2 ", page(r.ZonePage("", 1, 3)))
492 uassert.Equal(t, "z6 ", page(r.ZonePage("", 3, 3)))
493 uassert.Equal(t, "", page(r.ZonePage("", 4, 3)))
494 uassert.Equal(t, "", page(r.ZonePage("", 0, 3)))
495 uassert.Equal(t, "", page(r.ZonePage("", 1<<62, 1<<10)), "no overflow into a small offset")
496 uassert.Equal(t, "z0 z1 z2 z3 z4 z5 z6 ", page(r.ZonePage("", 1, 1<<40)))
497 // The status index follows a review and a removal.
498 uassert.NoError(t, r.ReviewZone("z0", Rejected, rev(r, "z0"), curator, 3, "dup"))
499 uassert.NoError(t, r.RemoveZone("z2", rev(r, "z2")))
500 uassert.Equal(t, "z4 z6 ", page(r.ZonePage(Pending, 1, 10)))
501 uassert.Equal(t, "z0 ", page(r.ZonePage(Rejected, 1, 10)))
502 uassert.Equal(t, "z1 z3 z5 ", page(r.Zones(ZoneFilter{Status: Approved})))
503
504 // Chain ids are not unique, only approved zones are indexed by one (so a
505 // proposal naming a real chain id costs a reader nothing), and the index
506 // follows an edit and a retirement.
507 uassert.Equal(t, "z1 z3 z5 ", page(r.ApprovedByChainID("onyx-1")))
508 in := onyx()
509 in.ChainID = "onyx-2"
510 uassert.NoError(t, r.Edit("z1", rev(r, "z1"), in, curator, 4, "relaunched"))
511 uassert.Equal(t, "z1 ", page(r.ApprovedByChainID("onyx-2")))
512 uassert.Equal(t, "z3 z5 ", page(r.ApprovedByChainID("onyx-1")))
513 uassert.NoError(t, r.Edit("z4", rev(r, "z4"), in, alice, 4, "")) // pending: not indexed
514 uassert.Equal(t, "z1 ", page(r.ApprovedByChainID("onyx-2")))
515 uassert.NoError(t, r.ReviewZone("z5", Retired, rev(r, "z5"), curator, 4, "gone"))
516 uassert.Equal(t, "z3 ", page(r.ApprovedByChainID("onyx-1")))
517
518 // Endpoints: counts from the indexes, pages by kind.
519 for i := 0; i < 5; i++ {
520 _, err := r.Register(alice, 5, "z3", RPC, "https://e"+strconv.Itoa(i)+".example.com", "")
521 uassert.NoError(t, err)
522 }
523 pid, err := r.Register(alice, 5, "z3", Peer, nodeID+"@p.example.com:26656", "")
524 uassert.NoError(t, err)
525 uassert.NoError(t, r.ReviewEndpoint(pid, Verified, zr(r, pid), erev(r, pid), curator, 6, ""))
526 v, total := r.Count("z3")
527 uassert.Equal(t, 1, v)
528 uassert.Equal(t, 6, total)
529 uassert.Equal(t, 5, r.EndpointCount("z3", RPC))
530 uassert.Equal(t, 1, r.EndpointCount("z3", Peer))
531 uassert.Equal(t, 0, r.EndpointCount("z3", Faucet))
532 uassert.Equal(t, 6, r.EndpointCount("z3", ""))
533 uassert.Equal(t, 1, len(r.EndpointPage("z3", RPC, 3, 2))) // page 3 of 5 rpcs at 2 a page holds one
534 uassert.Equal(t, "https://e4.example.com", r.EndpointPage("z3", RPC, 3, 2)[0].Address)
535 uassert.Equal(t, nodeID+"@p.example.com:26656", r.EndpointPage("z3", Peer, 1, 10)[0].Address)
536 uassert.Equal(t, 6, len(r.EndpointPage("z3", "", 1, 10)))
537 uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "z3", Kind: Peer})))
538 uassert.NoError(t, r.RemoveEndpoint(pid, erev(r, pid)))
539 uassert.Equal(t, 0, r.EndpointCount("z3", Peer))
540 v, total = r.Count("z3")
541 uassert.Equal(t, 0, v)
542 uassert.Equal(t, 5, total)
543}
544
545// rev is a zone's current revision, what a curator who just read it approves.
546func rev(r *Registry, slug string) int64 {
547 z, _ := r.Zone(slug)
548 return z.Revision
549}
550
551// erev is an endpoint's revision, what a verdict or a removal names.
552func erev(r *Registry, id int64) int64 {
553 e, ok := r.Endpoint(id)
554 if !ok {
555 return 0
556 }
557 return e.Revision
558}
559
560// zr is the revision of an endpoint's zone, what a verification also names.
561func zr(r *Registry, id int64) int64 {
562 e, ok := r.Endpoint(id)
563 if !ok {
564 return 0
565 }
566 return rev(r, e.Zone)
567}
568
569// An approval names the revision the curator read. An edit landing between the
570// reading and the approval makes the approval fail, rather than making text the
571// curator never saw official under their name. Remove-and-propose-again cannot
572// reuse a revision either.
573func TestApprovalBindsToRevision(t *testing.T) {
574 r := NewRegistry()
575 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
576 read := rev(r, "onyx")
577
578 swapped := onyx()
579 swapped.RPCURL = "https://attacker.example.com"
580 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), swapped, alice, 2, ""))
581 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, read, curator, 3, ""), "changed since you read it")
582 z, _ := r.Zone("onyx")
583 uassert.Equal(t, string(Pending), string(z.Status))
584 uassert.Equal(t, alice.String(), z.EditedBy.String())
585 uassert.Equal(t, int64(2), z.EditedAt)
586
587 // Removed and proposed again under the same slug: a new revision, never the old one.
588 uassert.NoError(t, r.RemoveZone("onyx", rev(r, "onyx")))
589 uassert.NoError(t, r.Propose(alice, 4, "onyx", onyx()))
590 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, read, curator, 5, ""), "changed since you read it")
591 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 5, ""))
592
593 // Verifying an endpoint binds the same way: to the chain id it was checked against.
594 id, err := r.Register(alice, 6, "onyx", RPC, "https://a.example.com", "")
595 uassert.NoError(t, err)
596 checked := zr(r, id)
597 moved := onyx()
598 moved.ChainID = "onyx-2"
599 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, curator, 7, "relaunch"))
600 uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, checked, erev(r, id), curator, 8, ""), "changed since you checked it against it")
601 // A revision from the future is as wrong as a past one.
602 uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id)+1, erev(r, id), curator, 8, ""), "changed since")
603 uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id)+1, curator, 8, ""), "changed since")
604 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 8, ""))
605 // Every verdict binds to the endpoint as read, too: a verify written before
606 // another curator's flag fails rather than silently reversing it.
607 read = erev(r, id)
608 uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), read, curator, 9, "down"))
609 uassert.ErrorContains(t, r.ReviewEndpoint(id, Unverified, zr(r, id), read, alice, 10, ""), "changed since you checked it")
610 // The same verdict again, with a new reason, restates it.
611 uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 11, "down since block 9"))
612 uassert.ErrorContains(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 12, "down since block 9"), "restating it needs a new reason")
613}
614
615func TestPendingEditTakesNoReason(t *testing.T) {
616 r := NewRegistry()
617 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
618 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), alice, 2, "fixed a typo"), "takes no reason")
619 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
620 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), curator, 4, "\u200b"), "invisible")
621 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), curator, 4, ""), "needs a reason")
622 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), onyx(), curator, 4, "no change"), "changes nothing")
623 in := onyx()
624 in.Title = "Onyx testnet"
625 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), in, curator, 4, "retitled"))
626}
627
628// Rejected and retired zones are kept for the record, each up to a cap of its
629// own, and neither counts against the live registry: nothing a proposer or a
630// curator does, and no amount of time, fills it for good.
631func TestRecordsNeverFillTheRegistry(t *testing.T) {
632 r := NewRegistry()
633 for i := 0; i < MaxRejected+3; i++ {
634 slug := "rej" + strconv.Itoa(i)
635 uassert.NoError(t, r.Propose(testAddr(i), 1, slug, onyx()))
636 uassert.NoError(t, r.ReviewZone(slug, Rejected, rev(r, slug), curator, 2, "spam"))
637 }
638 uassert.Equal(t, MaxRejected, r.ZoneCount(Rejected))
639 // The three that entered the rejected state first made room.
640 _, ok := r.Zone("rej0")
641 uassert.False(t, ok)
642 _, ok = r.Zone("rej3")
643 uassert.True(t, ok)
644 uassert.Equal(t, 0, r.Live())
645
646 for i := 0; i < MaxRetired+2; i++ {
647 slug := "ret" + strconv.Itoa(i)
648 uassert.NoError(t, r.Propose(curator, 3, slug, onyx()))
649 uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 3, ""))
650 uassert.NoError(t, r.ReviewZone(slug, Retired, rev(r, slug), curator, 4, "stopped"))
651 }
652 uassert.Equal(t, MaxRetired, r.ZoneCount(Retired))
653 _, ok = r.Zone("ret0")
654 uassert.False(t, ok)
655 uassert.Equal(t, 0, r.Live())
656 uassert.Equal(t, MaxRejected+MaxRetired, r.Len())
657
658 // The live registry is untouched by all of it.
659 uassert.NoError(t, r.Propose(bob, 5, "honest", onyx()))
660 uassert.Equal(t, 1, r.Live())
661}
662
663// An evicted zone takes its endpoints and every index entry with it, so its
664// slug and its addresses can be used again.
665func TestEvictionUnwindsEverything(t *testing.T) {
666 r := NewRegistry()
667 uassert.NoError(t, r.Propose(alice, 1, "first", onyx()))
668 _, err := r.Register(alice, 1, "first", RPC, "https://f.example.com", "")
669 uassert.NoError(t, err)
670 fl, err := r.Register(alice, 1, "first", RPC, "https://flagged.example.com", "")
671 uassert.NoError(t, err)
672 uassert.NoError(t, r.ReviewEndpoint(fl, Flagged, zr(r, fl), erev(r, fl), curator, 1, "spam"))
673 uassert.NoError(t, r.ReviewZone("first", Rejected, rev(r, "first"), curator, 2, "dup"))
674 for i := 0; i < MaxRejected; i++ {
675 slug := "rej" + strconv.Itoa(i)
676 uassert.NoError(t, r.Propose(testAddr(i), 3, slug, onyx()))
677 uassert.NoError(t, r.ReviewZone(slug, Rejected, rev(r, slug), curator, 4, "spam"))
678 }
679 _, ok := r.Zone("first")
680 uassert.False(t, ok)
681 _, total := r.Count("first")
682 uassert.Equal(t, 0, total)
683 uassert.Equal(t, 0, r.OwnerCount("first", alice))
684 uassert.Equal(t, 0, r.EndpointCount("first", RPC))
685 uassert.NoError(t, r.Propose(alice, 5, "first", onyx()))
686 // Neither the review queue nor the flag count of the evicted zone carries
687 // over to the slug proposed again.
688 uassert.Equal(t, 0, r.Awaiting("first"))
689 _, err = r.Register(alice, 5, "first", RPC, "https://f.example.com", "")
690 uassert.NoError(t, err)
691 uassert.Equal(t, 1, r.Awaiting("first"))
692}
693
694// Approving a rejected or retired zone back into the live registry respects
695// its cap.
696func TestReviveRespectsTheLiveCap(t *testing.T) {
697 r := NewRegistry()
698 uassert.NoError(t, r.Propose(alice, 1, "back", onyx()))
699 uassert.NoError(t, r.ReviewZone("back", Rejected, rev(r, "back"), curator, 1, "not yet"))
700 for i := 0; i < MaxZones; i++ {
701 slug := "z" + strconv.Itoa(i)
702 uassert.NoError(t, r.ProposeExempt(curator, 2, slug, onyx()))
703 uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 2, ""))
704 }
705 uassert.Equal(t, MaxZones, r.Live())
706 uassert.ErrorContains(t, r.ReviewZone("back", Approved, rev(r, "back"), curator, 3, ""), "full at")
707 uassert.ErrorContains(t, r.Propose(bob, 3, "more", onyx()), "full at")
708 uassert.NoError(t, r.ReviewZone("z0", Retired, rev(r, "z0"), curator, 3, "stopped"))
709 uassert.NoError(t, r.ReviewZone("back", Approved, rev(r, "back"), curator, 4, ""))
710}
711
712func TestUppercaseAddressesAreRefused(t *testing.T) {
713 r := NewRegistry()
714 upper := address(strings.ToUpper(alice.String()))
715 uassert.True(t, upper.IsValid(), "bech32 itself decodes the uppercase form")
716 uassert.False(t, ValidAddress(upper))
717 uassert.ErrorContains(t, r.Propose(upper, 1, "onyx", onyx()), "lowercase")
718 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
719 _, err := r.Register(upper, 1, "onyx", RPC, "https://a.example.com", "")
720 uassert.ErrorContains(t, err, "lowercase")
721 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), upper, 2, ""), "lowercase")
722}
723
724// A peer is stored lowercased whole, so the listed form is the one tm2 dials,
725// and two spellings of one host are one peer.
726func TestPeerStoredLowercase(t *testing.T) {
727 r := NewRegistry()
728 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
729 id, err := r.Register(alice, 1, "onyx", Peer, nodeID+"@SEED-1.Onyx.example.com:26656", "")
730 uassert.NoError(t, err)
731 e, _ := r.Endpoint(id)
732 uassert.Equal(t, nodeID+"@seed-1.onyx.example.com:26656", e.Address)
733 _, err = r.Register(alice, 1, "onyx", Peer, nodeID+"@seed-1.onyx.example.com:26656", "")
734 uassert.ErrorContains(t, err, "already lists that peer")
735 got, ok := r.EndpointByAddress("onyx", Peer, nodeID+"@Seed-1.onyx.example.com:26656")
736 uassert.True(t, ok)
737 uassert.Equal(t, id, got.ID)
738}
739
740// Rejecting sends what was verified on the proposal back to unverified: a
741// rejected zone was never vouched for, so nothing on it reads as checked.
742func TestRejectUnverifies(t *testing.T) {
743 r := NewRegistry()
744 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
745 id, err := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
746 uassert.NoError(t, err)
747 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 2, ""))
748 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 3, "not a gno chain"))
749 e, _ := r.Endpoint(id)
750 uassert.Equal(t, string(Unverified), string(e.Status))
751 uassert.Equal(t, ZoneRejected, e.Reason)
752 uassert.Equal(t, curator.String(), e.ReviewedBy.String())
753 v, _ := r.Count("onyx")
754 uassert.Equal(t, 0, v)
755}
756
757// Eviction goes by when a zone entered its state, not when it was proposed: a
758// long-lived network retired today is not the next record to go.
759func TestEvictionIsByEntryOrder(t *testing.T) {
760 r := NewRegistry()
761 uassert.NoError(t, r.Propose(curator, 1, "mainnet", onyx())) // proposed first
762 uassert.NoError(t, r.ReviewZone("mainnet", Approved, rev(r, "mainnet"), curator, 1, ""))
763 for i := 0; i < MaxRetired; i++ {
764 slug := "ret" + strconv.Itoa(i)
765 uassert.NoError(t, r.Propose(curator, 2, slug, onyx()))
766 uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 2, ""))
767 uassert.NoError(t, r.ReviewZone(slug, Retired, rev(r, slug), curator, 3, "stopped"))
768 }
769 // Retiring mainnet now drops ret0, the zone retired longest ago.
770 uassert.NoError(t, r.ReviewZone("mainnet", Retired, rev(r, "mainnet"), curator, 4, "end of an era"))
771 _, ok := r.Zone("ret0")
772 uassert.False(t, ok)
773 // And the next retirement drops ret1, not mainnet.
774 uassert.NoError(t, r.Propose(curator, 5, "next", onyx()))
775 uassert.NoError(t, r.ReviewZone("next", Approved, rev(r, "next"), curator, 5, ""))
776 uassert.NoError(t, r.ReviewZone("next", Retired, rev(r, "next"), curator, 6, "stopped"))
777 _, ok = r.Zone("mainnet")
778 uassert.True(t, ok)
779 _, ok = r.Zone("ret1")
780 uassert.False(t, ok)
781}
782
783// Every decision on a zone names the revision it was made on.
784func TestEveryZoneDecisionIsRevisionBound(t *testing.T) {
785 r := NewRegistry()
786 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
787 old := rev(r, "onyx")
788 edited := onyx()
789 edited.Title = "Onyx, edited"
790 uassert.NoError(t, r.Edit("onyx", old, edited, alice, 2, ""))
791 uassert.ErrorContains(t, r.Edit("onyx", old, onyx(), curator, 3, ""), "changed since you read it")
792 uassert.ErrorContains(t, r.ReviewZone("onyx", Rejected, old, curator, 3, "spam"), "changed since you read it")
793 uassert.ErrorContains(t, r.RemoveZone("onyx", old), "changed since you read it")
794 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 3, ""))
795 uassert.ErrorContains(t, r.ReviewZone("onyx", Retired, old, curator, 4, "gone"), "changed since you read it")
796 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 4, "gone"))
797}
798
799func TestCanonicalHostAndTextEdgeCases(t *testing.T) {
800 r := NewRegistry()
801 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
802 // A peer's host loses its terminal dot, so the two spellings are one peer.
803 _, err := r.Register(alice, 1, "onyx", Peer, nodeID+"@seed-1.onyx.example.com:26656", "")
804 uassert.NoError(t, err)
805 _, err = r.Register(alice, 1, "onyx", Peer, nodeID+"@seed-1.onyx.example.com.:26656", "")
806 uassert.ErrorContains(t, err, "already lists that peer")
807 // An IPv4 host with a terminal dot is refused: Go's dialer cannot use it.
808 uassert.ErrorContains(t, ValidateEndpoint(RPC, "http://1.2.3.4.:26657"), "DNS name or an IPv4")
809 uassert.ErrorContains(t, ValidateEndpoint(Peer, nodeID+"@1.2.3.4.:26656"), "DNS name or an IPv4")
810 // A loopback or private host is a local zone's only.
811 _, err = r.Register(alice, 1, "onyx", RPC, "http://127.0.0.1:26657", "")
812 uassert.ErrorContains(t, err, "private or special-use")
813 _, err = r.Register(alice, 1, "onyx", Peer, nodeID+"@10.1.2.3:26656", "")
814 uassert.ErrorContains(t, err, "private or special-use")
815 local := onyx()
816 local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
817 uassert.NoError(t, r.Propose(alice, 1, "mine", local))
818 _, err = r.Register(alice, 1, "mine", Peer, nodeID+"@127.0.0.1:26656", "")
819 uassert.NoError(t, err)
820 // Equivalent URL spellings are one endpoint.
821 for _, pair := range [][2]string{
822 {"https://h.example.com/path", "https://h.example.com/path?"},
823 {"https://h.example.com?a=1", "https://h.example.com/?a=1"},
824 {"https://h.example.com/%2F", "https://h.example.com/%2f"},
825 } {
826 uassert.Equal(t, Canonical(Indexer, pair[0]), Canonical(Indexer, pair[1]), pair[0])
827 }
828 uassert.ErrorContains(t, ValidateEndpoint(Indexer, "https://h.example.com/%7e"), "needs no escaping")
829 uassert.ErrorContains(t, ValidateEndpoint(Indexer, "https://h.example.com/%41"), "needs no escaping")
830 // Look-alike badges and characters that draw nothing.
831 in := onyx()
832 for _, title := range []string{"✔ official", "☑ mainnet", "\U0001D159", "main\U000E0100net", "x\ue000", "\u180b", "\u0378"} {
833 in.Title = title
834 uassert.Error(t, ValidateInfo(in), title)
835 }
836}
837
838// The per-proposer and per-registrant caps are counts, and they follow every
839// way a zone or an endpoint leaves.
840func TestCountersFollowRemovals(t *testing.T) {
841 r := NewRegistry()
842 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
843 a, _ := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
844 _, _ = r.Register(alice, 1, "onyx", RPC, "https://b.example.com", "")
845 uassert.Equal(t, 2, r.OwnerCount("onyx", alice))
846 uassert.NoError(t, r.RemoveEndpoint(a, erev(r, a)))
847 uassert.Equal(t, 1, r.OwnerCount("onyx", alice))
848 uassert.NoError(t, r.RemoveZone("onyx", rev(r, "onyx")))
849 uassert.Equal(t, 0, r.OwnerCount("onyx", alice))
850 for i := 0; i < MaxPendingPerProposer; i++ {
851 uassert.NoError(t, r.Propose(alice, 2, "p"+strconv.Itoa(i), onyx()))
852 }
853 uassert.ErrorContains(t, r.Propose(alice, 2, "over", onyx()), "pending proposals")
854 uassert.NoError(t, r.ReviewZone("p0", Approved, rev(r, "p0"), curator, 3, ""))
855 uassert.NoError(t, r.Propose(alice, 3, "over", onyx()))
856}
857
858func TestSoleApproved(t *testing.T) {
859 r := NewRegistry()
860 uassert.NoError(t, r.Propose(curator, 1, "za", onyx()))
861 _, ok := r.SoleApproved("onyx-1")
862 uassert.False(t, ok, "pending is not approved")
863 uassert.NoError(t, r.ReviewZone("za", Approved, rev(r, "za"), curator, 1, ""))
864 z, ok := r.SoleApproved("onyx-1")
865 uassert.True(t, ok)
866 uassert.Equal(t, "za", z.Slug)
867 uassert.NoError(t, r.Propose(curator, 1, "zb", onyx()))
868 uassert.NoError(t, r.ReviewZone("zb", Approved, rev(r, "zb"), curator, 1, ""))
869 _, ok = r.SoleApproved("onyx-1")
870 uassert.False(t, ok, "two approved zones share it: no guess")
871}
872
873// A status change bumps the revision too: an approval prepared before a
874// colleague's rejection fails instead of quietly reversing it.
875func TestStatusChangeBumpsTheRevision(t *testing.T) {
876 r := NewRegistry()
877 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
878 read := rev(r, "onyx")
879 uassert.NoError(t, r.ReviewZone("onyx", Rejected, read, bob, 2, "phishing RPC"))
880 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, read, curator, 3, ""), "changed since you read it")
881 z, _ := r.Zone("onyx")
882 uassert.Equal(t, "phishing RPC", z.Reason)
883}
884
885// Leaving the local kind drops the private endpoints in the same edit, so
886// nobody can block the edit by registering one again after each removal.
887func TestLeavingLocalDropsPrivateEndpoints(t *testing.T) {
888 r := NewRegistry()
889 local := onyx()
890 local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
891 uassert.NoError(t, r.Propose(alice, 1, "mine", local))
892 priv, err := r.Register(alice, 1, "mine", Peer, nodeID+"@10.0.0.5:26656", "")
893 uassert.NoError(t, err)
894 flagged, err := r.Register(bob, 1, "mine", RPC, "http://localhost:1", "")
895 uassert.NoError(t, err)
896 uassert.NoError(t, r.ReviewEndpoint(flagged, Flagged, zr(r, flagged), erev(r, flagged), curator, 1, "squatting"))
897 pub, err := r.Register(alice, 1, "mine", RPC, "https://rpc.mine.example.com", "")
898 uassert.NoError(t, err)
899 // A ruled one is a record: the edit refuses to drop it unseen, and a
900 // curator removes it first.
901 uassert.ErrorContains(t, r.Edit("mine", rev(r, "mine"), onyx(), alice, 2, ""), "which a curator ruled on")
902 uassert.NoError(t, r.RemoveEndpoint(flagged, erev(r, flagged)))
903 uassert.NoError(t, r.Edit("mine", rev(r, "mine"), onyx(), alice, 2, ""))
904 for _, id := range []int64{priv, flagged} {
905 _, ok := r.Endpoint(id)
906 uassert.False(t, ok, "a private endpoint is dropped")
907 }
908 _, ok := r.Endpoint(pub)
909 uassert.True(t, ok, "a public one stays")
910 uassert.Equal(t, 1, r.EndpointCount("mine", ""))
911 uassert.Equal(t, 0, r.OwnerCount("mine", bob))
912 uassert.Equal(t, 1, r.Awaiting("mine"))
913}
914
915// A curator's chain-id edit of a pending zone names the curator on the resets;
916// only the proposer's own edit records nobody.
917func TestResetNamesACuratorEditor(t *testing.T) {
918 r := NewRegistry()
919 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
920 id, _ := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
921 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 2, ""))
922 moved := onyx()
923 moved.ChainID = "onyx-2"
924 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, curator, 3, ""))
925 e, _ := r.Endpoint(id)
926 uassert.Equal(t, curator.String(), e.ReviewedBy.String())
927}
928
929func TestPrivateHostsAndURLForms(t *testing.T) {
930 for _, h := range []string{"validator", "node", "printer.local", "metadata.google.internal", "router.home.arpa",
931 "foo.localdomain", "a.localhost", "192.0.0.1", "198.18.0.1", "198.19.255.255", "224.0.0.1", "255.255.255.255",
932 "127.1", "0x7f.1", "2130706433",
933 "rpc.test", "node.lan", "nas.home", "dc.corp", "wiki.intranet", "box.private", "x.onion", "y.alt",
934 "192.0.2.1", "198.51.100.7", "203.0.113.255",
935 "gno.example", "x.invalid", "rpc.mail", "ipv4only.arpa", "gnoland.default.service.arpa", "x.arpa",
936 "100.127.255.255", "172.16.0.1", "172.31.255.255", "svc.service.consul", "node.lxd", "x.docker", "y.localnet"} {
937 uassert.True(t, IsPrivateHost(h), h)
938 }
939 for _, h := range []string{"rpc.gno.land", "1.1.1.1", "198.20.0.1", "172.32.0.1", "172.15.255.255", "gno.land.", "100.128.0.1",
940 "a.latest", "my.salt", "192.0.3.1", "198.51.101.1", "203.0.114.1",
941 "example.com", "gmail.com", "arpa.example.com"} {
942 uassert.False(t, IsPrivateHost(h), h)
943 }
944 // tcp:// and http:// are one rpc endpoint, the way gnokey dials them.
945 uassert.Equal(t, Canonical(RPC, "tcp://h.example.com:26657"), Canonical(RPC, "http://h.example.com:26657"))
946 uassert.Equal(t, Canonical(RPC, "tcp://h.example.com:80"), Canonical(RPC, "http://h.example.com"))
947 // An empty query's ? goes; a query ending in ? keeps it.
948 uassert.Equal(t, "https://h.example.com/p?a?", Canonical(Indexer, "https://h.example.com/p?a?"))
949 // . and .. path segments are another spelling: refused.
950 for _, u := range []string{"https://h.example.com/.", "https://h.example.com/./x", "https://h.example.com/a/..", "https://h.example.com/../a"} {
951 uassert.ErrorContains(t, ValidateEndpoint(Indexer, u), "path segment", u)
952 }
953 uassert.NoError(t, ValidateEndpoint(Indexer, "https://h.example.com/a.b/..c/x."))
954 in := onyx()
955 for _, title := range []string{"⌛ pending", "❎ rejected", "\U0001F6D1 stop", "☒ no"} {
956 in.Title = title
957 uassert.ErrorContains(t, ValidateInfo(in), "status glyph", title)
958 }
959}
960
961// A copied Registry value is the same registry: it shares the revision
962// counter with the zones it shares, so no revision is handed out twice. With
963// the counter inline, an edit through the copy and then one through the
964// original would both produce the same revision number, and a decision made on
965// the first content would pass against the second.
966func TestCopiedRegistryIsTheSameRegistry(t *testing.T) {
967 r1 := NewRegistry()
968 uassert.NoError(t, r1.Propose(alice, 1, "onyx", onyx()))
969 r2 := *r1
970 in := onyx()
971 in.Title = "via the copy"
972 uassert.NoError(t, r2.Edit("onyx", rev(r1, "onyx"), in, alice, 2, ""))
973 seen := rev(r1, "onyx") // what a curator read: the copy's content
974 in.Title = "via the original"
975 uassert.NoError(t, r1.Edit("onyx", seen, in, alice, 3, ""))
976 uassert.True(t, rev(r1, "onyx") != seen, "a new content, a new revision")
977 uassert.ErrorContains(t, r1.ReviewZone("onyx", Approved, seen, curator, 4, ""), "changed since you read it")
978}
979
980// Editing a local zone that stays local drops nothing, and every Info URL is
981// checked for a private host, not only the main RPC.
982func TestOnlyLeavingLocalDrops(t *testing.T) {
983 r := NewRegistry()
984 local := onyx()
985 local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
986 uassert.NoError(t, r.Propose(alice, 1, "mine", local))
987 id, err := r.Register(alice, 1, "mine", Peer, nodeID+"@10.0.0.5:26656", "")
988 uassert.NoError(t, err)
989 local.Title = "Mine, renamed"
990 uassert.NoError(t, r.Edit("mine", rev(r, "mine"), local, alice, 2, ""))
991 _, ok := r.Endpoint(id)
992 uassert.True(t, ok, "a local zone keeps its private endpoints")
993
994 for _, set := range []func(*Info){
995 func(in *Info) { in.GnowebURL = "http://192.168.1.2" },
996 func(in *Info) { in.GenesisURL = "https://files.lan/genesis.json" },
997 } {
998 in := onyx()
999 set(&in)
1000 uassert.ErrorContains(t, ValidateInfo(in), "private or special-use")
1001 }
1002}
1003
1004// Any edit to an approved zone is a review on record, so a chain-id reset it
1005// causes names its editor, whoever that is; only a pending zone's own
1006// proposer resets as nobody.
1007func TestApprovedEditResetNamesTheEditor(t *testing.T) {
1008 r := NewRegistry()
1009 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1010 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, ""))
1011 id, _ := r.Register(bob, 3, "onyx", RPC, "https://a.example.com", "")
1012 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 4, ""))
1013 moved := onyx()
1014 moved.ChainID = "onyx-2"
1015 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, alice, 5, "relaunched"))
1016 e, _ := r.Endpoint(id)
1017 uassert.Equal(t, alice.String(), e.ReviewedBy.String())
1018}
1019
1020// A curator's exemption is from the review queue, never from the live cap.
1021func TestExemptStillMeetsTheLiveCap(t *testing.T) {
1022 r := NewRegistry()
1023 for i := 0; i < MaxZones; i++ {
1024 slug := "z" + strconv.Itoa(i)
1025 uassert.NoError(t, r.ProposeExempt(curator, 1, slug, onyx()))
1026 uassert.NoError(t, r.ReviewZone(slug, Approved, rev(r, slug), curator, 1, ""))
1027 }
1028 uassert.ErrorContains(t, r.ProposeExempt(curator, 2, "one-more", onyx()), "the registry is full")
1029}
1030
1031// The URL forms each kind dials: a tcp:// rpc is host and port only, an
1032// indexer may be a websocket, and IsPrivateHost fails closed on anything but a
1033// bare host.
1034func TestURLFormsPerKind(t *testing.T) {
1035 uassert.ErrorContains(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657/websocket"), "host and port only")
1036 uassert.ErrorContains(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657?x=1"), "host and port only")
1037 uassert.NoError(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657"))
1038 uassert.NoError(t, ValidateEndpoint(RPC, "wss://rpc.example.com/websocket"))
1039 uassert.NoError(t, ValidateEndpoint(Indexer, "wss://indexer.example.com/graphql/query"))
1040 uassert.Error(t, ValidateEndpoint(Faucet, "wss://faucet.example.com"))
1041 for _, h := range []string{"127.0.0.1:26657", "10.0.0.1/", "foo.local:80", "a b", "192.88.99.1"} {
1042 uassert.True(t, IsPrivateHost(h), h)
1043 }
1044}
1045
1046// A rejection or a retirement is restated with a new reason, the only way to
1047// correct one, without a second eviction or reset.
1048func TestARejectionReasonCanBeRestated(t *testing.T) {
1049 r := NewRegistry()
1050 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1051 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 2, "wrong netwrok"))
1052 entered := func() int64 { z, _ := r.Zone("onyx"); return z.Entered }
1053 before := entered()
1054 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), bob, 3, "wrong network"))
1055 z, _ := r.Zone("onyx")
1056 uassert.Equal(t, "wrong network", z.Reason)
1057 uassert.Equal(t, bob.String(), z.ReviewedBy.String())
1058 uassert.Equal(t, before, entered(), "a restatement is not a new entry")
1059 uassert.ErrorContains(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), bob, 4, "wrong network"), "already rejected")
1060}
1061
1062// Flagging a verified endpoint puts it back in the unchecked index, so counts
1063// and a later reset see it as what it is.
1064func TestFlaggingAVerifiedEndpointUnchecksIt(t *testing.T) {
1065 r := NewRegistry()
1066 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1067 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1068 id, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
1069 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 3, ""))
1070 uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), carol, 4, "down"))
1071 v, total := r.Count("onyx")
1072 uassert.Equal(t, 0, v)
1073 uassert.Equal(t, 1, total)
1074 uassert.Equal(t, 0, r.Awaiting("onyx"))
1075 // A retirement then keeps the flag: it resets only what is verified.
1076 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 5, "shut down"))
1077 e, _ := r.Endpoint(id)
1078 uassert.Equal(t, string(Flagged), string(e.Status))
1079 // A restated verdict names its new reviewer.
1080 uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 6, "down for good"))
1081 e, _ = r.Endpoint(id)
1082 uassert.Equal(t, curator.String(), e.ReviewedBy.String())
1083}
1084
1085// A reset moves the endpoint's revision, so a verdict written against the
1086// verified endpoint fails after a retirement reset it.
1087func TestAResetMovesTheEndpointRevision(t *testing.T) {
1088 r := NewRegistry()
1089 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1090 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1091 id, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
1092 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 3, ""))
1093 read := erev(r, id)
1094 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 4, "shut down"))
1095 uassert.ErrorContains(t, r.ReviewEndpoint(id, Flagged, zr(r, id), read, carol, 5, "x"), "changed since")
1096}
1097
1098// The package README's example, verbatim but for must and the names: it is
1099// the first thing an importer copies, so it is run.
1100func TestTheReadmeExampleRuns(t *testing.T) {
1101 proposer, height := alice, int64(1)
1102 r := NewRegistry()
1103 uassert.NoError(t, r.Propose(proposer, height, "onyx", Info{ChainID: "onyx-1", Title: "Onyx",
1104 Kind: Testnet, RPCURL: "https://rpc.onyx.testnets.gno.land"}))
1105 z, _ := r.Zone("onyx")
1106 uassert.NoError(t, r.ReviewZone("onyx", Approved, z.Revision, curator, height, ""))
1107 z, _ = r.Zone("onyx")
1108 id, err := r.Register(proposer, height, "onyx", Peer,
1109 "g1x5mlj5ava0dw9vkf4j6admjlzswm6f06p44krn@seed-1.onyx.testnets.gno.land:26656", "gno core")
1110 uassert.NoError(t, err)
1111 e, _ := r.Endpoint(id)
1112 uassert.NoError(t, r.ReviewEndpoint(id, Verified, z.Revision, e.Revision, curator, height, "answers onyx-1"))
1113 uassert.Equal(t, 1, len(r.Endpoints(EndpointFilter{Zone: "onyx", Kind: Peer, Status: Verified})))
1114}
1115
1116// A verification restated with a new reason names its new reviewer, and a
1117// zone removed and proposed again starts with no private endpoints on record.
1118func TestRestatedVerifyAndARemovedZonesPrivateIndex(t *testing.T) {
1119 r := NewRegistry()
1120 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1121 id, _ := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
1122 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 2, "answers"))
1123 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), carol, 3, "answers, re-probed"))
1124 e, _ := r.Endpoint(id)
1125 uassert.Equal(t, carol.String(), e.ReviewedBy.String())
1126
1127 local := onyx()
1128 local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
1129 uassert.NoError(t, r.Propose(alice, 4, "lab", local))
1130 _, err := r.Register(alice, 4, "lab", RPC, "http://10.0.0.1:26657", "")
1131 uassert.NoError(t, err)
1132 uassert.NoError(t, r.RemoveZone("lab", rev(r, "lab")))
1133 uassert.NoError(t, r.Propose(alice, 5, "lab", local))
1134 uassert.Equal(t, 0, len(r.PrivateEndpoints("lab")))
1135 uassert.NoError(t, r.Edit("lab", rev(r, "lab"), onyx(), alice, 6, ""))
1136}
1137
1138// A zone decision naming a revision from the future fails as a stale one does.
1139func TestAFutureRevisionIsRefused(t *testing.T) {
1140 r := NewRegistry()
1141 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1142 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx")+1, curator, 2, ""), "changed since you read it")
1143}
1144
1145// Restating: any review state with a new visible reason, bumping the
1146// revision; never with an empty or the same reason.
1147func TestRestatementRules(t *testing.T) {
1148 r := NewRegistry()
1149 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1150 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 2, "answers onyx-1"))
1151 read := rev(r, "onyx")
1152 uassert.NoError(t, r.ReviewZone("onyx", Approved, read, carol, 3, "answers onyx-1, re-probed"))
1153 uassert.True(t, rev(r, "onyx") > read, "a restatement moves the revision")
1154 uassert.ErrorContains(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), carol, 4, " "), "already approved")
1155 id, _ := r.Register(bob, 4, "onyx", RPC, "https://a.example.com", "")
1156 uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 4, "down"))
1157 uassert.ErrorContains(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), curator, 4, ""), "needs a new reason")
1158 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), curator, 5, "shut down"))
1159 uassert.NoError(t, r.ReviewZone("onyx", Retired, rev(r, "onyx"), carol, 6, "shut down 2026-10-01"))
1160 z, _ := r.Zone("onyx")
1161 uassert.Equal(t, "shut down 2026-10-01", z.Reason)
1162
1163}
1164
1165// What a URL is stored as: scheme and host lowercased in ASCII only, the path
1166// and query as typed; a letter that lowercases into ASCII is refused, not
1167// folded. A tcp:// rpc path is refused whatever the scheme's case.
1168func TestStoredURLForm(t *testing.T) {
1169 r := NewRegistry()
1170 in := onyx()
1171 in.GenesisURL = "HTTPS://Files.Example.com/G?Q=1"
1172 uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1173 z, _ := r.Zone("onyx")
1174 uassert.Equal(t, "https://files.example.com/G?Q=1", z.GenesisURL)
1175 id, err := r.Register(alice, 1, "onyx", Indexer, "HTTPS://Idx.Example.com?Q=1", "")
1176 uassert.NoError(t, err)
1177 e, _ := r.Endpoint(id)
1178 uassert.Equal(t, "https://idx.example.com?Q=1", e.Address)
1179 for _, u := range []string{"https://Key.example.com", "https://rpc.İnfo.example.com"} {
1180 bad := onyx()
1181 bad.RPCURL = u
1182 uassert.Error(t, r.Propose(bob, 2, "x"+strconv.Itoa(len(u)), bad), u)
1183 }
1184 uassert.ErrorContains(t, ValidateEndpoint(RPC, "TCP://h.example.com:26657/websocket"), "host and port only")
1185 uassert.False(t, IsPrivateHost("RPC.GNO.LAND"))
1186 uassert.ErrorContains(t, ValidateEndpoint(RPC, "https://café.example.com"), "'é'")
1187}
1188
1189// A curator's leave-local edit is refused while a private endpoint carries a
1190// ruling of any kind: a reset's reason, a reasonless unverify, a verification;
1191// a curator who proposed the zone included.
1192func TestLeavingLocalRefusesEveryRuling(t *testing.T) {
1193 setup := func() (*Registry, int64) {
1194 r := NewRegistry()
1195 local := onyx()
1196 local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
1197 uassert.NoError(t, r.Propose(curator, 1, "lab", local))
1198 id, err := r.Register(curator, 1, "lab", RPC, "http://10.0.0.1:26657", "")
1199 uassert.NoError(t, err)
1200 return r, id
1201 }
1202 r, id := setup()
1203 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), carol, 2, ""))
1204 uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 3, ""), "is verified")
1205
1206 r, id = setup()
1207 uassert.NoError(t, r.ReviewEndpoint(id, Flagged, zr(r, id), erev(r, id), carol, 2, "x"))
1208 uassert.NoError(t, r.ReviewEndpoint(id, Unverified, zr(r, id), erev(r, id), carol, 3, ""))
1209 uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 4, ""), "a curator ruled on")
1210
1211 r, id = setup()
1212 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), carol, 2, ""))
1213 moved := onyx()
1214 moved.Kind, moved.RPCURL, moved.ChainID = Local, "http://127.0.0.1:26657", "lab-2"
1215 uassert.NoError(t, r.Edit("lab", rev(r, "lab"), moved, curator, 3, "")) // the proposer's reset: no reviewer, a reason
1216 e, _ := r.Endpoint(id)
1217 uassert.Equal(t, "", e.ReviewedBy.String())
1218 uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 4, ""), "a curator ruled on")
1219}
1220
1221// Clearable counts what a bulk clear may remove: registered through the gate,
1222// never ruled on. A first verdict or a removal takes one out (a reset cannot:
1223// it touches only verified endpoints); an exempt registration never counts.
1224func TestTheClearableCount(t *testing.T) {
1225 r := NewRegistry()
1226 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1227 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1228 a, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", "")
1229 b, _ := r.Register(bob, 2, "onyx", RPC, "https://b.example.com", "")
1230 c, _ := r.Register(bob, 2, "onyx", RPC, "https://c.example.com", "")
1231 x, _ := r.RegisterExempt(curator, 2, "onyx", RPC, "https://x.example.com", "")
1232 ex, _ := r.Endpoint(x)
1233 uassert.True(t, ex.Exempt)
1234 uassert.False(t, ex.Clearable())
1235 uassert.Equal(t, 3, r.Clearable("onyx"))
1236 uassert.NoError(t, r.ReviewEndpoint(a, Verified, zr(r, a), erev(r, a), curator, 3, ""))
1237 uassert.Equal(t, 2, r.Clearable("onyx"))
1238 uassert.NoError(t, r.ReviewEndpoint(a, Flagged, zr(r, a), erev(r, a), curator, 3, "down"))
1239 uassert.Equal(t, 2, r.Clearable("onyx"), "a second verdict changes nothing")
1240 uassert.NoError(t, r.RemoveEndpoint(b, erev(r, b)))
1241 uassert.Equal(t, 1, r.Clearable("onyx"))
1242 moved := onyx()
1243 moved.ChainID = "onyx-2"
1244 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), moved, curator, 4, "relaunch"))
1245 uassert.Equal(t, 1, r.Clearable("onyx"), "a reset touches only verified endpoints")
1246 _ = c
1247}
1248
1249// One edit off the local kind drops at most MaxDropPerEdit endpoints.
1250func TestLeavingLocalDropsAtMostTheCap(t *testing.T) {
1251 r := NewRegistry()
1252 local := onyx()
1253 local.Kind, local.RPCURL = Local, "http://127.0.0.1:26657"
1254 uassert.NoError(t, r.Propose(curator, 1, "lab", local))
1255 for i := 0; i <= MaxDropPerEdit; i++ {
1256 _, err := r.RegisterExempt(curator, 1, "lab", RPC, "http://10.0.0."+strconv.Itoa(i%250+1)+":"+strconv.Itoa(26000+i), "")
1257 uassert.NoError(t, err)
1258 }
1259 uassert.ErrorContains(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 2, ""), "more than 64 in one edit")
1260 ids := r.PrivateEndpoints("lab")
1261 uassert.NoError(t, r.RemoveEndpoint(ids[0].ID, ids[0].Revision))
1262 uassert.NoError(t, r.Edit("lab", rev(r, "lab"), onyx(), curator, 3, ""))
1263}
1264
1265// The numbers the READMEs state, pinned: a change to one is a change to what
1266// the docs promise.
1267func TestTheDocumentedBounds(t *testing.T) {
1268 for name, got := range map[string]int{"MaxZones": MaxZones, "MaxPending": MaxPending, "MaxPendingPerProposer": MaxPendingPerProposer,
1269 "MaxRejected": MaxRejected, "MaxRetired": MaxRetired, "MaxEndpointsPerZone": MaxEndpointsPerZone,
1270 "MaxUnverifiedPerZone": MaxUnverifiedPerZone, "MaxEndpointsPerAddress": MaxEndpointsPerAddress, "MaxDropPerEdit": MaxDropPerEdit,
1271 "MaxTitleLen": MaxTitleLen, "MaxDescriptionLen": MaxDescriptionLen, "MaxLabelLen": MaxLabelLen, "MaxReasonLen": MaxReasonLen, "MaxURLLen": MaxURLLen} {
1272 want := map[string]int{"MaxZones": 256, "MaxPending": 64, "MaxPendingPerProposer": 4, "MaxRejected": 64, "MaxRetired": 128,
1273 "MaxEndpointsPerZone": 128, "MaxUnverifiedPerZone": 64, "MaxEndpointsPerAddress": 16, "MaxDropPerEdit": 64,
1274 "MaxTitleLen": 64, "MaxDescriptionLen": 512, "MaxLabelLen": 64, "MaxReasonLen": 280, "MaxURLLen": 256}[name]
1275 uassert.Equal(t, want, got, name)
1276 }
1277}
1278
1279// Rules the other tests reach only in part.
1280func TestValidationEdges(t *testing.T) {
1281 // A private host is refused on every kind but local.
1282 for _, k := range []Kind{Mainnet, Testnet, Devnet} {
1283 in := onyx()
1284 in.Kind, in.RPCURL = k, "http://10.0.0.1:26657"
1285 uassert.ErrorContains(t, ValidateInfo(in), "private or special-use", string(k))
1286 }
1287 // The combining grapheme joiner draws nothing.
1288 in := onyx()
1289 in.Title = "On\u034fyx"
1290 uassert.ErrorContains(t, ValidateInfo(in), "invisible")
1291 // Needless escapes of unreserved characters, and an entity shape.
1292 for _, u := range []string{"https://h.example.com/a%5Fb", "https://h.example.com/a%2Db", "https://h.example.com/%2E%2E/x",
1293 "https://h.example.com/a&b"} {
1294 uassert.Error(t, ValidateEndpoint(RPC, u), u)
1295 }
1296 // Dot segments are a path rule; a query may hold dots.
1297 uassert.NoError(t, ValidateEndpoint(Explorer, "https://h.example.com/p?x=../y"))
1298 // Default websocket ports are dropped like the http ones.
1299 uassert.Equal(t, Canonical(RPC, "wss://h.example.com/ws"), Canonical(RPC, "wss://h.example.com:443/ws"))
1300 uassert.Equal(t, Canonical(RPC, "ws://h.example.com/ws"), Canonical(RPC, "ws://h.example.com:80/ws"))
1301}
1302
1303// What a caller types is trimmed before it is used: the kind, a label, a
1304// reason; and a restatement of verify or unverify needs a new visible reason.
1305func TestTrimsAndRestatementReasons(t *testing.T) {
1306 r := NewRegistry()
1307 in := onyx()
1308 in.Kind = " testnet "
1309 uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1310 z, _ := r.Zone("onyx")
1311 uassert.Equal(t, string(Testnet), string(z.Kind))
1312 uassert.NoError(t, r.ReviewZone("onyx", Approved, rev(r, "onyx"), curator, 1, ""))
1313 id, _ := r.Register(bob, 2, "onyx", RPC, "https://a.example.com", " bob's node ")
1314 e, _ := r.Endpoint(id)
1315 uassert.Equal(t, "bob's node", e.Label)
1316 uassert.NoError(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 3, " answers "))
1317 e, _ = r.Endpoint(id)
1318 uassert.Equal(t, "answers", e.Reason)
1319 uassert.ErrorContains(t, r.ReviewEndpoint(id, Verified, zr(r, id), erev(r, id), curator, 4, " "), "needs a new reason")
1320 uassert.NoError(t, r.ReviewEndpoint(id, Unverified, zr(r, id), erev(r, id), curator, 5, "x"))
1321 uassert.ErrorContains(t, r.ReviewEndpoint(id, Unverified, zr(r, id), erev(r, id), curator, 6, ""), "needs a new reason")
1322 edited := onyx()
1323 edited.Title = "Onyx, edited"
1324 uassert.NoError(t, r.Edit("onyx", rev(r, "onyx"), edited, curator, 7, " retitled "))
1325 z, _ = r.Zone("onyx")
1326 uassert.Equal(t, "retitled", z.Reason)
1327 uassert.Equal(t, rev(r, "onyx"), r.Revision(), "the last revision handed out")
1328}
1329
1330// A zone removed and proposed again starts with nothing clearable.
1331func TestARemovedZonesClearableCountIsGone(t *testing.T) {
1332 r := NewRegistry()
1333 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1334 _, err := r.Register(alice, 1, "onyx", RPC, "https://a.example.com", "")
1335 uassert.NoError(t, err)
1336 uassert.Equal(t, 1, r.Clearable("onyx"))
1337 uassert.NoError(t, r.RemoveZone("onyx", rev(r, "onyx")))
1338 uassert.NoError(t, r.Propose(alice, 2, "onyx", onyx()))
1339 uassert.Equal(t, 0, r.Clearable("onyx"))
1340}
1341
1342// URL characters, escapes, schemes, selectors, Parse trimming, pending
1343// restatement and stored tails.
1344func TestURLAndTextRules(t *testing.T) {
1345 // Every character a URL may not hold, one at a time.
1346 for _, c := range []string{"<", ">", "`", "(", ")", "[", "]", "{", "}", "|", "\\", "^", "#", "\"", "'"} {
1347 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a"+c+"b"), c)
1348 }
1349 // A needless escape of a digit or a lowercase letter; the hex case of the
1350 // first escape digit.
1351 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%30"))
1352 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%61"))
1353 uassert.Equal(t, Canonical(RPC, "https://h.example.com/%af"), Canonical(RPC, "https://h.example.com/%AF"))
1354 // A hex last label reads as an IPv4 number.
1355 uassert.Error(t, ValidateEndpoint(RPC, "https://a.0x1"))
1356 uassert.True(t, IsPrivateHost("a.0x1"))
1357 uassert.True(t, IsPrivateHost("PRINTER.LOCAL"))
1358 // Schemes per field.
1359 in := onyx()
1360 in.GnowebURL = "tcp://onyx.example.com:26657"
1361 uassert.Error(t, ValidateInfo(in))
1362 uassert.Error(t, ValidateEndpoint(Indexer, "tcp://indexer.example.com:8546"))
1363 // Variation selectors only after a base they modify.
1364 for _, s := range []string{"a\ufe00", "᠀\u180b"} {
1365 in := onyx()
1366 in.Title = s
1367 uassert.ErrorContains(t, ValidateInfo(in), "invisible", s)
1368 }
1369 ok := onyx()
1370 ok.Title = "ᠨ\u180f"
1371 uassert.NoError(t, ValidateInfo(ok))
1372 uassert.False(t, HasVisible("\u200b\u200d"))
1373 // The Parse functions trim.
1374 st, err := ParseStatus(" approved ")
1375 uassert.NoError(t, err)
1376 uassert.Equal(t, string(Approved), string(st))
1377 k, err := ParseEndpointKind(" rpc ")
1378 uassert.NoError(t, err)
1379 uassert.Equal(t, string(RPC), string(k))
1380 v, err := ParseVerification(" verified ")
1381 uassert.NoError(t, err)
1382 uassert.Equal(t, string(Verified), string(v))
1383 // Nothing goes back to pending, not even as a restatement.
1384 r := NewRegistry()
1385 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1386 uassert.Error(t, r.ReviewZone("onyx", Pending, rev(r, "onyx"), curator, 2, "x"))
1387 z, _ := r.Zone("onyx")
1388 uassert.Equal(t, "", z.ReviewedBy.String())
1389 // An empty query and a bare "/" are not stored, so the dialable spelling
1390 // is the one listed.
1391 id, err := r.Register(alice, 3, "onyx", RPC, "https://rpc2.zz.example.com/?", "")
1392 uassert.NoError(t, err)
1393 e, _ := r.Endpoint(id)
1394 uassert.Equal(t, "https://rpc2.zz.example.com", e.Address)
1395 _, err = r.Register(alice, 3, "onyx", RPC, "https://rpc2.zz.example.com", "")
1396 uassert.ErrorContains(t, err, "already lists")
1397 // More private suffixes, more look-alikes.
1398 for _, h := range []string{"rpc.node.incus", "web.dns.podman", "foo.i2p", "gnoland.default.svc"} {
1399 uassert.True(t, IsPrivateHost(h), h)
1400 }
1401 for _, r := range []rune{0x1FBBD, 0x1F6AD, 0x1F6AF, 0x1F6B1, 0x1F6B3, 0x1F6B7, 0x1F4F5, 0x1F51E, 0x1F10D, 0x1F10F, 0x1F16E} {
1402 uassert.Error(t, ValidateLabel(string(r)+" official"), string(r))
1403 }
1404}
1405
1406// What is stored canonicalizes as what was typed, so a check made on the
1407// typed address (the realm's own-URL reservation) holds for the stored one.
1408func TestTheStoredFormKeepsItsCanonicalForm(t *testing.T) {
1409 for _, u := range []string{"https://h.example.com??", "https://h.example.com/??", "https://h.example.com?/",
1410 "https://h.example.com?path=/", "https://h.example.com/p?x=?", "https://h.example.com/p??", "https://h.example.com/?",
1411 "https://h.example.com/", "https://h.example.com?", "https://h.example.com/?q=1", "https://h.example.com/p/"} {
1412 uassert.Equal(t, Canonical(RPC, u), Canonical(RPC, trimEmptyTail(lowerAuthority(u))), u)
1413 }
1414 uassert.Equal(t, "https://h.example.com?path=/", trimEmptyTail("https://h.example.com?path=/"))
1415 uassert.Equal(t, "https://h.example.com??", trimEmptyTail("https://h.example.com??"))
1416}
1417
1418// Spaces of every kind (Unicode Zs) and tabs are trimmed; a line separator at
1419// an edge reaches the validator and is refused, never silently cut. A zone's
1420// gnoweb and genesis URLs have an empty tail repaired, its main RPC is refused
1421// with one, and an endpoint is validated as it will be stored.
1422func TestTrimsSpacesNotLineBreaks(t *testing.T) {
1423 r := NewRegistry()
1424 in := onyx()
1425 in.Title = "Onyx\u2028"
1426 uassert.ErrorContains(t, r.Propose(alice, 1, "onyx", in), "control character")
1427 in = onyx()
1428 in.Description = "\u0085" + in.Description
1429 uassert.ErrorContains(t, r.Propose(alice, 1, "onyx", in), "control character")
1430 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1431 _, err := r.Register(alice, 2, "onyx", RPC, "https://a.example.com", "operator\u2028")
1432 uassert.ErrorContains(t, err, "control character")
1433 uassert.Equal(t, "x", TrimSpaces(" \tx\t "))
1434 for _, u := range []string{"https://g.example.com/", "https://g.example.com?", "https://g.example.com/?"} {
1435 r := NewRegistry()
1436 in := onyx()
1437 in.GnowebURL, in.GenesisURL = u, u
1438 uassert.NoError(t, r.Propose(alice, 1, "xz", in), u)
1439 z, _ := r.Zone("xz")
1440 uassert.Equal(t, "https://g.example.com", z.GnowebURL, u)
1441 uassert.Equal(t, "https://g.example.com", z.GenesisURL, u)
1442 }
1443 for _, u := range []string{"https://rpc.example.com/", "https://rpc.example.com?"} {
1444 bad := onyx()
1445 bad.RPCURL = u
1446 uassert.Error(t, ValidateInfo(bad), u)
1447 uassert.Error(t, NewRegistry().Propose(alice, 1, "xz", bad), "the registry refuses it too: "+u)
1448 }
1449 uassert.Equal(t, "Onyx", TrimSpaces("\u00a0Onyx\u3000"))
1450 uassert.Equal(t, "Onyx\n", TrimSpaces("Onyx\n"))
1451 // ValidateEndpoint judges the string it is given; Register stores the
1452 // trimmed form and validates that.
1453 uassert.Error(t, ValidateEndpoint(RPC, "tcp://rpc.example.com:26657/"), "as given, it has a path")
1454 id, err := r.Register(alice, 2, "onyx", RPC, "tcp://rpc.example.com:26657/", "")
1455 uassert.NoError(t, err)
1456 e, _ := r.Endpoint(id)
1457 uassert.Equal(t, "tcp://rpc.example.com:26657", e.Address)
1458 long := "https://e.example.com/" + strings.Repeat("a", MaxURLLen-len("https://e.example.com/"))
1459 _, err = r.Register(alice, 2, "onyx", Explorer, long+"?", "")
1460 uassert.NoError(t, err, "257 bytes as typed, 256 as stored")
1461 _, err = r.Register(alice, 2, "onyx", Explorer, long+"b", "")
1462 uassert.Error(t, err, "257 as stored")
1463}
1464
1465// Chain id charset, text and URL bounds, controls, escapes and trims.
1466func TestMoreBoundaries(t *testing.T) {
1467 in := onyx()
1468 in.ChainID = "onyx:1"
1469 uassert.Error(t, ValidateInfo(in))
1470 uassert.Error(t, ValidateReason(strings.Repeat("x", MaxReasonLen+1)))
1471 uassert.NoError(t, ValidateReason(strings.Repeat("x", MaxReasonLen)))
1472 uassert.Error(t, ValidateLabel(strings.Repeat("x", MaxLabelLen+1)))
1473 uassert.NoError(t, ValidateLabel(strings.Repeat("x", MaxLabelLen)))
1474 for _, c := range []string{"\x1f", "\x7f", "\u009f"} {
1475 uassert.ErrorContains(t, ValidateLabel("a"+c+"b"), "control character")
1476 }
1477 ok := onyx()
1478 ok.Title = "ᠠ\u180b"
1479 uassert.NoError(t, ValidateInfo(ok))
1480 host := "https://" + strings.Repeat("a", 20) + ".example.com/"
1481 long := host + strings.Repeat("p", MaxURLLen-len(host))
1482 uassert.NoError(t, ValidateEndpoint(RPC, long))
1483 uassert.Error(t, ValidateEndpoint(RPC, long+"p"))
1484 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a\x7fb"))
1485 uassert.NoError(t, ValidateEndpoint(Explorer, "https://h.example.com/a&;b"))
1486 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%2z"))
1487 uassert.NoError(t, ValidateEndpoint(Explorer, "https://h.example.com/p?a=/../"))
1488 uassert.Error(t, ValidateEndpoint(RPC, "https://a.0X1"))
1489 uassert.Error(t, ValidateEndpoint(RPC, "https://256.1.1.1"))
1490
1491 r := NewRegistry()
1492 in = onyx()
1493 in.Description = " \t" + in.Description + "\t "
1494 uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1495 z, _ := r.Zone("onyx")
1496 uassert.Equal(t, onyx().Description, z.Description)
1497 edited := onyx()
1498 edited.Title = "Onyx, edited"
1499 uassert.ErrorContains(t, r.Edit("onyx", rev(r, "onyx"), edited, alice, 2, "x"), "takes no reason")
1500}
1501
1502// Boundaries the suites reached only in part.
1503func TestFurtherBoundaries(t *testing.T) {
1504 uassert.ErrorContains(t, ValidateLabel(" "), "nothing visible")
1505 uassert.Error(t, ValidateLabel("a\U000E01EF"))
1506 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/a%2!"))
1507 uassert.Error(t, ValidateEndpoint(RPC, "https://h.example.com/%2d"))
1508 uassert.Error(t, ValidateEndpoint(RPC, "https://1.2.3.4.5"))
1509 uassert.Error(t, ValidateEndpoint(Peer, nodeID+"@"+strings.Repeat("a", 250)+".example.com:26656"))
1510 uassert.Equal(t, 16, ReservedForReviewers)
1511
1512 r := NewRegistry()
1513 in := onyx()
1514 in.ChainID = " onyx-1 "
1515 uassert.NoError(t, r.Propose(alice, 1, "onyx", in))
1516 z, _ := r.Zone("onyx")
1517 uassert.Equal(t, "onyx-1", z.ChainID)
1518 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), curator, 2, "first"))
1519 uassert.NoError(t, r.ReviewZone("onyx", Rejected, rev(r, "onyx"), carol, 9, "restated"))
1520 z, _ = r.Zone("onyx")
1521 uassert.Equal(t, int64(9), z.ReviewedAt, "a restatement records when")
1522}
1523
1524// Round 13: rules that had no test of their own.
1525func TestRoundThirteenBoundaries(t *testing.T) {
1526 // HasVisible is exported, so its filler rule is its own, not checkText's.
1527 uassert.False(t, HasVisible("\u3164"), "a Hangul filler is not visible")
1528 uassert.True(t, HasVisible("a\u3164"))
1529 // A 0x last label is held to the address rule even when it is not hex.
1530 uassert.Error(t, ValidateEndpoint(RPC, "https://a.0xyz"))
1531 // A peer is at most MaxURLLen bytes, exactly.
1532 peer := func(n int) string {
1533 host := strings.Repeat("a", 63) + "." + strings.Repeat("b", 63) + "." + strings.Repeat("c", 63) + "."
1534 pre, post := nodeID+"@", ".com:26656"
1535 return pre + host + strings.Repeat("d", n-len(pre)-len(host)-len(post)) + post
1536 }
1537 uassert.Equal(t, MaxURLLen, len(peer(MaxURLLen)))
1538 uassert.NoError(t, ValidateEndpoint(Peer, peer(MaxURLLen)))
1539 uassert.Error(t, ValidateEndpoint(Peer, peer(MaxURLLen+1)))
1540 // A filtered list is capped at its length, so an importer's append copies
1541 // instead of writing into a slice it may not write.
1542 r := NewRegistry()
1543 uassert.NoError(t, r.Propose(alice, 1, "onyx", onyx()))
1544 for i := 0; i < 3; i++ {
1545 _, err := r.Register(alice, 1, "onyx", RPC, "https://r"+strconv.Itoa(i)+".example.com", "")
1546 uassert.NoError(t, err)
1547 }
1548 id, err := r.Register(alice, 1, "onyx", RPC, "https://v.example.com", "")
1549 uassert.NoError(t, err)
1550 e, _ := r.Endpoint(id)
1551 z, _ := r.Zone("onyx")
1552 uassert.NoError(t, r.ReviewEndpoint(id, Verified, z.Revision, e.Revision, alice, 1, ""))
1553 for _, es := range [][]Endpoint{
1554 r.Endpoints(EndpointFilter{Zone: "onyx", Status: Verified}),
1555 r.Endpoints(EndpointFilter{Zone: "onyx", Kind: RPC, Status: Verified}),
1556 r.Endpoints(EndpointFilter{Status: Verified}),
1557 } {
1558 uassert.Equal(t, 1, len(es))
1559 uassert.Equal(t, len(es), cap(es))
1560 }
1561 uassert.NoError(t, r.Propose(alice, 1, "dev", Info{ChainID: "dev", Title: "D", Kind: Devnet, RPCURL: "https://rpc.dev.example.com"}))
1562 for _, zs := range [][]Zone{r.Zones(ZoneFilter{Kind: Devnet}), r.Zones(ZoneFilter{Status: Pending, Kind: Devnet})} {
1563 uassert.Equal(t, 1, len(zs))
1564 uassert.Equal(t, len(zs), cap(zs))
1565 }
1566}