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

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 g&#49;manfred"))
 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}