zones_test.gno
87.82 Kb · 1820 lines
1package zones
2
3import (
4 "chain/runtime"
5 "strconv"
6 "strings"
7 "testing"
8
9 "gno.land/p/moul/addrset/v1"
10 "gno.land/p/moul/zones/v0"
11 "gno.land/p/nt/testutils/v0"
12 "gno.land/p/nt/uassert/v0"
13 "gno.land/p/nt/urequire/v0"
14)
15
16var (
17 alice = testutils.TestAddress("alice")
18 bob = testutils.TestAddress("bob")
19 carol = testutils.TestAddress("carol")
20)
21
22const nodeID = "g15rcv5yqef3kvnmueqvkyw8y05sd40jz9p3n5su"
23
24// reset puts the realm back to its init() state.
25//
26// Realm globals persist for the whole test binary and examples run after every
27// Test, so without this an ExampleRender's pinned output depends silently on
28// which tests ran before it.
29func reset() {
30 reg = zones.NewRegistry()
31 curators = addrset.Set{}
32 invited = addrset.Set{}
33 inviter = map[address]address{}
34 curators.Add(Admin)
35 seed()
36}
37
38func slugs(zs []zones.Zone) string {
39 out := []string{}
40 for _, z := range zs {
41 out = append(out, z.Slug)
42 }
43 return strings.Join(out, " ")
44}
45
46func TestRealmURLMatchesTheModule(cur realm, t *testing.T) {
47 uassert.Equal(t, "gno.land"+realmURL, cur.PkgPath())
48}
49
50func TestSeeded(t *testing.T) {
51 reset()
52 uassert.Equal(t, "mainnet onyx staging moul-staging", slugs(ListZones("approved", "")))
53 uassert.Equal(t, "", slugs(ListZones("pending", "")))
54 uassert.Equal(t, "onyx", slugs(ListZones("", "testnet")))
55 uassert.Equal(t, "staging moul-staging", slugs(ListZones("approved", "devnet")))
56
57 z, ok := GetZone("onyx")
58 urequire.True(t, ok)
59 uassert.Equal(t, "onyx-1", z.ChainID)
60 uassert.Equal(t, Admin.String(), z.ReviewedBy.String())
61 uassert.Equal(t, seedReason, z.Reason)
62
63 peers := ListAddresses("onyx", "peer", "verified")
64 uassert.Equal(t, 2, len(peers))
65 uassert.Equal(t, "g1x5mlj5ava0dw9vkf4j6admjlzswm6f06p44krn@seed-1.onyx.testnets.gno.land:26656", peers[0])
66 uassert.Equal(t, "https://rpc.gno.land", strings.Join(ListAddresses("mainnet", "rpc", ""), ","))
67
68 // staging did not answer when the seed was written, so nothing on it is
69 // verified, but it is all listed.
70 uassert.Equal(t, 0, len(ListEndpoints("staging", "", "verified")))
71 flagged := ListEndpoints("staging", "", "flagged")
72 uassert.Equal(t, 2, len(flagged))
73 uassert.True(t, strings.Contains(flagged[0].Reason, "probed 2026-10-01"))
74 uassert.Equal(t, Admin.String(), flagged[0].ReviewedBy.String())
75
76 // Every seed endpoint is filed under its own zone, and only there.
77 n := 0
78 for _, s := range seeds() {
79 uassert.Equal(t, len(s.endpoints), len(ListEndpoints(s.slug, "", "")), s.slug)
80 for _, e := range ListEndpoints(s.slug, "", "") {
81 uassert.Equal(t, s.slug, e.Zone)
82 }
83 n += len(s.endpoints)
84 }
85 uassert.Equal(t, n, len(reg.Endpoints(zones.EndpointFilter{})))
86}
87
88func TestReadsRefuseUnknownFilters(cur realm, t *testing.T) {
89 reset()
90 // The reads cross nothing, so a refusal is a plain panic, not an abort.
91 uassert.PanicsContains(t, cur, "unknown status", func() { ListZones("official", "") })
92 uassert.PanicsContains(t, cur, "unknown kind", func() { ListZones("", "betanet") })
93 uassert.PanicsContains(t, cur, "unknown endpoint kind", func() { ListEndpoints("onyx", "grpc", "") })
94 uassert.PanicsContains(t, cur, "unknown verification", func() { ListAddresses("onyx", "", "trusted") })
95 // Every zone at once is not a read a node should be asked for.
96 uassert.PanicsContains(t, cur, "name a zone", func() { ListEndpoints("", "", "") })
97 uassert.PanicsContains(t, cur, "name a zone", func() { ListAddresses(" ", "rpc", "") })
98 _, ok := GetZone("nope")
99 uassert.False(t, ok)
100 _, ok = GetEndpoint(9999)
101 uassert.False(t, ok)
102}
103
104func TestProposeThenCurate(cur realm, t *testing.T) {
105 reset()
106 testing.SetRealm(testing.NewUserRealm(alice))
107 ProposeZone(cross(cur), "alice-dev", "alicedev-1", "Alice's devnet", "for trying things",
108 "devnet", "", "https://rpc.alice.example.com", "")
109 z, ok := GetZone("alice-dev")
110 urequire.True(t, ok)
111 uassert.Equal(t, string(zones.Pending), string(z.Status))
112 uassert.Equal(t, alice.String(), z.Proposer.String())
113 uassert.Equal(t, "alice-dev", slugs(ListZones("pending", "")))
114
115 // The proposer fills in an endpoint and edits the zone (its gnoweb URL)
116 // while pending.
117 id := RegisterEndpoint(cross(cur), "alice-dev", "peer", nodeID+"@alice.example.com:26656", "alice")
118 // A proposer waits between their own edits: every edit bumps the revision a
119 // curator's decision must name, and curators need a window to make one.
120 uassert.AbortsContains(t, cur, "may act on it again in", func() {
121 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "for trying things",
122 "devnet", "https://alice.example.com", "https://rpc.alice.example.com", "", "")
123 })
124 testing.SkipHeights(ReviewWindow)
125 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "for trying things",
126 "devnet", "https://alice.example.com", "https://rpc.alice.example.com", "", "")
127 uassert.AbortsContains(t, cur, "may act on it again in", func() {
128 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "for trying more",
129 "devnet", "https://alice.example.com", "https://rpc.alice.example.com", "", "")
130 })
131 z, _ = GetZone("alice-dev")
132 uassert.Equal(t, "https://alice.example.com", z.GnowebURL)
133 uassert.False(t, z.Reviewed()) // a pending edit is just an edit
134
135 // Nobody but a curator approves, and bob cannot touch alice's proposal.
136 uassert.AbortsContains(t, cur, "is not a curator", func() { ApproveZone(cross(cur), "alice-dev", zrev("alice-dev"), "") })
137 testing.SetRealm(testing.NewUserRealm(bob))
138 uassert.AbortsContains(t, cur, "only a curator, or the proposer", func() {
139 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "x", "x", "", "devnet", "", "https://x.example.com", "", "")
140 })
141 uassert.AbortsContains(t, cur, "only a curator, or the proposer while it is pending", func() {
142 RemoveZone(cross(cur), "alice-dev", zrev("alice-dev"))
143 })
144 uassert.AbortsContains(t, cur, "only a curator or its registrant", func() {
145 RemoveEndpoint(cross(cur), id, erev(id))
146 })
147 uassert.AbortsContains(t, cur, "is not a curator", func() { VerifyEndpoint(cross(cur), id, zr(id), erev(id), "") })
148
149 testing.SetRealm(testing.NewUserRealm(Admin))
150 ApproveZone(cross(cur), "alice-dev", zrev("alice-dev"), "checked the node answers")
151 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "")
152 z, _ = GetZone("alice-dev")
153 uassert.Equal(t, string(zones.Approved), string(z.Status))
154 uassert.Equal(t, "checked the node answers", z.Reason)
155 uassert.Equal(t, 1, len(ListAddresses("alice-dev", "peer", "verified")))
156
157 // Approved, it is the curator's to edit now, not the proposer's.
158 testing.SetRealm(testing.NewUserRealm(alice))
159 uassert.AbortsContains(t, cur, "only a curator, or the proposer while it is pending", func() {
160 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "renamed", "", "devnet", "", "https://rpc.alice.example.com", "", "x")
161 })
162
163 // A curator's edit to a reviewed zone replaces the review on record, and
164 // needs a reason, so the page never shows new values under the old review.
165 testing.SetRealm(testing.NewUserRealm(Admin))
166 uassert.AbortsContains(t, cur, "an edit needs a reason", func() {
167 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc2.alice.example.com", "", "")
168 })
169 EditZone(cross(cur), "alice-dev", zrev("alice-dev"), "alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc2.alice.example.com", "", "rpc moved")
170 z, _ = GetZone("alice-dev")
171 uassert.Equal(t, "https://rpc2.alice.example.com", z.RPCURL)
172 uassert.Equal(t, "rpc moved", z.Reason)
173 uassert.Equal(t, string(zones.Approved), string(z.Status))
174 testing.SetRealm(testing.NewUserRealm(alice))
175 // Nor hers to remove; and not even a curator removes an approved zone: it is
176 // retired instead.
177 uassert.AbortsContains(t, cur, "while it is pending, may remove", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) })
178 testing.SetRealm(testing.NewUserRealm(Admin))
179 uassert.AbortsContains(t, cur, "retire it instead", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) })
180 uassert.AbortsContains(t, cur, "needs a reason", func() { RetireZone(cross(cur), "alice-dev", zrev("alice-dev"), "") })
181 RetireZone(cross(cur), "alice-dev", zrev("alice-dev"), "alice turned it off")
182 z, _ = GetZone("alice-dev")
183 uassert.Equal(t, string(zones.Retired), string(z.Status))
184 _, err := reg.Register(bob, 1, "alice-dev", zones.RPC, "https://late.example.com", "")
185 uassert.ErrorContains(t, err, "takes no endpoints")
186
187 // Retired, it is a record the curators keep: the proposer cannot erase it.
188 testing.SetRealm(testing.NewUserRealm(alice))
189 uassert.AbortsContains(t, cur, "while it is pending, may remove", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) })
190 // Nor can a curator: a retired zone is kept on record.
191 testing.SetRealm(testing.NewUserRealm(Admin))
192 uassert.AbortsContains(t, cur, "kept on record", func() { RemoveZone(cross(cur), "alice-dev", zrev("alice-dev")) })
193 _, ok = GetZone("alice-dev")
194 uassert.True(t, ok)
195}
196
197// The printed command is pasted into a shell. The main RPC cannot carry shell
198// syntax at all; the quote is defense in depth.
199func TestQueryCommandQuotesTheRemote(t *testing.T) {
200 reset()
201 // Two layers. The main RPC cannot carry shell syntax at all: its host is
202 // [A-Za-z0-9.-], its port digits, and a path, query or fragment is refused.
203 // And the printed remote is single-quoted anyway, in case a later version
204 // widens the validator.
205 z, _ := GetZone("moul-staging")
206 in := zones.Info{ChainID: runtime.ChainID(), Title: z.Title, Kind: z.Kind, RPCURL: "https://rpc.example.com/;id;"}
207 uassert.ErrorContains(t, reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "test"), "no path")
208 in.RPCURL = "https://rpc.example.com:443"
209 must(reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "test"))
210 got := queryCommand(`GetZone("onyx")`)
211 uassert.Equal(t, `gnokey query vm/qeval -remote 'https://rpc.example.com:443' -data 'gno.land/r/moul/zones/v0.GetZone("onyx")'`, got)
212
213 // Two approved zones on the same chain id: no -remote, rather than a guess.
214 must(reg.Edit("staging", zrev("staging"), zones.Info{ChainID: runtime.ChainID(), Title: "S", Kind: zones.Devnet,
215 RPCURL: "https://other.example.com"}, Admin, 1, "test"))
216 uassert.Equal(t, `gnokey query vm/qeval -data 'gno.land/r/moul/zones/v0.GetZone("onyx")'`, queryCommand(`GetZone("onyx")`))
217 uassert.ErrorContains(t, zones.ValidateEndpoint(zones.RPC, "https://rpc.example.com/'x"), "contains")
218}
219
220func TestRejectAndWithdraw(cur realm, t *testing.T) {
221 reset()
222 testing.SetRealm(testing.NewUserRealm(bob))
223 ProposeZone(cross(cur), "fake-mainnet", "gnoland-1", "Mainnet (faster!)", "",
224 "mainnet", "", "https://rpc.fake.example.com", "")
225 RegisterEndpoint(cross(cur), "fake-mainnet", "rpc", "https://bob.example.com", "")
226 // A stranger cannot pin a pending proposal with an endpoint of their own.
227 testing.SetRealm(testing.NewUserRealm(alice))
228 uassert.AbortsContains(t, cur, "only its proposer or a curator may register", func() {
229 RegisterEndpoint(cross(cur), "fake-mainnet", "rpc", "https://alice.example.com", "")
230 })
231 // A curator can.
232 testing.SetRealm(testing.NewUserRealm(Admin))
233 theirs := RegisterEndpoint(cross(cur), "fake-mainnet", "rpc", "https://curator.example.com", "")
234
235 uassert.AbortsContains(t, cur, "needs a reason", func() { RejectZone(cross(cur), "fake-mainnet", zrev("fake-mainnet"), " ") })
236 RejectZone(cross(cur), "fake-mainnet", zrev("fake-mainnet"), "not the mainnet RPC")
237 z, _ := GetZone("fake-mainnet")
238 uassert.Equal(t, string(zones.Rejected), string(z.Status))
239 uassert.Equal(t, "fake-mainnet", slugs(ListZones("rejected", "")))
240 uassert.Equal(t, "mainnet onyx staging moul-staging", slugs(ListZones("approved", "")))
241
242 // A rejected zone is the curators' record: its proposer cannot take it down
243 // (and so cannot collect the deposit the curator paid to record the
244 // rejection). A curator can.
245 testing.SetRealm(testing.NewUserRealm(bob))
246 uassert.AbortsContains(t, cur, "while it is pending, may remove", func() {
247 RemoveZone(cross(cur), "fake-mainnet", zrev("fake-mainnet"))
248 })
249 testing.SetRealm(testing.NewUserRealm(Admin))
250 RemoveEndpoint(cross(cur), theirs, erev(theirs))
251 RemoveZone(cross(cur), "fake-mainnet", zrev("fake-mainnet"))
252 _, ok := GetZone("fake-mainnet")
253 uassert.False(t, ok)
254
255 // A proposer withdrawing a PENDING zone waits out the review window, and cannot
256 // while it carries somebody else's endpoint: the refund for it, which they
257 // paid, would go to the proposer who signs.
258 testing.SetRealm(testing.NewUserRealm(bob))
259 ProposeZone(cross(cur), "bob-again", "bob-1", "Bob again", "", "devnet", "", "https://rpc.bob.example.com", "")
260 uassert.AbortsContains(t, cur, "may act on it again in", func() {
261 RemoveZone(cross(cur), "bob-again", zrev("bob-again"))
262 })
263 testing.SetRealm(testing.NewUserRealm(Admin))
264 RegisterEndpoint(cross(cur), "bob-again", "rpc", "https://curator2.example.com", "")
265 testing.SkipHeights(ReviewWindow)
266 testing.SetRealm(testing.NewUserRealm(bob))
267 uassert.AbortsContains(t, cur, "must be removed first", func() {
268 RemoveZone(cross(cur), "bob-again", zrev("bob-again"))
269 })
270}
271
272// A registrant withdraws their endpoint only after the review window: removing and
273// registering again every block would give it a new id faster than a curator
274// could flag the old one.
275func TestRegistrantWithdrawalWaits(cur realm, t *testing.T) {
276 reset()
277 testing.SetRealm(testing.NewUserRealm(bob))
278 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://cycle.example.com", "")
279 uassert.AbortsContains(t, cur, "may be withdrawn in", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
280 testing.SkipHeights(ReviewWindow)
281 RemoveEndpoint(cross(cur), id, erev(id))
282 _, ok := GetEndpoint(id)
283 uassert.False(t, ok)
284}
285
286func TestEndpointLifecycle(cur realm, t *testing.T) {
287 reset()
288 testing.SetRealm(testing.NewUserRealm(bob))
289 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://onyx-rpc.bob.example.com", "bob's node")
290 e, ok := GetEndpoint(id)
291 urequire.True(t, ok)
292 uassert.Equal(t, string(zones.Unverified), string(e.Status))
293 uassert.Equal(t, bob.String(), e.Registrant.String())
294
295 // Unverified is listed, and a verified-only read leaves it out.
296 uassert.Equal(t, 2, len(ListAddresses("onyx", "rpc", "")))
297 uassert.Equal(t, 1, len(ListAddresses("onyx", "rpc", "verified")))
298
299 uassert.AbortsContains(t, cur, "already lists that rpc", func() {
300 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://ONYX-rpc.bob.example.com", "")
301 })
302 uassert.AbortsContains(t, cur, "unknown endpoint kind", func() {
303 RegisterEndpoint(cross(cur), "onyx", "grpc", "https://x.example.com", "")
304 })
305 uassert.AbortsContains(t, cur, "no zone", func() {
306 RegisterEndpoint(cross(cur), "nope", "rpc", "https://x.example.com", "")
307 })
308
309 testing.SetRealm(testing.NewUserRealm(Admin))
310 uassert.AbortsContains(t, cur, "needs a reason", func() { FlagEndpoint(cross(cur), id, erev(id), "") })
311 FlagEndpoint(cross(cur), id, erev(id), "answers for the wrong chain id")
312 e, _ = GetEndpoint(id)
313 uassert.Equal(t, string(zones.Flagged), string(e.Status))
314 uassert.Equal(t, 1, len(ListEndpoints("onyx", "rpc", "flagged")))
315 UnverifyEndpoint(cross(cur), id, erev(id), "bob fixed it")
316 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "")
317 uassert.Equal(t, 2, len(ListAddresses("onyx", "rpc", "verified")))
318
319 // Flagged, it is a warning: its registrant cannot take it down (and so
320 // cannot wipe it by registering it again).
321 testing.SetRealm(testing.NewUserRealm(Admin))
322 FlagEndpoint(cross(cur), id, erev(id), "serves the wrong chain")
323 testing.SetRealm(testing.NewUserRealm(bob))
324 uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
325 // Unverified by a curator, it carries why: still not the registrant's to
326 // take down, even after the review window, or removing and registering it
327 // again would wipe the warning. A curator may.
328 testing.SetRealm(testing.NewUserRealm(Admin))
329 UnverifyEndpoint(cross(cur), id, erev(id), "key was sold; re-checking")
330 testing.SetRealm(testing.NewUserRealm(bob))
331 testing.SkipHeights(ReviewWindow)
332 uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
333 testing.SetRealm(testing.NewUserRealm(Admin))
334 RemoveEndpoint(cross(cur), id, erev(id))
335 _, ok = GetEndpoint(id)
336 uassert.False(t, ok)
337 uassert.AbortsContains(t, cur, "no endpoint", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
338}
339
340func TestCurators(cur realm, t *testing.T) {
341 reset()
342 uassert.True(t, IsCurator(Admin))
343 uassert.False(t, IsCurator(alice))
344
345 testing.SetRealm(testing.NewUserRealm(alice))
346 uassert.AbortsContains(t, cur, "is not a curator", func() { AddCurator(cross(cur), alice) })
347
348 testing.SetRealm(testing.NewUserRealm(Admin))
349 uassert.AbortsContains(t, cur, "is the last curator", func() { RemoveCurator(cross(cur), Admin) })
350 AddCurator(cross(cur), alice)
351 uassert.AbortsContains(t, cur, "already invited", func() { AddCurator(cross(cur), alice) })
352 // An invitation is not a seat: until alice accepts, the admin is still the
353 // last curator, so a mistyped invite cannot strand the registry.
354 uassert.True(t, IsInvited(alice))
355 uassert.False(t, IsCurator(alice))
356 uassert.Equal(t, 1, len(Curators()))
357 uassert.AbortsContains(t, cur, "is the last curator", func() { RemoveCurator(cross(cur), Admin) })
358 // Withdrawing an invitation works, and a withdrawn one cannot be accepted.
359 AddCurator(cross(cur), bob)
360 RemoveCurator(cross(cur), bob)
361 testing.SetRealm(testing.NewUserRealm(bob))
362 uassert.AbortsContains(t, cur, "has no curator invitation", func() { AcceptCurator(cross(cur)) })
363
364 testing.SetRealm(testing.NewUserRealm(alice))
365 AcceptCurator(cross(cur))
366 uassert.True(t, IsCurator(alice))
367 uassert.False(t, IsInvited(alice))
368 testing.SetRealm(testing.NewUserRealm(Admin))
369 uassert.AbortsContains(t, cur, "already a curator", func() { AddCurator(cross(cur), alice) })
370 uassert.Equal(t, 2, len(Curators()))
371
372 // A curator added by the admin curates on equal terms, the admin included.
373 testing.SetRealm(testing.NewUserRealm(alice))
374 ProposeZone(cross(cur), "alice-dev", "alicedev-1", "Alice's devnet", "", "devnet", "", "https://rpc.alice.example.com", "")
375 ApproveZone(cross(cur), "alice-dev", zrev("alice-dev"), "")
376 RemoveCurator(cross(cur), Admin)
377 uassert.False(t, IsCurator(Admin))
378 uassert.AbortsContains(t, cur, "is not a curator", func() { RemoveCurator(cross(cur), bob) })
379}
380
381// Every caller string a Render interpolates is escaped: a title that is a link,
382// a reason that would open a table column. The shapes that can carry markup at
383// all (slug, chain id, URL) are refused at write time instead, so they never
384// reach a Render.
385func TestRenderEscapesCallerText(cur realm, t *testing.T) {
386 reset()
387 testing.SetRealm(testing.NewUserRealm(alice))
388 ProposeZone(cross(cur), "evil", "evil-1", "[click](https://evil.example.com)", "**bold** | col",
389 "devnet", "", "https://rpc.evil.example.com", "")
390 RegisterEndpoint(cross(cur), "evil", "rpc", "https://rpc2.evil.example.com", "a|b")
391
392 page := Render("zone/evil")
393 uassert.False(t, strings.Contains(page, "[click](https://evil.example.com)"), "a title renders as text, not a link")
394 uassert.False(t, strings.Contains(page, "**bold**"), "a description renders as text, not markup")
395 uassert.True(t, strings.Contains(page, `a\|b`), "a label cannot open a table column")
396
397 // A pipe in a title cannot open a column in the zone tables.
398 ProposeZone(cross(cur), "evil-pipe", "evil-9", "Alice|official", "", "devnet", "", "https://rpc.pipe.example.com", "")
399 uassert.True(t, strings.Contains(Render("proposals"), `[Alice\|official](/r/moul/zones/v0:zone/evil-pipe)`))
400
401 // A scheme and host are stored lowercased, so the link is live (the
402 // sanitizer gno's md.Link uses accepts lowercase http and https only) and
403 // every later lowercasing is free; the path keeps its case.
404 testing.SetRealm(testing.NewUserRealm(bob))
405 ProposeZone(cross(cur), "upper", "upper-1", "Upper", "", "devnet", "HTTPS://Upper.example.com/Web", "HTTPS://rpc.upper.example.com", "")
406 z, _ := GetZone("upper")
407 uassert.Equal(t, "https://upper.example.com/Web", z.GnowebURL)
408 uassert.Equal(t, "https://rpc.upper.example.com", z.RPCURL)
409 // A proposal's URL is shown as code, to copy and check, not as a link.
410 uassert.True(t, strings.Contains(Render("zone/upper"), "`https://upper.example.com/Web`"))
411 uassert.False(t, strings.Contains(Render("zone/upper"), "](https://upper.example.com/Web)"))
412 id := RegisterEndpoint(cross(cur), "upper", "faucet", "HTTP://f.upper.example.com", "")
413 e, _ := GetEndpoint(id)
414 uassert.Equal(t, "http://f.upper.example.com", e.Address)
415 testing.SetRealm(testing.NewUserRealm(alice))
416
417 uassert.AbortsContains(t, cur, "contains", func() {
418 ProposeZone(cross(cur), "evil2", "evil-2", "x", "", "devnet", "", "https://rpc.evil.example.com/)[x](y", "")
419 })
420 uassert.AbortsContains(t, cur, "slug", func() {
421 ProposeZone(cross(cur), "evil|3", "evil-3", "x", "", "devnet", "", "https://rpc.evil.example.com", "")
422 })
423}
424
425func TestRenderPagesAnswer(t *testing.T) {
426 reset()
427 uassert.True(t, strings.Contains(Render("zone/nope"), "No such zone."))
428 uassert.True(t, strings.Contains(Render("nope"), "No such page."))
429 uassert.True(t, strings.Contains(Render("proposals"), "Nothing waiting for review."))
430 uassert.True(t, strings.Contains(Render("zone/staging"), "⚠️ flagged: did not answer when probed 2026\\-10\\-01"))
431}
432
433// Every list is paginated and clamps a reader's ?page=, which is what keeps a
434// full registry inside vm/qrender's gas. What a page reads is measured
435// separately; this test pins only the paging.
436func TestRenderPaginates(t *testing.T) {
437 reset()
438 for i := 0; i < 2*PageSize; i++ {
439 slug := "z" + strconv.Itoa(i)
440 must(reg.Propose(alice, 1, slug, info("c-"+strconv.Itoa(i), "Zone "+strconv.Itoa(i), "", "devnet", "", "https://r"+strconv.Itoa(i)+".example.com", "")))
441 must(reg.ReviewZone(slug, zones.Approved, zrev(slug), Admin, 2, ""))
442 }
443 // 4 seeded + 50 = 54 approved, so 3 pages of 25.
444 first := Render("")
445 uassert.True(t, strings.Contains(first, "page 1 of 3"))
446 uassert.True(t, strings.Contains(first, "Zone 20"))
447 uassert.False(t, strings.Contains(first, "Zone 21"), "row 26 is on page 2")
448 last := Render("?page=3")
449 uassert.True(t, strings.Contains(last, "Zone 49"))
450 uassert.False(t, strings.Contains(last, "Zone 45]"))
451 uassert.True(t, strings.Contains(last, "page 3 of 3"))
452 uassert.Equal(t, first, Render("?page=-1"))
453 uassert.Equal(t, last, Render("?page=99999999999999999"))
454 uassert.Equal(t, last, Render("?page=999999999999999999999999999999"), "past int is past the end, not page 1")
455 uassert.Equal(t, last, Render("?page=%2B999999999999999999999999999999"), "a + sign does not change that")
456 uassert.True(t, strings.Contains(Render("?page=%2B2"), "page 2 of 3"))
457 uassert.Equal(t, first, Render("?page=-999999999999999999999999999999"))
458 uassert.Equal(t, first, Render("?page=nope"))
459 uassert.True(t, strings.Contains(Render("?status=nope"), "No such list."))
460
461 for i := 0; i < PageSize+3; i++ {
462 who := testutils.TestAddress("reg" + strconv.Itoa(i/10))
463 _, err := reg.Register(who, 3, "z0", zones.RPC, "https://e"+strconv.Itoa(i)+".example.com", "")
464 must(err)
465 }
466 _, err := reg.Register(alice, 3, "z0", zones.Peer, nodeID+"@p.example.com:26656", "")
467 must(err)
468 zp := Render("zone/z0")
469 uassert.True(t, strings.Contains(zp, "page 1 of 2"))
470 uassert.True(t, strings.Contains(zp, "**all (29)**"))
471 uassert.True(t, strings.Contains(zp, `[rpc \(28\)](/r/moul/zones/v0:zone/z0?kind=rpc)`))
472 peers := Render("zone/z0?kind=peer")
473 uassert.True(t, strings.Contains(peers, "p.example.com"))
474 uassert.False(t, strings.Contains(peers, "e0.example.com"))
475 uassert.False(t, strings.Contains(peers, "page 1 of"), "one page needs no pager")
476 uassert.True(t, strings.Contains(Render("zone/z0?kind=rpc&page=2"), "[previous](/r/moul/zones/v0:zone/z0?kind=rpc&page=1)"))
477 uassert.True(t, strings.Contains(Render("zone/z0?kind=grpc"), "No such endpoint kind."))
478}
479
480// zrev is a zone's current revision, what a curator who just read it approves.
481func zrev(slug string) int64 {
482 z, _ := GetZone(slug)
483 return z.Revision
484}
485
486// erev is an endpoint's revision, what a verdict or a removal names.
487func erev(id int64) int64 {
488 e, ok := GetEndpoint(id)
489 if !ok {
490 return 0
491 }
492 return e.Revision
493}
494
495// zr is the revision of an endpoint's zone, what a verification also names.
496func zr(id int64) int64 {
497 e, ok := GetEndpoint(id)
498 if !ok {
499 return 0
500 }
501 return zrev(e.Zone)
502}
503
504// An invitation dies with its inviter, can be listed, and the set is bounded.
505func TestCuratorInvitations(cur realm, t *testing.T) {
506 reset()
507 testing.SetRealm(testing.NewUserRealm(Admin))
508 upper := address(strings.ToUpper(alice.String()))
509 uassert.AbortsContains(t, cur, "lowercase", func() { AddCurator(cross(cur), upper) })
510
511 AddCurator(cross(cur), alice)
512 testing.SetRealm(testing.NewUserRealm(alice))
513 AcceptCurator(cross(cur))
514 // alice invites bob, then is removed: bob's invitation goes with her.
515 AddCurator(cross(cur), bob)
516 uassert.Equal(t, bob.String(), Invited()[0].String())
517 testing.SetRealm(testing.NewUserRealm(Admin))
518 RemoveCurator(cross(cur), alice)
519 uassert.False(t, IsInvited(bob))
520 uassert.Equal(t, 0, len(Invited()))
521 testing.SetRealm(testing.NewUserRealm(bob))
522 uassert.AbortsContains(t, cur, "has no curator invitation", func() { AcceptCurator(cross(cur)) })
523
524 // Curators and open invitations together stop at MaxCurators.
525 testing.SetRealm(testing.NewUserRealm(Admin))
526 for i := 1; i < MaxCurators; i++ {
527 AddCurator(cross(cur), testutils.TestAddress("c"+strconv.Itoa(i)))
528 }
529 uassert.AbortsContains(t, cur, "remove one first", func() { AddCurator(cross(cur), bob) })
530 RemoveCurator(cross(cur), testutils.TestAddress("c1"))
531 AddCurator(cross(cur), bob)
532}
533
534func TestRenderRoutesAreExact(t *testing.T) {
535 reset()
536 uassert.True(t, strings.Contains(Render("zone/onyx/approved-by-gno-core"), "No such page."))
537 uassert.True(t, strings.Contains(Render("proposals/x"), "No such page."))
538 uassert.True(t, strings.Contains(Render("zone"), "No such page."))
539 uassert.True(t, strings.Contains(Render("zone/onyx?kind=%20rpc"), "No such endpoint kind."))
540 uassert.True(t, strings.Contains(Render("zone/onyx?kind=seed"), "No seed endpoint registered yet."))
541 uassert.True(t, strings.Contains(Render("zone/onyx"), "Approve revision") == false, "an approved zone offers no Approve")
542}
543
544// gnoweb turns an @name into a user-profile link with an icon; free text
545// carries a backslash before every @ so a label cannot vouch for an account.
546func TestMentionsAreEscaped(cur realm, t *testing.T) {
547 reset()
548 testing.SetRealm(testing.NewUserRealm(Admin))
549 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://m.example.com", "run by @gnocore")
550 page := Render("zone/onyx?kind=rpc")
551 uassert.True(t, strings.Contains(page, `run by \@gnocore`))
552 uassert.False(t, strings.Contains(page, "run by @gnocore"))
553}
554
555// The printed -remote is never one the same page flags.
556func TestQueryCommandSkipsAFlaggedRemote(cur realm, t *testing.T) {
557 reset()
558 z, _ := GetZone("moul-staging")
559 in := zones.Info{ChainID: runtime.ChainID(), Title: z.Title, Kind: z.Kind, RPCURL: z.RPCURL, GnowebURL: z.GnowebURL}
560 must(reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "test"))
561 uassert.True(t, strings.Contains(queryCommand("X()"), "-remote 'https://rpc.gno-staging.moul.p2p.team'"))
562 e, ok := reg.EndpointByAddress("moul-staging", zones.RPC, z.RPCURL)
563 urequire.True(t, ok)
564 must(reg.ReviewEndpoint(e.ID, zones.Flagged, 0, erev(e.ID), Admin, 2, "down"))
565 uassert.False(t, strings.Contains(queryCommand("X()"), "-remote"))
566}
567
568// A proposer cannot wipe a curator's verdict by removing the zone and
569// proposing it again.
570func TestProposerCannotRemoveOverAVerdict(cur realm, t *testing.T) {
571 reset()
572 testing.SetRealm(testing.NewUserRealm(bob))
573 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
574 id := RegisterEndpoint(cross(cur), "bobs", "peer", nodeID+"@p.bobs.example.com:26656", "")
575 testing.SetRealm(testing.NewUserRealm(Admin))
576 FlagEndpoint(cross(cur), id, erev(id), "malicious peer")
577 testing.SetRealm(testing.NewUserRealm(bob))
578 testing.SkipHeights(ReviewWindow)
579 uassert.AbortsContains(t, cur, "a curator ruled on", func() {
580 RemoveZone(cross(cur), "bobs", zrev("bobs"))
581 })
582}
583
584// A bare g1 address in free text would become a profile link: its 1 is
585// written as a character reference so the mention parser does not match.
586func TestBareAddressesAreNotMentions(cur realm, t *testing.T) {
587 reset()
588 testing.SetRealm(testing.NewUserRealm(Admin))
589 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://g.example.com", "run by "+Admin.String())
590 page := Render("zone/onyx?kind=rpc")
591 uassert.False(t, strings.Contains(page, "run by g1manfred"))
592 uassert.True(t, strings.Contains(page, "run by g1manfred"))
593}
594
595// The main RPC is marked where it is shown when its endpoint is flagged, so the
596// official table and the zone page agree (TestQueryCommandSkipsAFlaggedRemote
597// covers the printed command).
598func TestFlaggedMainRPCIsMarked(t *testing.T) {
599 reset()
600 uassert.True(t, strings.Contains(Render(""), "`https://rpc.staging.gno.land` ⚠️ flagged"))
601 uassert.True(t, strings.Contains(Render("zone/staging"), "`https://rpc.staging.gno.land` ⚠️ flagged"))
602 uassert.False(t, strings.Contains(Render(""), "`https://rpc.gno.land` ⚠️"))
603}
604
605// A flagged gnoweb URL is shown as code and marked, never as a link the page's
606// own endpoint table says not to use.
607func TestFlaggedGnowebIsNotALink(t *testing.T) {
608 reset()
609 page := Render("zone/staging")
610 uassert.True(t, strings.Contains(page, "`https://staging.gno.land` ⚠️ flagged"))
611 uassert.False(t, strings.Contains(page, "](https://staging.gno.land)"))
612}
613
614// A curator's flag on an rpc endpoint under the tcp:// spelling still marks a
615// main RPC written as http://, and the reverse: gnokey dials them alike.
616func TestFlaggedRPCMatchesAcrossTcpAndHttp(cur realm, t *testing.T) {
617 reset()
618 testing.SetRealm(testing.NewUserRealm(Admin))
619 z, _ := GetZone("onyx")
620 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "tcp://rpc.tcp.example.com:26657", "")
621 FlagEndpoint(cross(cur), id, erev(id), "down")
622 in := zones.Info{ChainID: z.ChainID, Title: z.Title, Description: z.Description, Kind: z.Kind,
623 GnowebURL: z.GnowebURL, RPCURL: "http://rpc.tcp.example.com:26657", GenesisURL: z.GenesisURL}
624 must(reg.Edit("onyx", zrev("onyx"), in, Admin, 1, "moved"))
625 uassert.True(t, strings.Contains(Render("zone/onyx"), "`http://rpc.tcp.example.com:26657` ⚠️ flagged"))
626
627 id = RegisterEndpoint(cross(cur), "onyx", "rpc", "http://rpc.http.example.com:26657", "")
628 FlagEndpoint(cross(cur), id, erev(id), "down")
629 in.RPCURL = "tcp://rpc.http.example.com:26657"
630 must(reg.Edit("onyx", zrev("onyx"), in, Admin, 2, "moved back"))
631 uassert.True(t, strings.Contains(Render("zone/onyx"), "`tcp://rpc.http.example.com:26657` ⚠️ flagged"))
632}
633
634// A page offers an action only while somebody's call can succeed: with the
635// review queue full, Propose stays for curators only; with the live registry
636// full, Propose and a rejected zone's Approve give way to a note.
637func TestFullQueuesSayWhoMayStillAct(t *testing.T) {
638 reset()
639 in := func(i int) zones.Info {
640 return zones.Info{ChainID: "fill-" + strconv.Itoa(i), Title: "Fill", Kind: zones.Testnet,
641 RPCURL: "https://rpc" + strconv.Itoa(i) + ".example.com"}
642 }
643 slug := func(i int) string { return "fill-" + strconv.Itoa(i) }
644 who := func(i int) address { return testutils.TestAddress("p" + strconv.Itoa(i/zones.MaxPendingPerProposer)) }
645
646 must(reg.Propose(alice, 1, "nope", in(-1)))
647 must(reg.ReviewZone("nope", zones.Rejected, zrev("nope"), Admin, 1, "no"))
648 uassert.True(t, strings.Contains(Render("zone/nope"), "Approve revision"))
649
650 for i := 0; i < zones.MaxPending; i++ {
651 must(reg.Propose(who(i), 2, slug(i), in(i)))
652 }
653 // The queue is full for everybody but curators: the note says so, and the
654 // link stays, labelled for them.
655 uassert.True(t, strings.Contains(Render(""), "the review queue is full at 64 proposals"))
656 uassert.True(t, strings.Contains(Render(""), "curators only, while full"))
657
658 // Approve the queue away, then keep proposing and approving until the live
659 // registry is full: no proposal is waiting, so Propose is hidden by the
660 // live cap alone.
661 for i := 0; i < zones.MaxPending; i++ {
662 must(reg.ReviewZone(slug(i), zones.Approved, zrev(slug(i)), Admin, 3, ""))
663 }
664 uassert.True(t, strings.Contains(Render(""), "func=ProposeZone"))
665 for i := zones.MaxPending; reg.Live() < zones.MaxZones; i++ {
666 must(reg.ProposeExempt(who(i), 4, slug(i), in(i)))
667 must(reg.ReviewZone(slug(i), zones.Approved, zrev(slug(i)), Admin, 4, ""))
668 }
669 uassert.Equal(t, 0, reg.ZoneCount(zones.Pending))
670 uassert.False(t, strings.Contains(Render("zone/nope"), "Approve revision"))
671 uassert.True(t, strings.Contains(Render("zone/nope"), "approving it waits for a free place"))
672 uassert.False(t, strings.Contains(Render(""), "func=ProposeZone"), "the live cap binds curators too")
673 uassert.True(t, strings.Contains(Render(""), "the registry is full at 256 live zones"))
674}
675
676// Withdrawing the zone is no way round an endpoint's own review window: the
677// proposer removing it the block an endpoint appears would delete it before a
678// curator could see it.
679func TestRemoveZoneWaitsForItsEndpoints(cur realm, t *testing.T) {
680 reset()
681 testing.SetRealm(testing.NewUserRealm(bob))
682 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
683 testing.SkipHeights(ReviewWindow)
684 RegisterEndpoint(cross(cur), "bobs", "rpc", "https://fresh.bobs.example.com", "")
685 uassert.AbortsContains(t, cur, "may be withdrawn in", func() { RemoveZone(cross(cur), "bobs", zrev("bobs")) })
686 testing.SkipHeights(ReviewWindow)
687 RemoveZone(cross(cur), "bobs", zrev("bobs"))
688 _, ok := GetZone("bobs")
689 uassert.False(t, ok)
690}
691
692// A rejected zone is a record, endpoints and all: its registrant cannot strip
693// the evidence a rejection cites.
694func TestRejectedZoneKeepsItsEndpoints(cur realm, t *testing.T) {
695 reset()
696 testing.SetRealm(testing.NewUserRealm(bob))
697 ProposeZone(cross(cur), "fake", "fake-1", "Fake", "", "devnet", "", "https://rpc.fake.example.com", "")
698 id := RegisterEndpoint(cross(cur), "fake", "rpc", "https://phish.example.com", "")
699 testing.SetRealm(testing.NewUserRealm(Admin))
700 RejectZone(cross(cur), "fake", zrev("fake"), "its rpc is a phishing clone")
701 testing.SetRealm(testing.NewUserRealm(bob))
702 testing.SkipHeights(ReviewWindow)
703 uassert.AbortsContains(t, cur, "is rejected, a record", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
704}
705
706// Curators pass the admission gates: a flood that fills the review queue does
707// not lock out the people who clear it. The hard caps still hold.
708func TestCuratorsPassTheAdmissionGates(cur realm, t *testing.T) {
709 reset()
710 in := func(i int) zones.Info {
711 return zones.Info{ChainID: "f-" + strconv.Itoa(i), Title: "F", Kind: zones.Testnet,
712 RPCURL: "https://rpc" + strconv.Itoa(i) + ".example.com"}
713 }
714 for i := 0; i < zones.MaxPending; i++ {
715 must(reg.Propose(testutils.TestAddress("p"+strconv.Itoa(i/zones.MaxPendingPerProposer)), 1, "f-"+strconv.Itoa(i), in(i)))
716 }
717 testing.SetRealm(testing.NewUserRealm(bob))
718 uassert.AbortsContains(t, cur, "waiting for review", func() {
719 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
720 })
721 testing.SetRealm(testing.NewUserRealm(Admin))
722 ProposeZone(cross(cur), "admins", "admins-1", "Admin's", "", "devnet", "", "https://rpc.admins.example.com", "")
723 for i := 0; i < zones.MaxPendingPerProposer; i++ {
724 ProposeZone(cross(cur), "a-"+strconv.Itoa(i), "a-"+strconv.Itoa(i), "A", "", "devnet", "", "https://rpc.a"+strconv.Itoa(i)+".example.com", "")
725 }
726
727 for i := 0; i < zones.MaxUnverifiedPerZone; i++ {
728 _, err := reg.Register(testutils.TestAddress("r"+strconv.Itoa(i/zones.MaxEndpointsPerAddress)), 2, "onyx", zones.RPC,
729 "https://n"+strconv.Itoa(i)+".example.com", "")
730 must(err)
731 }
732 testing.SetRealm(testing.NewUserRealm(bob))
733 uassert.AbortsContains(t, cur, "waiting for review", func() {
734 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "")
735 })
736 testing.SetRealm(testing.NewUserRealm(Admin))
737 for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone; i++ {
738 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://c"+strconv.Itoa(i)+".example.com", "")
739 }
740 uassert.AbortsContains(t, cur, "is full at", func() {
741 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://over.example.com", "")
742 })
743}
744
745// A flag can be restated with a new reason, so correcting one never passes
746// through a state its registrant could remove it from.
747func TestAFlagsReasonCanBeAmended(cur realm, t *testing.T) {
748 reset()
749 testing.SetRealm(testing.NewUserRealm(bob))
750 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://typo.example.com", "")
751 testing.SetRealm(testing.NewUserRealm(Admin))
752 FlagEndpoint(cross(cur), id, erev(id), "srves the wrong chian")
753 FlagEndpoint(cross(cur), id, erev(id), "serves the wrong chain")
754 e, _ := GetEndpoint(id)
755 uassert.Equal(t, "serves the wrong chain", e.Reason)
756 uassert.AbortsContains(t, cur, "already flagged", func() { FlagEndpoint(cross(cur), id, erev(id), "serves the wrong chain") })
757 uassert.Equal(t, 0, reg.Awaiting("onyx"), "restating a flag does not count it twice")
758}
759
760// Only a flag under the kind a URL is shown as marks it: a listing under
761// another kind is anybody's to make, so its flag never marks the zone's own
762// URL. The genesis URL, which no kind lists, is never marked.
763func TestOnlyTheShownKindsFlagMarks(cur realm, t *testing.T) {
764 reset()
765 testing.SetRealm(testing.NewUserRealm(bob))
766 z, _ := GetZone("mainnet")
767 misKinded := []int64{
768 RegisterEndpoint(cross(cur), "mainnet", "explorer", z.RPCURL, ""),
769 RegisterEndpoint(cross(cur), "mainnet", "faucet", z.GnowebURL, ""),
770 RegisterEndpoint(cross(cur), "mainnet", "explorer", z.GenesisURL, ""),
771 }
772 testing.SetRealm(testing.NewUserRealm(Admin))
773 for _, id := range misKinded {
774 FlagEndpoint(cross(cur), id, erev(id), "not that kind")
775 }
776 page := Render("zone/mainnet")
777 uassert.False(t, strings.Contains(page, "` ⚠️ flagged"), "no fact is marked by another kind's flag")
778 uassert.True(t, strings.Contains(page, "](https://gno.land)"))
779 uassert.False(t, strings.Contains(Render(""), "`"+z.RPCURL+"` ⚠️"))
780
781 // Its own kind's flag does mark it.
782 e, ok := reg.EndpointByAddress("mainnet", zones.Gnoweb, z.GnowebURL)
783 urequire.True(t, ok)
784 FlagEndpoint(cross(cur), e.ID, erev(e.ID), "a phishing clone took the name")
785 page = Render("zone/mainnet")
786 uassert.True(t, strings.Contains(page, "`https://gno.land` ⚠️ flagged"))
787 uassert.False(t, strings.Contains(page, "](https://gno.land)"))
788}
789
790// The note in place of an action says which cap is full, and nothing more:
791// advice on what frees it could name the zone the note is shown on.
792func TestFullCapsSayWhichIsFull(cur realm, t *testing.T) {
793 reset()
794 testing.SetRealm(testing.NewUserRealm(Admin))
795 for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone; i++ {
796 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://c"+strconv.Itoa(i)+".example.com", "")
797 FlagEndpoint(cross(cur), id, erev(id), "spam")
798 }
799 page := Render("zone/onyx")
800 uassert.True(t, strings.Contains(page, "endpoints are full at 128"))
801 uassert.False(t, strings.Contains(page, "func=RegisterEndpoint"))
802
803 for i := 0; reg.EndpointCount("mainnet", "") < zones.MaxUnverifiedPerZone+8; i++ {
804 RegisterEndpoint(cross(cur), "mainnet", "rpc", "https://m"+strconv.Itoa(i)+".example.com", "")
805 }
806 uassert.True(t, reg.Awaiting("mainnet") >= zones.MaxUnverifiedPerZone)
807 page = Render("zone/mainnet")
808 uassert.True(t, strings.Contains(page, "the review queue is full at 64 endpoints awaiting review"))
809 uassert.True(t, strings.Contains(page, "curators only, while full"))
810}
811
812// An edited zone names its editor in full, as it does its proposer.
813func TestEditorIsShownInFull(cur realm, t *testing.T) {
814 reset()
815 testing.SetRealm(testing.NewUserRealm(Admin))
816 z, _ := GetZone("onyx")
817 EditZone(cross(cur), "onyx", z.Revision, z.ChainID, "Onyx, edited", z.Description, string(z.Kind),
818 z.GnowebURL, z.RPCURL, z.GenesisURL, "a better title")
819 uassert.True(t, strings.Contains(Render("zone/onyx"), "**edited** by `"+Admin.String()+"`"))
820}
821
822// A curator clears a cycling flood in one call; whatever a curator ruled on
823// stays.
824func TestClearUnreviewed(cur realm, t *testing.T) {
825 reset()
826 testing.SetRealm(testing.NewUserRealm(bob))
827 spam := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam1.example.com", "")
828 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam2.example.com", "")
829 ruled := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://ruled.example.com", "")
830 uassert.AbortsContains(t, cur, "is not a curator", func() { ClearUnreviewed(cross(cur), "onyx", reg.Revision()) })
831 testing.SetRealm(testing.NewUserRealm(Admin))
832 UnverifyEndpoint(cross(cur), ruled, erev(ruled), "looking into it")
833 uassert.Equal(t, 2, ClearUnreviewed(cross(cur), "onyx", reg.Revision()))
834 _, ok := GetEndpoint(spam)
835 uassert.False(t, ok)
836 _, ok = GetEndpoint(ruled)
837 uassert.True(t, ok, "a curator's ruling is kept")
838 uassert.Equal(t, 1, reg.OwnerCount("onyx", bob))
839 uassert.Equal(t, 0, ClearUnreviewed(cross(cur), "onyx", reg.Revision()))
840}
841
842// The review windows are the full ReviewWindow, a block short is refused, and
843// the zone's runs from its last edit by anybody.
844func TestReviewWindowsAreExact(cur realm, t *testing.T) {
845 reset()
846 testing.SetRealm(testing.NewUserRealm(bob))
847 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
848 id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://a.bobs.example.com", "")
849 testing.SkipHeights(ReviewWindow - 1)
850 uassert.AbortsContains(t, cur, "may be withdrawn in 1 blocks", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
851 uassert.AbortsContains(t, cur, "may act on it again in 1 blocks", func() {
852 RemoveZone(cross(cur), "bobs", zrev("bobs"))
853 })
854 testing.SkipHeights(1)
855 // A curator's edit restarts the proposer's window: it runs from the last
856 // edit by anybody.
857 testing.SetRealm(testing.NewUserRealm(Admin))
858 z, _ := GetZone("bobs")
859 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, "Bob's, retitled", z.Description, string(z.Kind),
860 z.GnowebURL, z.RPCURL, z.GenesisURL, "")
861 testing.SetRealm(testing.NewUserRealm(bob))
862 uassert.AbortsContains(t, cur, "may act on it again in", func() {
863 RemoveZone(cross(cur), "bobs", zrev("bobs"))
864 })
865}
866
867// A proposer's edit off local drops private endpoints only under the rules a
868// removal by the proposer has: never somebody else's, never one a curator
869// ruled on.
870func TestProposerLeavingLocalMeetsTheWithdrawalRules(cur realm, t *testing.T) {
871 reset()
872 testing.SetRealm(testing.NewUserRealm(bob))
873 ProposeZone(cross(cur), "lab", "lab-1", "Lab", "", "local", "", "http://127.0.0.1:26657", "")
874 mine := RegisterEndpoint(cross(cur), "lab", "rpc", "http://10.1.2.3:26657", "")
875 leave := func() {
876 z, _ := GetZone("lab")
877 EditZone(cross(cur), "lab", z.Revision, z.ChainID, z.Title, z.Description, "devnet",
878 "", "https://rpc.lab.example.com", "", "")
879 }
880 testing.SetRealm(testing.NewUserRealm(Admin))
881 theirs := RegisterEndpoint(cross(cur), "lab", "rpc", "http://10.9.9.9:26657", "")
882 testing.SetRealm(testing.NewUserRealm(bob))
883 testing.SkipHeights(ReviewWindow)
884 uassert.AbortsContains(t, cur, "which somebody else registered", leave)
885 testing.SetRealm(testing.NewUserRealm(Admin))
886 RemoveEndpoint(cross(cur), theirs, erev(theirs))
887 FlagEndpoint(cross(cur), mine, erev(mine), "a trap")
888 testing.SetRealm(testing.NewUserRealm(bob))
889 uassert.AbortsContains(t, cur, "a curator ruled on", leave)
890 testing.SetRealm(testing.NewUserRealm(Admin))
891 UnverifyEndpoint(cross(cur), mine, erev(mine), "")
892 RemoveEndpoint(cross(cur), mine, erev(mine))
893 testing.SetRealm(testing.NewUserRealm(bob))
894 fresh := RegisterEndpoint(cross(cur), "lab", "rpc", "http://10.4.4.4:26657", "")
895 // Dropping it is withdrawing it, so it waits out its own review window.
896 uassert.AbortsContains(t, cur, "may be withdrawn in", leave)
897 testing.SkipHeights(ReviewWindow)
898 leave()
899 _, ok := GetEndpoint(fresh)
900 uassert.False(t, ok, "the proposer's own, never ruled on, is dropped")
901}
902
903// A reset reaches only a verified endpoint, which its registrant could
904// withdraw, and it says the zone changed, not the endpoint: so it leaves that
905// withdrawal in place, after a chain-id edit and after a retirement undone.
906func TestAResetLeavesTheWithdrawal(cur realm, t *testing.T) {
907 reset()
908 testing.SetRealm(testing.NewUserRealm(bob))
909 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
910 id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://a.bobs.example.com", "")
911 id2 := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://b.bobs.example.com", "")
912 testing.SetRealm(testing.NewUserRealm(Admin))
913 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "answers bobs-1")
914 VerifyEndpoint(cross(cur), id2, zr(id2), erev(id2), "answers bobs-1")
915 testing.SetRealm(testing.NewUserRealm(bob))
916 testing.SkipHeights(ReviewWindow)
917 z, _ := GetZone("bobs")
918 EditZone(cross(cur), "bobs", z.Revision, "bobs-2", z.Title, z.Description, string(z.Kind),
919 z.GnowebURL, z.RPCURL, z.GenesisURL, "")
920 e, _ := GetEndpoint(id)
921 uassert.Equal(t, "", e.ReviewedBy.String())
922 uassert.Equal(t, zones.ChainIDChanged, e.Reason)
923 RemoveEndpoint(cross(cur), id, erev(id))
924 _, ok := GetEndpoint(id)
925 uassert.False(t, ok, "a chain-id reset leaves the withdrawal")
926
927 testing.SetRealm(testing.NewUserRealm(Admin))
928 VerifyEndpoint(cross(cur), id2, zr(id2), erev(id2), "answers bobs-2")
929 ApproveZone(cross(cur), "bobs", zr(id2), "")
930 RetireZone(cross(cur), "bobs", zr(id2), "shut down")
931 ApproveZone(cross(cur), "bobs", zr(id2), "back")
932 e, _ = GetEndpoint(id2)
933 uassert.Equal(t, zones.ZoneRetired, e.Reason)
934 testing.SetRealm(testing.NewUserRealm(bob))
935 RemoveEndpoint(cross(cur), id2, erev(id2))
936 _, ok = GetEndpoint(id2)
937 uassert.False(t, ok, "a retirement's reset leaves it too")
938
939 // A curator's own unverify is still a warning.
940 id3 := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://c.bobs.example.com", "")
941 testing.SetRealm(testing.NewUserRealm(Admin))
942 UnverifyEndpoint(cross(cur), id3, erev(id3), "answers a different chain")
943 testing.SetRealm(testing.NewUserRealm(bob))
944 testing.SkipHeights(ReviewWindow)
945 uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id3, erev(id3)) })
946}
947
948// The zone's own main RPC and gnoweb, under their own kind, are its proposer's
949// or a curator's to list: a stranger's listing, flagged for its label, would
950// mark the zone's own URL.
951func TestTheZonesOwnURLsAreReserved(cur realm, t *testing.T) {
952 reset()
953 z, _ := GetZone("onyx")
954 reg.RemoveEndpoint(mustRPC(t, "onyx", z.RPCURL))
955 ge, ok := reg.EndpointByAddress("onyx", zones.Gnoweb, z.GnowebURL)
956 urequire.True(t, ok)
957 reg.RemoveEndpoint(ge.ID, ge.Revision)
958 testing.SetRealm(testing.NewUserRealm(bob))
959 for _, a := range []string{strings.ToUpper(z.RPCURL), " " + z.RPCURL, z.RPCURL + "\t"} {
960 uassert.AbortsContains(t, cur, "onyx's own rpc", func() {
961 RegisterEndpoint(cross(cur), "onyx", "rpc", a, "moved: use mine")
962 })
963 }
964 uassert.AbortsContains(t, cur, "onyx's own gnoweb", func() {
965 RegisterEndpoint(cross(cur), "onyx", "gnoweb", z.GnowebURL+"\t", "")
966 })
967 // Under another kind it is anybody's, and its flag marks nothing.
968 RegisterEndpoint(cross(cur), "onyx", "explorer", z.RPCURL, "")
969 testing.SetRealm(testing.NewUserRealm(Admin))
970 RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL, "gno core")
971}
972
973// mustRPC returns the id and revision of the rpc endpoint listing url.
974func mustRPC(t *testing.T, slug, url string) (int64, int64) {
975 e, ok := reg.EndpointByAddress(slug, zones.RPC, url)
976 urequire.True(t, ok, url)
977 return e.ID, e.Revision
978}
979
980// ClearUnreviewed takes only what the curator read: nothing registered after
981// the revision they name, nothing a curator registered, nothing a curator
982// ruled on (an unverify with no reason included), nothing a reset left.
983func TestClearUnreviewedIsBoundToWhatWasRead(cur realm, t *testing.T) {
984 reset()
985 testing.SetRealm(testing.NewUserRealm(Admin))
986 uassert.AbortsContains(t, cur, "no zone", func() { ClearUnreviewed(cross(cur), "nope", reg.Revision()) })
987 mine := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://curators-own.example.com", "")
988 testing.SetRealm(testing.NewUserRealm(bob))
989 spam := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam.example.com", "")
990 ruled := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://ruled.example.com", "")
991 testing.SetRealm(testing.NewUserRealm(Admin))
992 FlagEndpoint(cross(cur), ruled, erev(ruled), "checking")
993 UnverifyEndpoint(cross(cur), ruled, erev(ruled), "")
994 read := reg.Revision()
995 testing.SetRealm(testing.NewUserRealm(alice))
996 late := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://late.example.com", "alice's node")
997 testing.SetRealm(testing.NewUserRealm(Admin))
998 uassert.Equal(t, 1, ClearUnreviewed(cross(cur), "onyx", read))
999 for id, kept := range map[int64]bool{mine: true, spam: false, ruled: true, late: true} {
1000 _, ok := GetEndpoint(id)
1001 uassert.Equal(t, kept, ok, strconv.FormatInt(id, 10))
1002 }
1003 uassert.True(t, strings.Contains(Render("zone/onyx"), "func=ClearUnreviewed"))
1004}
1005
1006// A removal names the endpoint's revision: one written before a colleague's
1007// verdict fails rather than delete it unseen. A verified endpoint is its
1008// registrant's to take down; listing it again starts it unverified.
1009func TestRemovalIsBoundAndAVerifiedListingIsWithdrawable(cur realm, t *testing.T) {
1010 reset()
1011 uassert.Equal(t, int64(100), int64(ReviewWindow))
1012 testing.SetRealm(testing.NewUserRealm(bob))
1013 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bobs-node.example.com", "")
1014 read := erev(id)
1015 testing.SetRealm(testing.NewUserRealm(Admin))
1016 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "answers onyx-1")
1017 uassert.AbortsContains(t, cur, "changed since you read it", func() { RemoveEndpoint(cross(cur), id, read) })
1018 testing.SetRealm(testing.NewUserRealm(bob))
1019 testing.SkipHeights(ReviewWindow)
1020 RemoveEndpoint(cross(cur), id, erev(id))
1021 _, ok := GetEndpoint(id)
1022 uassert.False(t, ok)
1023}
1024
1025// A proposer leaving local keeps every public endpoint, a curator's included:
1026// the withdrawal rules apply only to what the edit drops.
1027func TestLeavingLocalKeepsOthersPublicEndpoints(cur realm, t *testing.T) {
1028 reset()
1029 testing.SetRealm(testing.NewUserRealm(bob))
1030 ProposeZone(cross(cur), "lab", "lab-1", "Lab", "", "local", "", "http://127.0.0.1:26657", "")
1031 testing.SetRealm(testing.NewUserRealm(Admin))
1032 pub := RegisterEndpoint(cross(cur), "lab", "rpc", "https://rpc.lab.example.com", "")
1033 testing.SetRealm(testing.NewUserRealm(bob))
1034 testing.SkipHeights(ReviewWindow)
1035 z, _ := GetZone("lab")
1036 EditZone(cross(cur), "lab", z.Revision, z.ChainID, z.Title, z.Description, "devnet", "", "https://rpc.lab.example.com", "", "")
1037 _, ok := GetEndpoint(pub)
1038 uassert.True(t, ok)
1039}
1040
1041// The index's example falls back to onyx when this chain's zone lists no
1042// peer, rather than print a query certain to return nothing.
1043func TestTheIndexExampleFallsBack(t *testing.T) {
1044 reset()
1045 z, _ := GetZone("moul-staging")
1046 in := zones.Info{ChainID: runtime.ChainID(), Title: z.Title, Kind: z.Kind, RPCURL: z.RPCURL, GnowebURL: z.GnowebURL}
1047 must(reg.Edit("moul-staging", zrev("moul-staging"), in, Admin, 1, "on this chain"))
1048 uassert.Equal(t, 0, reg.EndpointCount("moul-staging", zones.Peer))
1049 uassert.True(t, strings.Contains(Render(""), `ListAddresses("onyx", "peer", "verified")`))
1050}
1051
1052// ClearUnreviewed keeps an endpoint a reset left with a reason: a curator
1053// verified it, and the proposer's chain-id edit undid that.
1054func TestClearKeepsAResetEndpoint(cur realm, t *testing.T) {
1055 reset()
1056 testing.SetRealm(testing.NewUserRealm(bob))
1057 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1058 id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://a.bobs.example.com", "")
1059 testing.SetRealm(testing.NewUserRealm(Admin))
1060 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "")
1061 testing.SetRealm(testing.NewUserRealm(bob))
1062 testing.SkipHeights(ReviewWindow)
1063 z, _ := GetZone("bobs")
1064 EditZone(cross(cur), "bobs", z.Revision, "bobs-2", z.Title, z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "")
1065 testing.SetRealm(testing.NewUserRealm(Admin))
1066 uassert.Equal(t, 0, ClearUnreviewed(cross(cur), "bobs", reg.Revision()))
1067}
1068
1069// The reservation exempts any curator, and the proposer while the zone is
1070// pending, each alone: every proposer right ends at approval.
1071func TestTheReservationsExemptions(cur realm, t *testing.T) {
1072 reset()
1073 testing.SetRealm(testing.NewUserRealm(bob))
1074 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "https://gnoweb.bobs.example.com", "https://rpc.bobs.example.com", "")
1075 RegisterEndpoint(cross(cur), "bobs", "rpc", "https://rpc.bobs.example.com", "mine") // the proposer, pending
1076 testing.SetRealm(testing.NewUserRealm(Admin))
1077 ApproveZone(cross(cur), "bobs", zrev("bobs"), "")
1078 testing.SetRealm(testing.NewUserRealm(bob))
1079 uassert.AbortsContains(t, cur, "only a curator, or its proposer while it is pending", func() {
1080 RegisterEndpoint(cross(cur), "bobs", "gnoweb", "https://gnoweb.bobs.example.com", "mine too")
1081 })
1082 testing.SetRealm(testing.NewUserRealm(Admin))
1083 RegisterEndpoint(cross(cur), "bobs", "gnoweb", "https://gnoweb.bobs.example.com", "") // a curator, not the proposer
1084}
1085
1086// On an approved zone the proposer's listing of a new own URL is anybody's
1087// listing: a curator's edit drops it if nobody ruled on it, rather than adopt
1088// it as the zone's own.
1089func TestApprovedProposersListingIsNotAdopted(cur realm, t *testing.T) {
1090 reset()
1091 testing.SetRealm(testing.NewUserRealm(bob))
1092 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1093 testing.SetRealm(testing.NewUserRealm(Admin))
1094 ApproveZone(cross(cur), "bobs", zrev("bobs"), "")
1095 testing.SetRealm(testing.NewUserRealm(bob))
1096 id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://rpc2.bobs.example.com", "official next")
1097 testing.SetRealm(testing.NewUserRealm(Admin))
1098 z, _ := GetZone("bobs")
1099 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind),
1100 z.GnowebURL, "https://rpc2.bobs.example.com", z.GenesisURL, "moved")
1101 _, ok := GetEndpoint(id)
1102 uassert.False(t, ok, "the proposer's unruled listing is dropped, not adopted")
1103}
1104
1105// An edit cannot make a stranger's listing the zone's own. One nobody ruled on
1106// is dropped in the edit, so pre-listing a zone's next URL cannot hold its
1107// move off; one a curator ruled on refuses the edit until a curator removes
1108// it, however the URL is padded. Each case starts fresh: a test does not roll
1109// back an aborted call's writes, a transaction does.
1110func TestAnEditCannotAdoptAStrangersListing(cur realm, t *testing.T) {
1111 setup := func() zones.Zone {
1112 reset()
1113 _, err := reg.Register(bob, 1, "onyx", zones.RPC, "https://rpc2.onyx.example.com", "official onyx rpc")
1114 must(err)
1115 for _, l := range [][2]string{{"gnoweb", "https://web2.onyx.example.com"}, {"rpc", "https://rpc3.onyx.example.com"}} {
1116 k, _ := zones.ParseEndpointKind(l[0])
1117 id, err := reg.Register(bob, 1, "onyx", k, l[1], "official onyx")
1118 must(err)
1119 must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "not run by the onyx team"))
1120 }
1121 z, _ := GetZone("onyx")
1122 return z
1123 }
1124 testing.SetRealm(testing.NewUserRealm(Admin))
1125 edit := func(z zones.Zone, gnoweb, rpc string) func() {
1126 return func() {
1127 EditZone(cross(cur), "onyx", zrev("onyx"), z.ChainID, z.Title+".", z.Description, string(z.Kind),
1128 gnoweb, rpc, z.GenesisURL, "moved")
1129 }
1130 }
1131 for _, c := range [][2]string{{"https://web2.onyx.example.com", ""}, {" https://web2.onyx.example.com\t", ""}, {"", " https://rpc3.onyx.example.com"}} {
1132 z := setup()
1133 gw, rpc := c[0], c[1]
1134 if gw == "" {
1135 gw = z.GnowebURL
1136 }
1137 if rpc == "" {
1138 rpc = z.RPCURL
1139 }
1140 uassert.AbortsContains(t, cur, "with a curator's ruling", edit(z, gw, rpc))
1141 }
1142 z := setup()
1143 squat, ok := reg.EndpointByAddress("onyx", zones.RPC, "https://rpc2.onyx.example.com")
1144 urequire.True(t, ok)
1145 edit(z, z.GnowebURL, "https://rpc2.onyx.example.com")()
1146 _, ok = GetEndpoint(squat.ID)
1147 uassert.False(t, ok, "the unruled squat is dropped in the edit")
1148 z, _ = GetZone("onyx")
1149 uassert.Equal(t, "https://rpc2.onyx.example.com", z.RPCURL)
1150}
1151
1152// A listing of the zone's current URL by a curator since removed does not
1153// block an edit that leaves the URL alone.
1154func TestAnExCuratorsListingDoesNotFreezeTheZone(cur realm, t *testing.T) {
1155 reset()
1156 testing.SetRealm(testing.NewUserRealm(Admin))
1157 AddCurator(cross(cur), carol)
1158 testing.SetRealm(testing.NewUserRealm(carol))
1159 AcceptCurator(cross(cur))
1160 testing.SetRealm(testing.NewUserRealm(bob))
1161 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1162 testing.SetRealm(testing.NewUserRealm(Admin))
1163 ApproveZone(cross(cur), "bobs", zrev("bobs"), "")
1164 testing.SetRealm(testing.NewUserRealm(carol))
1165 listed := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://rpc.bobs.example.com", "")
1166 testing.SetRealm(testing.NewUserRealm(Admin))
1167 RemoveCurator(cross(cur), carol)
1168 z, _ := GetZone("bobs")
1169 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, "Bob's, retitled", z.Description, string(z.Kind),
1170 z.GnowebURL, z.RPCURL, z.GenesisURL, "a better title")
1171 _, ok := GetEndpoint(listed)
1172 uassert.True(t, ok, "an edit that leaves the URL alone touches no listing of it")
1173}
1174
1175// The Clear link reaches every page and kind, so it is offered only on the
1176// unfiltered view of a zone that takes endpoints, bound to the revision the
1177// page was rendered at; the call clears an endpoint registered exactly at the
1178// revision it names.
1179func TestTheClearLinksScope(cur realm, t *testing.T) {
1180 reset()
1181 testing.SetRealm(testing.NewUserRealm(bob))
1182 id := RegisterEndpoint(cross(cur), "onyx", "faucet", "https://spam.example.com", "")
1183 at := strconv.FormatInt(reg.Revision(), 10)
1184 uassert.True(t, strings.Contains(Render("zone/onyx"), "throughRevision="+at))
1185 uassert.False(t, strings.Contains(Render("zone/onyx?kind=rpc"), "func=ClearUnreviewed"))
1186 testing.SetRealm(testing.NewUserRealm(Admin))
1187 uassert.Equal(t, 1, ClearUnreviewed(cross(cur), "onyx", erev(id)))
1188 testing.SetRealm(testing.NewUserRealm(bob))
1189 RegisterEndpoint(cross(cur), "onyx", "faucet", "https://spam2.example.com", "")
1190 testing.SetRealm(testing.NewUserRealm(Admin))
1191 RetireZone(cross(cur), "onyx", zrev("onyx"), "shut down")
1192 uassert.Equal(t, 1, reg.Clearable("onyx"))
1193 uassert.True(t, strings.Contains(Render("zone/onyx"), "func=ClearUnreviewed"), "a retired zone's flood is a curator's to clear")
1194}
1195
1196// A registrant cannot withdraw an endpoint a curator unverified, even with no
1197// reason: the reviewer is the ruling.
1198func TestAReasonlessUnverifyIsARuling(cur realm, t *testing.T) {
1199 reset()
1200 testing.SetRealm(testing.NewUserRealm(bob))
1201 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "")
1202 testing.SetRealm(testing.NewUserRealm(Admin))
1203 FlagEndpoint(cross(cur), id, erev(id), "checking")
1204 UnverifyEndpoint(cross(cur), id, erev(id), "")
1205 e, _ := GetEndpoint(id)
1206 uassert.Equal(t, "", e.Reason)
1207 testing.SetRealm(testing.NewUserRealm(bob))
1208 testing.SkipHeights(ReviewWindow)
1209 uassert.AbortsContains(t, cur, "a curator ruled on", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
1210}
1211
1212// With no listing of its own kind at all, a flag under another kind still
1213// does not mark the zone's URL: there is no fallback.
1214func TestNoCrossKindFallback(cur realm, t *testing.T) {
1215 reset()
1216 z, _ := GetZone("mainnet")
1217 reg.RemoveEndpoint(mustRPC(t, "mainnet", z.RPCURL))
1218 testing.SetRealm(testing.NewUserRealm(bob))
1219 id := RegisterEndpoint(cross(cur), "mainnet", "explorer", z.RPCURL, "")
1220 testing.SetRealm(testing.NewUserRealm(Admin))
1221 FlagEndpoint(cross(cur), id, erev(id), "not an explorer")
1222 uassert.False(t, strings.Contains(Render("zone/mainnet"), "`"+z.RPCURL+"` ⚠️"))
1223}
1224
1225// Inputs are trimmed where a caller types them, the edit-time reservation
1226// exempts the proposer and trims too, and the documented realm bounds hold.
1227func TestRealmTrimsAndBounds(cur realm, t *testing.T) {
1228 reset()
1229 uassert.Equal(t, 16, MaxCurators)
1230 _, ok := GetZone(" onyx ")
1231 uassert.True(t, ok)
1232 testing.SetRealm(testing.NewUserRealm(bob))
1233 ProposeZone(cross(cur), " bobs ", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1234 _, ok = GetZone("bobs")
1235 uassert.True(t, ok)
1236 id := RegisterEndpoint(cross(cur), " bobs ", "rpc", "https://rpc2.bobs.example.com", "")
1237 testing.SetRealm(testing.NewUserRealm(Admin))
1238 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "")
1239 uassert.Equal(t, reg.Revision(), erev(id), "the last revision handed out is this verdict's")
1240 // The proposer's own listing of the new URL is no stranger's.
1241 testing.SetRealm(testing.NewUserRealm(bob))
1242 testing.SkipHeights(ReviewWindow)
1243 z, _ := GetZone("bobs")
1244 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind),
1245 z.GnowebURL, " https://rpc2.bobs.example.com ", z.GenesisURL, "")
1246 _, ok = GetEndpoint(id)
1247 uassert.True(t, ok, "the proposer's listing stays")
1248}
1249
1250// The list routes and their links: the approved alias, the retired list and
1251// its link from the index, the pending list, a retired or rejected zone's
1252// URLs shown as code, the review-queue note at exactly the gate, and the
1253// pending zone's Register label.
1254func TestRoutesNotesAndLabels(cur realm, t *testing.T) {
1255 reset()
1256 uassert.Equal(t, Render(""), Render("?status=approved"))
1257 testing.SetRealm(testing.NewUserRealm(Admin))
1258 z, _ := GetZone("onyx")
1259 RetireZone(cross(cur), "onyx", zrev("onyx"), "shut down")
1260 uassert.True(t, strings.Contains(Render(""), "1 retired zone"))
1261 page := Render("?status=retired")
1262 uassert.True(t, strings.Contains(page, "Onyx"))
1263 uassert.False(t, strings.Contains(Render("zone/onyx"), "]("+z.GnowebURL+")"), "a retired zone's URLs are code")
1264 uassert.True(t, strings.Contains(Render("zone/onyx"), "`"+z.GnowebURL+"`"))
1265
1266 testing.SetRealm(testing.NewUserRealm(bob))
1267 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "https://web.bobs.example.com", "https://rpc.bobs.example.com", "")
1268 uassert.True(t, strings.Contains(Render("proposals"), "bobs"))
1269 uassert.True(t, strings.Contains(Render("zone/bobs"), "proposer or curator"))
1270 testing.SetRealm(testing.NewUserRealm(Admin))
1271 RejectZone(cross(cur), "bobs", zrev("bobs"), "no")
1272 uassert.False(t, strings.Contains(Render("zone/bobs"), "](https://web.bobs.example.com)"), "a rejected zone's URLs are code")
1273
1274 for i := 0; reg.Awaiting("mainnet") < zones.MaxUnverifiedPerZone; i++ {
1275 _, err := reg.Register(testutils.TestAddress("q"+strconv.Itoa(i/zones.MaxEndpointsPerAddress)), 1, "mainnet", zones.RPC,
1276 "https://q"+strconv.Itoa(i)+".example.com", "")
1277 must(err)
1278 }
1279 uassert.Equal(t, zones.MaxUnverifiedPerZone, reg.Awaiting("mainnet"))
1280 uassert.True(t, strings.Contains(Render("zone/mainnet"), "the review queue is full at 64 endpoints awaiting review"))
1281}
1282
1283// The Clear link shows only while something is clearable, and the admission
1284// gate bounds how much that can be.
1285func TestClearIsOfferedOnlyWhenItClears(cur realm, t *testing.T) {
1286 reset()
1287 testing.SetRealm(testing.NewUserRealm(Admin))
1288 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://curators.example.com", "")
1289 uassert.False(t, strings.Contains(Render("zone/onyx"), "func=ClearUnreviewed"), "a curator's own listing is not clearable")
1290 for i := 0; reg.Clearable("mainnet") <= zones.MaxUnverifiedPerZone; i++ {
1291 _, err := reg.Register(testutils.TestAddress("c"+strconv.Itoa(i/zones.MaxEndpointsPerAddress)), 1, "mainnet", zones.RPC,
1292 "https://c"+strconv.Itoa(i)+".example.com", "")
1293 if err != nil {
1294 // The admission gate holds the queue at 64: this is as full as a flood gets.
1295 break
1296 }
1297 }
1298 n := reg.Clearable("mainnet")
1299 uassert.True(t, strings.Contains(Render("zone/mainnet"), "Clear "+strconv.Itoa(n)+" never"))
1300 uassert.True(t, n <= zones.MaxUnverifiedPerZone)
1301 uassert.Equal(t, n, ClearUnreviewed(cross(cur), "mainnet", reg.Revision()))
1302 uassert.Equal(t, 0, reg.Clearable("mainnet"))
1303 uassert.False(t, strings.Contains(Render("zone/mainnet"), "func=ClearUnreviewed"))
1304}
1305
1306// Where a decision evicts a record, the page says so beside it.
1307func TestTheEvictionNote(cur realm, t *testing.T) {
1308 reset()
1309 for i := 0; reg.ZoneCount(zones.Rejected) < zones.MaxRejected; i++ {
1310 slug := "rj" + strconv.Itoa(i)
1311 must(reg.ProposeExempt(Admin, 1, slug, zones.Info{ChainID: slug, Title: "R", Kind: zones.Testnet,
1312 RPCURL: "https://rpc" + strconv.Itoa(i) + ".example.com"}))
1313 must(reg.ReviewZone(slug, zones.Rejected, zrev(slug), Admin, 1, "no"))
1314 }
1315 must(reg.ProposeExempt(Admin, 2, "next", zones.Info{ChainID: "next", Title: "N", Kind: zones.Testnet, RPCURL: "https://rpcn.example.com"}))
1316 uassert.True(t, strings.Contains(Render("zone/next"), "rejecting it drops the zone rejected longest ago"))
1317 uassert.False(t, strings.Contains(Render("zone/onyx"), "retiring it drops"))
1318}
1319
1320// The last places of each hard cap are curators': strangers who keep a cap
1321// full cannot lock out the people who would act under it, and the page says
1322// so with a link for them.
1323func TestTheHardCapsKeepPlacesForCurators(cur realm, t *testing.T) {
1324 reset()
1325 testing.SetRealm(testing.NewUserRealm(Admin))
1326 for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone-zones.ReservedForReviewers; i++ {
1327 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://v"+strconv.Itoa(i)+".example.com", "")
1328 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "")
1329 }
1330 testing.SetRealm(testing.NewUserRealm(bob))
1331 uassert.AbortsContains(t, cur, "kept for curators", func() {
1332 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "")
1333 })
1334 uassert.True(t, strings.Contains(Render("zone/onyx"), "endpoint places are kept for curators"))
1335 testing.SetRealm(testing.NewUserRealm(Admin))
1336 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://curator.example.com", "")
1337
1338 for i := 0; reg.Live() < zones.MaxZones-zones.ReservedForReviewers; i++ {
1339 s := "l" + strconv.Itoa(i)
1340 must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "L", Kind: zones.Testnet, RPCURL: "https://rpcl" + strconv.Itoa(i) + ".example.com"}))
1341 must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, ""))
1342 }
1343 testing.SetRealm(testing.NewUserRealm(bob))
1344 uassert.AbortsContains(t, cur, "kept for curators", func() {
1345 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1346 })
1347 uassert.True(t, strings.Contains(Render(""), "places are kept for curators"))
1348 testing.SetRealm(testing.NewUserRealm(Admin))
1349 ProposeZone(cross(cur), "admins", "admins-1", "A", "", "devnet", "", "https://rpc.admins.example.com", "")
1350}
1351
1352// Slugs are trimmed, each zone state offers its links, and reasons and titles
1353// are escaped in every cell.
1354func TestRealmTrimsLinksAndEscapes(cur realm, t *testing.T) {
1355 reset()
1356 testing.SetRealm(testing.NewUserRealm(Admin))
1357 // Every slug a caller types is trimmed.
1358 RetireZone(cross(cur), " onyx ", zrev("onyx"), "shut down")
1359 ApproveZone(cross(cur), " onyx ", zrev("onyx"), "")
1360 testing.SetRealm(testing.NewUserRealm(bob))
1361 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's | best @moul "+Admin.String(), "run by @moul "+Admin.String(), "devnet", "", "https://rpc.bobs.example.com", "")
1362 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://spam.example.com", "")
1363 testing.SetRealm(testing.NewUserRealm(Admin))
1364 uassert.Equal(t, 1, ClearUnreviewed(cross(cur), " onyx ", reg.Revision()))
1365 _, ok := GetEndpoint(id)
1366 uassert.False(t, ok)
1367
1368 // A pending zone offers Approve and Remove; its title and description
1369 // have no live mention.
1370 page := Render("zone/bobs")
1371 uassert.True(t, strings.Contains(page, "func=ApproveZone"))
1372 uassert.True(t, strings.Contains(page, "func=RemoveZone"))
1373 uassert.True(t, strings.Contains(page, "pending review"))
1374 uassert.False(t, strings.Contains(page, " @moul"))
1375 uassert.False(t, strings.Contains(page, " "+Admin.String()))
1376
1377 // A rejected zone offers Remove, its reason cell escapes a pipe and has
1378 // no live mention.
1379 RejectZone(cross(cur), " bobs ", zrev("bobs"), "a | b, see @moul")
1380 page = Render("zone/bobs")
1381 uassert.True(t, strings.Contains(page, "func=RemoveZone"))
1382 uassert.True(t, strings.Contains(page, "rejected"))
1383 uassert.False(t, strings.Contains(page, " @moul"))
1384 uassert.True(t, strings.Contains(Render("proposals?status=rejected"), `a \| b`))
1385 uassert.True(t, strings.Contains(Render("proposals?status=nope"), "No such list."))
1386
1387 // An endpoint reason with a pipe stays in its cell.
1388 e := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://r.example.com", "")
1389 FlagEndpoint(cross(cur), e, erev(e), "x | y")
1390 uassert.True(t, strings.Contains(Render("zone/onyx?kind=rpc"), `x \| y`))
1391
1392 // A retired zone offers Approve, and its reason cell escapes a pipe.
1393 RetireZone(cross(cur), "mainnet", zrev("mainnet"), "c | d")
1394 uassert.True(t, strings.Contains(Render("zone/mainnet"), "func=ApproveZone"))
1395 uassert.True(t, strings.Contains(Render("zone/mainnet"), "retired"))
1396 uassert.True(t, strings.Contains(Render("?status=retired"), `c \| d`))
1397}
1398
1399// The index's example asks this chain's zone for peers when it lists one, not
1400// the onyx fallback.
1401func TestTheIndexExampleUsesThisChainsZone(t *testing.T) {
1402 reset()
1403 m, _ := GetZone("mainnet")
1404 in := zones.Info{ChainID: runtime.ChainID(), Title: m.Title, Kind: m.Kind, RPCURL: m.RPCURL, GnowebURL: m.GnowebURL}
1405 must(reg.Edit("mainnet", zrev("mainnet"), in, Admin, 1, "on this chain"))
1406 uassert.True(t, reg.EndpointCount("mainnet", zones.Peer) > 0)
1407 uassert.True(t, strings.Contains(Render(""), `ListAddresses("mainnet", "peer", "verified")`))
1408}
1409
1410// Retiring a zone with the retired list full says it evicts the oldest.
1411func TestTheRetireEvictionNote(t *testing.T) {
1412 reset()
1413 for i := 0; reg.ZoneCount(zones.Retired) < zones.MaxRetired; i++ {
1414 s := "rt" + strconv.Itoa(i)
1415 must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "R", Kind: zones.Testnet, RPCURL: "https://rpcr" + strconv.Itoa(i) + ".example.com"}))
1416 must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, ""))
1417 must(reg.ReviewZone(s, zones.Retired, zrev(s), Admin, 1, "gone"))
1418 }
1419 uassert.True(t, strings.Contains(Render("zone/onyx"), "retiring it drops the zone retired longest ago"))
1420}
1421
1422// An edit is validated before it acts: one that cannot pass removes nothing
1423// on its way to failing. A former curator's unruled listing of a new URL is
1424// dropped like a stranger's: only a ruling refuses.
1425func TestAnEditValidatesBeforeItRemoves(cur realm, t *testing.T) {
1426 reset()
1427 testing.SetRealm(testing.NewUserRealm(bob))
1428 ws := RegisterEndpoint(cross(cur), "onyx", "rpc", "wss://rpc9.onyx.example.com/websocket", "")
1429 testing.SetRealm(testing.NewUserRealm(Admin))
1430 FlagEndpoint(cross(cur), ws, erev(ws), "not ours")
1431 z, _ := GetZone("onyx")
1432 edit := func(rpc string) func() {
1433 return func() {
1434 EditZone(cross(cur), "onyx", zrev("onyx"), z.ChainID, z.Title+".", z.Description, string(z.Kind),
1435 z.GnowebURL, rpc, z.GenesisURL, "moved")
1436 }
1437 }
1438 uassert.AbortsContains(t, cur, "rpc url", edit("wss://rpc9.onyx.example.com/websocket"))
1439 _, ok := GetEndpoint(ws)
1440 uassert.True(t, ok, "a failing edit removed nothing")
1441
1442 AddCurator(cross(cur), carol)
1443 testing.SetRealm(testing.NewUserRealm(carol))
1444 AcceptCurator(cross(cur))
1445 old := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://next.onyx.example.com", "")
1446 testing.SetRealm(testing.NewUserRealm(Admin))
1447 RemoveCurator(cross(cur), carol)
1448 edit("https://next.onyx.example.com")()
1449 _, ok = GetEndpoint(old)
1450 uassert.False(t, ok, "an unruled former curator's listing is dropped")
1451}
1452
1453// No tail a registration trims gets a stranger past the own-URL reservation.
1454func TestTheReservationHoldsWhateverTheTail(cur realm, t *testing.T) {
1455 reset()
1456 z, _ := GetZone("onyx")
1457 reg.RemoveEndpoint(mustRPC(t, "onyx", z.RPCURL))
1458 testing.SetRealm(testing.NewUserRealm(bob))
1459 for _, tail := range []string{"/", "?", "/?"} {
1460 uassert.AbortsContains(t, cur, "onyx's own rpc", func() {
1461 RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL+tail, "moved: use mine")
1462 })
1463 }
1464 // A tail that is a real query is not the zone's URL, and is stored keyed
1465 // as it was typed ("/??" is the same endpoint as "??").
1466 for _, tail := range []string{"??", "?/"} {
1467 id := RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL+tail, "")
1468 e, _ := GetEndpoint(id)
1469 uassert.True(t, zones.Canonical(zones.RPC, e.Address) != zones.Canonical(zones.RPC, z.RPCURL), tail)
1470 }
1471 uassert.AbortsContains(t, cur, "already lists", func() {
1472 RegisterEndpoint(cross(cur), "onyx", "rpc", z.RPCURL+"/??", "")
1473 })
1474}
1475
1476// Curator removal, invitation order, rulings on a stranger's listing, the
1477// genesis URL, the status badges and the decision links.
1478func TestMoreRealmRules(cur realm, t *testing.T) {
1479 reset()
1480 // Only a curator removes a curator.
1481 testing.SetRealm(testing.NewUserRealm(bob))
1482 uassert.AbortsContains(t, cur, "is not a curator", func() { RemoveCurator(cross(cur), Admin) })
1483 // Curators and invitations list in address order.
1484 testing.SetRealm(testing.NewUserRealm(Admin))
1485 AddCurator(cross(cur), carol)
1486 AddCurator(cross(cur), alice)
1487 inv := Invited()
1488 for i := 1; i < len(inv); i++ {
1489 uassert.True(t, inv[i-1].String() < inv[i].String())
1490 }
1491 // A verdict with no reason on a stranger's listing of the new URL is a
1492 // ruling: the edit refuses.
1493 testing.SetRealm(testing.NewUserRealm(bob))
1494 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://next.onyx.example.com", "")
1495 testing.SetRealm(testing.NewUserRealm(Admin))
1496 VerifyEndpoint(cross(cur), id, zr(id), erev(id), "")
1497 z, _ := GetZone("onyx")
1498 uassert.AbortsContains(t, cur, "with a curator's ruling", func() {
1499 EditZone(cross(cur), "onyx", zrev("onyx"), z.ChainID, z.Title+".", z.Description, string(z.Kind),
1500 z.GnowebURL, "https://next.onyx.example.com", z.GenesisURL, "moved")
1501 })
1502 reset() // a test does not roll back the aborted edit's writes; a transaction does
1503 z, _ = GetZone("onyx")
1504 // The genesis URL is never marked, whatever kind lists it.
1505 g := RegisterEndpoint(cross(cur), "onyx", "gnoweb", z.GenesisURL, "")
1506 FlagEndpoint(cross(cur), g, erev(g), "not a gnoweb")
1507 uassert.False(t, strings.Contains(Render("zone/onyx"), "`"+z.GenesisURL+"` ⚠️"))
1508 // Status badges, exactly.
1509 uassert.True(t, strings.Contains(Render("zone/onyx"), "✅ **official**"))
1510 testing.SetRealm(testing.NewUserRealm(bob))
1511 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1512 uassert.True(t, strings.Contains(Render("zone/bobs"), "⏳ **pending review**"))
1513 // The pending zone's decision links carry its revision.
1514 rv := strconv.FormatInt(zrev("bobs"), 10)
1515 page := Render("zone/bobs")
1516 for _, fn := range []string{"ApproveZone", "RejectZone", "RemoveZone"} {
1517 uassert.True(t, regexpLinkCarries(page, fn, rv), fn)
1518 }
1519 testing.SetRealm(testing.NewUserRealm(Admin))
1520 RejectZone(cross(cur), "bobs", zrev("bobs"), "no")
1521 uassert.True(t, strings.Contains(Render("zone/bobs"), "❌ **rejected**"))
1522 RetireZone(cross(cur), "onyx", zrev("onyx"), "gone")
1523 uassert.True(t, strings.Contains(Render("zone/onyx"), "⏹️ **retired**"))
1524}
1525
1526// Paging clamps a reader's page number and shows every row.
1527func TestPagingEdges(t *testing.T) {
1528 reset()
1529 for i := 0; reg.EndpointCount("mainnet", "") < PageSize+1; i++ {
1530 _, err := reg.RegisterExempt(Admin, 1, "mainnet", zones.Indexer, "https://p"+strconv.Itoa(i)+".example.com", "")
1531 must(err)
1532 }
1533 all := reg.Endpoints(zones.EndpointFilter{Zone: "mainnet"})
1534 last := all[len(all)-1].Address
1535 uassert.True(t, strings.Contains(Render("zone/mainnet?page=2"), last), "the 26th row is on page 2")
1536 uassert.Equal(t, Render("zone/mainnet?page=1"), Render("zone/mainnet?page=0"))
1537 uassert.Equal(t, Render("zone/mainnet?page=2"), Render("zone/mainnet?page=3"))
1538}
1539
1540// A proposer's edit drops nothing that is not theirs: a former curator's
1541// listing on their pending zone refuses the edit instead of being dropped.
1542func TestAProposersEditDropsNothingNotTheirs(cur realm, t *testing.T) {
1543 reset()
1544 testing.SetRealm(testing.NewUserRealm(Admin))
1545 AddCurator(cross(cur), carol)
1546 testing.SetRealm(testing.NewUserRealm(carol))
1547 AcceptCurator(cross(cur))
1548 testing.SetRealm(testing.NewUserRealm(bob))
1549 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1550 testing.SetRealm(testing.NewUserRealm(carol))
1551 id := RegisterEndpoint(cross(cur), "bobs", "rpc", "https://b.example.com", "")
1552 testing.SetRealm(testing.NewUserRealm(Admin))
1553 RemoveCurator(cross(cur), carol)
1554 testing.SetRealm(testing.NewUserRealm(bob))
1555 testing.SkipHeights(ReviewWindow)
1556 z, _ := GetZone("bobs")
1557 uassert.AbortsContains(t, cur, "a curator removes it before", func() {
1558 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), "", "https://b.example.com", "", "")
1559 })
1560 _, ok := GetEndpoint(id)
1561 uassert.True(t, ok)
1562}
1563
1564// regexpLinkCarries reports whether a $help link to fn carries revision rv.
1565func regexpLinkCarries(page, fn, rv string) bool {
1566 for _, part := range strings.Split(page, "$help&") {
1567 end := strings.IndexAny(part, ")")
1568 if end < 0 {
1569 continue
1570 }
1571 link := part[:end]
1572 if strings.Contains(link, "func="+fn) && strings.Contains(link, "revision="+rv) {
1573 return true
1574 }
1575 }
1576 return false
1577}
1578
1579// A flagged listing of a zone's new URL refuses the edit whoever holds it and
1580// whoever edits: it would mark the zone's own URL. Each case starts fresh: a
1581// test does not roll back an aborted call's writes.
1582func TestAFlaggedOwnListingRefusesTheEdit(cur realm, t *testing.T) {
1583 for _, tc := range []struct {
1584 name string
1585 approve bool
1586 holder, editor address
1587 kind, url, want string
1588 }{
1589 {"the proposer's, on a pending zone, by the proposer", false, bob, bob, "rpc", "https://rpc2.bobs.example.com", "flagged; a curator removes it"},
1590 {"a curator's, under rpc, by a curator", true, Admin, Admin, "rpc", "https://rpc2.bobs.example.com", "flagged; a curator removes it"},
1591 {"a curator's, under gnoweb, by a curator", true, Admin, Admin, "gnoweb", "https://web2.bobs.example.com", "flagged; a curator removes it"},
1592 {"the proposer's, on an approved zone, by a curator", true, bob, Admin, "rpc", "https://rpc2.bobs.example.com", "a curator removes it before it becomes"},
1593 } {
1594 reset()
1595 testing.SetRealm(testing.NewUserRealm(bob))
1596 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "https://web.bobs.example.com", "https://rpc.bobs.example.com", "")
1597 if tc.approve {
1598 testing.SetRealm(testing.NewUserRealm(Admin))
1599 ApproveZone(cross(cur), "bobs", zrev("bobs"), "")
1600 }
1601 k, _ := zones.ParseEndpointKind(tc.kind)
1602 id, err := reg.Register(tc.holder, 1, "bobs", k, tc.url, "spam")
1603 must(err)
1604 must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "spam label"))
1605 testing.SkipHeights(ReviewWindow)
1606 testing.SetRealm(testing.NewUserRealm(tc.editor))
1607 z, _ := GetZone("bobs")
1608 web, rpc, reason := z.GnowebURL, z.RPCURL, ""
1609 if tc.kind == "gnoweb" {
1610 web = tc.url
1611 } else {
1612 rpc = tc.url
1613 }
1614 if tc.approve {
1615 reason = "moved"
1616 }
1617 uassert.AbortsContains(t, cur, tc.want, func() {
1618 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), web, rpc, z.GenesisURL, reason)
1619 }, tc.name)
1620 }
1621}
1622
1623// Paging keeps a list's status, and the next link shows on the page before
1624// the last; a registrant cannot withdraw from a retired zone.
1625func TestListPagingAndRetiredRecords(cur realm, t *testing.T) {
1626 reset()
1627 for i := 0; reg.ZoneCount(zones.Retired) < 2*PageSize+1; i++ {
1628 s := "pr" + strconv.Itoa(i)
1629 must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "P", Kind: zones.Testnet, RPCURL: "https://rpcp" + strconv.Itoa(i) + ".example.com"}))
1630 must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, ""))
1631 must(reg.ReviewZone(s, zones.Retired, zrev(s), Admin, 1, "gone"))
1632 }
1633 page := Render("?status=retired&page=2")
1634 uassert.True(t, strings.Contains(page, "status=retired&page=3"), "page 2 of 3 links to page 3, in the same list")
1635 uassert.True(t, strings.Contains(page, "status=retired&page=1"))
1636
1637 testing.SetRealm(testing.NewUserRealm(bob))
1638 id := RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "")
1639 testing.SetRealm(testing.NewUserRealm(Admin))
1640 RetireZone(cross(cur), "onyx", zrev("onyx"), "gone")
1641 testing.SetRealm(testing.NewUserRealm(bob))
1642 testing.SkipHeights(ReviewWindow)
1643 uassert.AbortsContains(t, cur, "a record", func() { RemoveEndpoint(cross(cur), id, erev(id)) })
1644 uassert.False(t, strings.Contains(Render("proposals"), "0 rejected"), "no link to an empty rejected list")
1645 uassert.Equal(t, Render("proposals"), Render("proposals?status=pending"))
1646}
1647
1648// The reserve notes come before the queue notes when both caps are full, and
1649// every link a non-curator's call would fail on says who it is for.
1650func TestReserveNotesAreLabelledAndComeFirst(cur realm, t *testing.T) {
1651 reset()
1652 for i := 0; reg.Live() < zones.MaxZones-zones.ReservedForReviewers-zones.MaxPending; i++ {
1653 s := "ap" + strconv.Itoa(i)
1654 must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "A", Kind: zones.Testnet, RPCURL: "https://rpca" + strconv.Itoa(i) + ".example.com"}))
1655 must(reg.ReviewZone(s, zones.Approved, zrev(s), Admin, 1, ""))
1656 }
1657 for i := 0; reg.ZoneCount(zones.Pending) < zones.MaxPending; i++ {
1658 s := "pp" + strconv.Itoa(i)
1659 must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "P", Kind: zones.Testnet, RPCURL: "https://rpcq" + strconv.Itoa(i) + ".example.com"}))
1660 }
1661 uassert.Equal(t, zones.MaxZones-zones.ReservedForReviewers, reg.Live())
1662 page := Render("")
1663 uassert.True(t, strings.Contains(page, "the registry's last 16 places are kept for curators"))
1664 uassert.True(t, strings.Contains(page, `Propose a zone \(curators only\)]`))
1665 uassert.False(t, strings.Contains(page, "the review queue is full"), "the reserve binds first")
1666
1667 reset()
1668 for i := 0; reg.Awaiting("onyx") < zones.MaxUnverifiedPerZone; i++ {
1669 _, err := reg.RegisterExempt(Admin, 1, "onyx", zones.RPC, "https://u"+strconv.Itoa(i)+".example.com", "")
1670 must(err)
1671 }
1672 for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone-zones.ReservedForReviewers; i++ {
1673 id, err := reg.RegisterExempt(Admin, 1, "onyx", zones.RPC, "https://f"+strconv.Itoa(i)+".example.com", "")
1674 must(err)
1675 must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "spam"))
1676 }
1677 page = Render("zone/onyx")
1678 uassert.True(t, strings.Contains(page, "the last 16 endpoint places are kept for curators"))
1679 uassert.True(t, strings.Contains(page, `Register an endpoint \(curators only\)]`))
1680 uassert.False(t, strings.Contains(page, "the review queue is full"), "the reserve binds first")
1681
1682 reset()
1683 testing.SetRealm(testing.NewUserRealm(bob))
1684 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "")
1685 uassert.True(t, strings.Contains(Render("zone/onyx"), `all pages and kinds \(curators only\)]`))
1686}
1687
1688// A rejected or retired zone takes no endpoints: no Register link and no cap
1689// note, but a curator still clears its never-reviewed endpoints.
1690func TestARecordZoneOffersNoRegistration(cur realm, t *testing.T) {
1691 reset()
1692 testing.SetRealm(testing.NewUserRealm(bob))
1693 RegisterEndpoint(cross(cur), "onyx", "rpc", "https://bob.example.com", "")
1694 for i := 0; reg.EndpointCount("onyx", "") < zones.MaxEndpointsPerZone; i++ {
1695 id, err := reg.RegisterExempt(Admin, 1, "onyx", zones.RPC, "https://f"+strconv.Itoa(i)+".example.com", "")
1696 must(err)
1697 must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "spam"))
1698 }
1699 must(reg.ReviewZone("onyx", zones.Retired, zrev("onyx"), Admin, 1, "gone"))
1700 page := Render("zone/onyx")
1701 uassert.False(t, strings.Contains(page, "func=RegisterEndpoint"))
1702 uassert.False(t, strings.Contains(page, "endpoints are full"))
1703 uassert.True(t, strings.Contains(page, "func=ClearUnreviewed"), "the record's flood is still a curator's to clear")
1704}
1705
1706// On a pending zone the proposer is the one gated registrant, so once they
1707// reach the per-address cap the link is for curators.
1708func TestThePendingRegisterLinkFollowsTheProposersCap(cur realm, t *testing.T) {
1709 reset()
1710 testing.SetRealm(testing.NewUserRealm(bob))
1711 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1712 uassert.True(t, strings.Contains(Render("zone/bobs"), `Register an endpoint \(proposer or curator\)]`))
1713 for i := 0; i < zones.MaxEndpointsPerAddress; i++ {
1714 RegisterEndpoint(cross(cur), "bobs", "rpc", "https://b"+strconv.Itoa(i)+".bobs.example.com", "")
1715 }
1716 page := Render("zone/bobs")
1717 uassert.True(t, strings.Contains(page, "the proposer has registered 16 endpoints here"))
1718 uassert.True(t, strings.Contains(page, `Register an endpoint \(curators only\)]`))
1719}
1720
1721// The rejected list's pager stays in the rejected list.
1722func TestTheRejectedPagerKeepsItsList(t *testing.T) {
1723 reset()
1724 for i := 0; reg.ZoneCount(zones.Rejected) < PageSize+1; i++ {
1725 s := "rj" + strconv.Itoa(i)
1726 must(reg.ProposeExempt(Admin, 1, s, zones.Info{ChainID: s, Title: "R", Kind: zones.Testnet, RPCURL: "https://rpcr" + strconv.Itoa(i) + ".example.com"}))
1727 must(reg.ReviewZone(s, zones.Rejected, zrev(s), Admin, 1, "no"))
1728 }
1729 uassert.True(t, strings.Contains(Render("proposals?status=rejected"), "status=rejected&page=2"))
1730}
1731
1732// A reason on an endpoint is free text: a mention in it stays text.
1733func TestAnEndpointReasonCannotMention(t *testing.T) {
1734 reset()
1735 id, err := reg.Register(bob, 1, "onyx", zones.RPC, "https://bob.example.com", "")
1736 must(err)
1737 must(reg.ReviewEndpoint(id, zones.Flagged, 0, erev(id), Admin, 1, "ask @alice"))
1738 page := Render("zone/onyx?kind=rpc")
1739 uassert.True(t, strings.Contains(page, `ask \@alice`))
1740 uassert.False(t, strings.Contains(page, "ask @alice"))
1741}
1742
1743// An edit onto a listing a curator ruled on is refused whatever the ruling
1744// left: a reasonless unverify, or a reset's reason with nobody recorded.
1745func TestAnEditRefusesEveryRuling(cur realm, t *testing.T) {
1746 // A curator's reasonless unverify.
1747 reset()
1748 id, err := reg.Register(bob, 1, "onyx", zones.RPC, "https://rpc2.onyx.example.com", "")
1749 must(err)
1750 must(reg.ReviewEndpoint(id, zones.Verified, zrev("onyx"), erev(id), Admin, 1, ""))
1751 must(reg.ReviewEndpoint(id, zones.Unverified, 0, erev(id), Admin, 1, ""))
1752 testing.SetRealm(testing.NewUserRealm(Admin))
1753 z, _ := GetZone("onyx")
1754 uassert.AbortsContains(t, cur, "with a curator's ruling", func() {
1755 EditZone(cross(cur), "onyx", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), z.GnowebURL, "https://rpc2.onyx.example.com", z.GenesisURL, "moved")
1756 })
1757
1758 // A proposer's chain-id reset of a former curator's listing: no reviewer,
1759 // a reason.
1760 reset()
1761 testing.SetRealm(testing.NewUserRealm(bob))
1762 ProposeZone(cross(cur), "bobs", "bobs-1", "Bob's", "", "devnet", "", "https://rpc.bobs.example.com", "")
1763 id, err = reg.RegisterExempt(carol, 1, "bobs", zones.RPC, "https://rpc2.bobs.example.com", "")
1764 must(err)
1765 must(reg.ReviewEndpoint(id, zones.Verified, zrev("bobs"), erev(id), Admin, 1, ""))
1766 testing.SkipHeights(ReviewWindow)
1767 z, _ = GetZone("bobs")
1768 EditZone(cross(cur), "bobs", z.Revision, "bobs-2", z.Title, z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "")
1769 e, _ := GetEndpoint(id)
1770 uassert.Equal(t, "", e.ReviewedBy.String())
1771 testing.SetRealm(testing.NewUserRealm(Admin))
1772 z, _ = GetZone("bobs")
1773 uassert.AbortsContains(t, cur, "with a curator's ruling", func() {
1774 EditZone(cross(cur), "bobs", z.Revision, z.ChainID, z.Title, z.Description, string(z.Kind), z.GnowebURL, "https://rpc2.bobs.example.com", z.GenesisURL, "")
1775 })
1776}
1777
1778// Respelling the zone's own URL is no change to it: the edit does not look at
1779// listings of it, ruled or not.
1780func TestRespellingTheOwnURLIsNoMove(cur realm, t *testing.T) {
1781 reset()
1782 z, _ := GetZone("onyx")
1783 reg.RemoveEndpoint(mustRPC(t, "onyx", z.RPCURL))
1784 id, err := reg.Register(bob, 1, "onyx", zones.RPC, z.RPCURL, "")
1785 must(err)
1786 must(reg.ReviewEndpoint(id, zones.Verified, zrev("onyx"), erev(id), Admin, 1, ""))
1787 testing.SetRealm(testing.NewUserRealm(Admin))
1788 z, _ = GetZone("onyx")
1789 EditZone(cross(cur), "onyx", z.Revision, z.ChainID, "Onyx, retitled", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL+":443", z.GenesisURL, "retitled")
1790 _, ok := GetEndpoint(id)
1791 uassert.True(t, ok)
1792}
1793
1794// The slug is trimmed on every write, EditZone and RemoveZone included.
1795func TestEveryWriteTrimsTheSlug(cur realm, t *testing.T) {
1796 reset()
1797 testing.SetRealm(testing.NewUserRealm(Admin))
1798 ProposeZone(cross(cur), "tz", "tz-1", "T", "", "devnet", "", "https://rpc.tz.example.com", "")
1799 z, _ := GetZone("tz")
1800 EditZone(cross(cur), " tz\t", z.Revision, z.ChainID, "T, edited", z.Description, string(z.Kind), z.GnowebURL, z.RPCURL, z.GenesisURL, "")
1801 z, _ = GetZone("tz")
1802 uassert.Equal(t, "T, edited", z.Title)
1803 RemoveZone(cross(cur), "\ttz ", z.Revision)
1804 _, ok := GetZone("tz")
1805 uassert.False(t, ok)
1806}
1807
1808// The seeded endpoints went through the gated registration, so none is
1809// Exempt; and a zone with no endpoints draws no kind bar.
1810func TestSeedsAreGatedAndAnEmptyZoneHasNoKindBar(cur realm, t *testing.T) {
1811 reset()
1812 for _, slug := range []string{"mainnet", "onyx", "staging", "moul-staging"} {
1813 for _, e := range ListEndpoints(slug, "", "") {
1814 uassert.False(t, e.Exempt, slug+" #"+strconv.FormatInt(e.ID, 10))
1815 }
1816 }
1817 testing.SetRealm(testing.NewUserRealm(Admin))
1818 ProposeZone(cross(cur), "tz", "tz-1", "T", "", "devnet", "", "https://rpc.tz.example.com", "")
1819 uassert.False(t, strings.Contains(Render("zone/tz"), "all (0)"))
1820}