Kind Pure package
Name v0
Namespace moul / zones
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/zones/v0
gno 0.9 ⧉
1 package zones 2 3 import ( 4 "strconv" 5 "strings" 6 "testing" 7 8 not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
"gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10 )
11
12 var (
13 alice = testutils.TestAddress( "alice" )
14 bob = testutils.TestAddress( "bob" )
15 curator = testutils.TestAddress( "curator" )
16 carol = testutils.TestAddress( "carol" )
17 )
18
19 func 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
30 func 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
53 func 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
64 func 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
80 func 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
133 func 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.
181 func 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
249 func 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
327 func 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
338 func 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
352 func 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
381 func 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.
393 func 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
437 func testAddr(i int ) address { return testutils.TestAddress( "flood" + strconv.Itoa(i)) }
438
439 func 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
464 func 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.
546 func 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.
552 func 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.
561 func 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.
573 func 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
615 func 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.
631 func 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.
665 func 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.
696 func 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
712 func 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.
726 func 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.
742 func 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.
759 func 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.
784 func 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
799 func 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.
840 func 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
858 func 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.
875 func 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.
887 func 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.
917 func 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
929 func 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.
966 func 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.
982 func 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.
1007 func 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.
1021 func 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.
1034 func 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.
1048 func 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.
1064 func 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.
1087 func 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.
1100 func 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.
1118 func 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.
1139 func 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.
1147 func 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.
1168 func 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.
1192 func 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.
1224 func 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.
1250 func 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.
1267 func 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.
1280 func 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.
1305 func 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.
1331 func 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.
1344 func 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.
1408 func 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.
1422 func 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.
1466 func 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.
1503 func 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.
1525 func 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 }
1567