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

service_registry_test.gno

25.59 Kb · 677 lines
  1package service_registry
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7	"testing"
  8
  9	"gno.land/p/nt/testutils/v0"
 10	"gno.land/p/nt/uassert/v0"
 11)
 12
 13var (
 14	owner    = testutils.TestAddress("owner")
 15	attacker = testutils.TestAddress("attacker")
 16	newOwner = testutils.TestAddress("newowner")
 17	third    = testutils.TestAddress("third")
 18)
 19
 20const goodPath = "gno.land/r/demo/oracle"
 21
 22// resetState resets all package-level state between tests.
 23func resetState() {
 24	services = make(map[string]*Service)
 25	names = []string{}
 26	retired = make(map[string]*Reservation)
 27	ownerServices = make(map[address]int)
 28	pendingOwners = make(map[string]address)
 29}
 30
 31func register(cur realm, name string) {
 32	testing.SetRealm(testing.NewUserRealm(owner))
 33	RegisterService(cross(cur), name, goodPath, "a price oracle", "oracle", "")
 34}
 35
 36// fillFor registers n services for one address under a name prefix.
 37//
 38// NOTE: testing.SetRealm is FRAME-scoped — the caller realm set here does
 39// not survive the return, so a caller that needs a specific identity
 40// afterwards must set it again itself.
 41func fillFor(cur realm, who address, prefix string, n int) {
 42	testing.SetRealm(testing.NewUserRealm(who))
 43	for i := 0; i < n; i++ {
 44		RegisterService(cross(cur), prefix+strconv.Itoa(i), goodPath, "d", "oracle", "")
 45	}
 46}
 47
 48// handOff runs a COMPLETE two-step ownership transfer: the sitting owner
 49// nominates, the nominee consents. Ownership does not move until the
 50// second call (pearl audit Y4).
 51func handOff(cur realm, from, to address, name string) {
 52	testing.SetRealm(testing.NewUserRealm(from))
 53	TransferOwnership(cross(cur), name, to)
 54	testing.SetRealm(testing.NewUserRealm(to))
 55	AcceptOwnership(cross(cur), name)
 56}
 57
 58func quotaMsg() string {
 59	return "address already holds the maximum of " +
 60		strconv.Itoa(MaxServicesPerOwner) + " services"
 61}
 62
 63// ---------- RegisterService ----------
 64
 65func TestRegister_Basic(cur realm, t *testing.T) {
 66	resetState()
 67	register(cur, "price_oracle")
 68
 69	svc := services["price_oracle"]
 70	uassert.Equal(t, string(owner), string(svc.Owner))
 71	uassert.Equal(t, goodPath, svc.PkgPath)
 72	uassert.Equal(t, "oracle", svc.ServiceType)
 73}
 74
 75func TestRegister_DuplicateRejected(cur realm, t *testing.T) {
 76	resetState()
 77	register(cur, "price_oracle")
 78
 79	testing.SetRealm(testing.NewUserRealm(attacker))
 80	uassert.AbortsWithMessage(t, cur,
 81		"service already registered: price_oracle",
 82		func() { RegisterService(cross(cur), "price_oracle", goodPath, "mine now", "oracle", "") },
 83	)
 84}
 85
 86func TestRegister_NameValidation(cur realm, t *testing.T) {
 87	resetState()
 88	testing.SetRealm(testing.NewUserRealm(owner))
 89
 90	for _, bad := range []string{"", "Upper", "has space", "semi;colon", strings.Repeat("a", MaxNameLen+1)} {
 91		uassert.AbortsContains(t, cur,
 92			"name must be",
 93			func() { RegisterService(cross(cur), bad, goodPath, "d", "oracle", "") },
 94		)
 95	}
 96}
 97
 98func TestRegister_PkgPathValidation(cur realm, t *testing.T) {
 99	resetState()
100	testing.SetRealm(testing.NewUserRealm(owner))
101
102	uassert.AbortsWithMessage(t, cur, "pkgpath must not be empty",
103		func() { RegisterService(cross(cur), "svc", "", "d", "oracle", "") })
104	uassert.AbortsContains(t, cur, "pkgpath must start with",
105		func() { RegisterService(cross(cur), "svc", "example.com/r/x", "d", "oracle", "") })
106	uassert.AbortsContains(t, cur, "pkgpath must be gno.land/r/",
107		func() { RegisterService(cross(cur), "svc", "gno.land/", "d", "oracle", "") })
108	uassert.AbortsContains(t, cur, "invalid pkgpath element",
109		func() { RegisterService(cross(cur), "svc", "gno.land/r/x|y", "d", "oracle", "") })
110	uassert.AbortsContains(t, cur, "pkgpath exceeds",
111		func() {
112			RegisterService(cross(cur), "svc", "gno.land/r/"+strings.Repeat("a", MaxPkgPathLen), "d", "oracle", "")
113		})
114}
115
116func TestRegister_FieldValidation(cur realm, t *testing.T) {
117	resetState()
118	testing.SetRealm(testing.NewUserRealm(owner))
119
120	uassert.AbortsWithMessage(t, cur, "description must not be empty",
121		func() { RegisterService(cross(cur), "svc", goodPath, "", "oracle", "") })
122	uassert.AbortsContains(t, cur, "description exceeds",
123		func() {
124			RegisterService(cross(cur), "svc", goodPath, strings.Repeat("d", MaxDescriptionLen+1), "oracle", "")
125		})
126	uassert.AbortsWithMessage(t, cur, "service type must not be empty",
127		func() { RegisterService(cross(cur), "svc", goodPath, "d", "", "") })
128	uassert.AbortsContains(t, cur, "service type must be",
129		func() { RegisterService(cross(cur), "svc", goodPath, "d", "Bad|Type", "") })
130	uassert.AbortsContains(t, cur, "metadata exceeds",
131		func() {
132			RegisterService(cross(cur), "svc", goodPath, "d", "oracle", strings.Repeat("m", MaxMetadataLen+1))
133		})
134}
135
136// ---------- Deregister + squat protection ----------
137
138func TestDeregister_NameStaysReservedForOwner(cur realm, t *testing.T) {
139	resetState()
140	register(cur, "price_oracle")
141
142	testing.SetRealm(testing.NewUserRealm(owner))
143	Deregister(cross(cur), "price_oracle")
144	uassert.PanicsWithMessage(t, cur,
145		"service not found: price_oracle",
146		func() { Resolve("price_oracle") },
147	)
148
149	// the squat the audit found: attacker re-registers the freed name
150	// and inherits every integrator still resolving it — must be blocked
151	testing.SetRealm(testing.NewUserRealm(attacker))
152	uassert.AbortsWithMessage(t, cur,
153		"name is reserved for its former owner: price_oracle",
154		func() {
155			RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "trust me", "oracle", "")
156		},
157	)
158
159	// the former owner can re-register it
160	testing.SetRealm(testing.NewUserRealm(owner))
161	RegisterService(cross(cur), "price_oracle", goodPath, "back again", "oracle", "")
162	uassert.Equal(t, goodPath, Resolve("price_oracle"))
163}
164
165func TestDeregister_OwnerOnly(cur realm, t *testing.T) {
166	resetState()
167	register(cur, "price_oracle")
168
169	testing.SetRealm(testing.NewUserRealm(attacker))
170	uassert.AbortsWithMessage(t, cur,
171		"only the owner can deregister",
172		func() { Deregister(cross(cur), "price_oracle") },
173	)
174}
175
176// ---------- UpdateService / TransferOwnership ----------
177
178func TestUpdate_OwnerOnly(cur realm, t *testing.T) {
179	resetState()
180	register(cur, "price_oracle")
181
182	testing.SetRealm(testing.NewUserRealm(attacker))
183	uassert.AbortsWithMessage(t, cur,
184		"only the owner can update this service",
185		func() { UpdateService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "hijacked", "oracle", "") },
186	)
187
188	testing.SetRealm(testing.NewUserRealm(owner))
189	UpdateService(cross(cur), "price_oracle", "gno.land/r/demo/oracle_v2", "v2", "oracle", "k=v")
190	uassert.Equal(t, "gno.land/r/demo/oracle_v2", Resolve("price_oracle"))
191}
192
193func TestTransferOwnership(cur realm, t *testing.T) {
194	resetState()
195	register(cur, "price_oracle")
196
197	testing.SetRealm(testing.NewUserRealm(attacker))
198	uassert.AbortsWithMessage(t, cur,
199		"only the owner can transfer ownership",
200		func() { TransferOwnership(cross(cur), "price_oracle", attacker) },
201	)
202
203	testing.SetRealm(testing.NewUserRealm(owner))
204	uassert.AbortsWithMessage(t, cur,
205		"invalid new owner address: junk",
206		func() { TransferOwnership(cross(cur), "price_oracle", address("junk")) },
207	)
208	uassert.AbortsWithMessage(t, cur,
209		"new owner is already the owner",
210		func() { TransferOwnership(cross(cur), "price_oracle", owner) },
211	)
212
213	handOff(cur, owner, newOwner, "price_oracle")
214	uassert.Equal(t, string(newOwner), string(GetOwner("price_oracle")))
215
216	// the new owner controls the entry now
217	testing.SetRealm(testing.NewUserRealm(newOwner))
218	UpdateService(cross(cur), "price_oracle", goodPath, "under new management", "oracle", "")
219}
220
221// ---------- queries ----------
222
223func TestResolve_UnknownPanics(cur realm, t *testing.T) {
224	resetState()
225
226	uassert.PanicsWithMessage(t, cur,
227		"service not found: ghost",
228		func() { Resolve("ghost") },
229	)
230}
231
232func TestListServices_And_ByType(cur realm, t *testing.T) {
233	resetState()
234	testing.SetRealm(testing.NewUserRealm(owner))
235	RegisterService(cross(cur), "alpha", goodPath, "d", "oracle", "")
236	RegisterService(cross(cur), "beta", goodPath, "d", "dex", "")
237	RegisterService(cross(cur), "gamma", goodPath, "d", "oracle", "")
238
239	uassert.Equal(t, "alpha, beta, gamma", ListServices())
240	uassert.Equal(t, "alpha, gamma", ListByType("oracle"))
241	uassert.Equal(t, "none", ListByType("bridge"))
242}
243
244func TestGetService_Summary(cur realm, t *testing.T) {
245	resetState()
246	register(cur, "price_oracle")
247
248	out := GetService("price_oracle")
249	uassert.True(t, strings.Contains(out, "PkgPath: "+goodPath), "summary must include pkgpath")
250	uassert.True(t, strings.Contains(out, string(owner)), "summary must include owner")
251}
252
253// ---------- render ----------
254
255func TestRender_EmptyState(cur realm, t *testing.T) {
256	resetState()
257	uassert.Equal(t, "# Service Registry\n\nNo services registered.\n", Render(""))
258}
259
260func TestRender_SanitizesInjection(cur realm, t *testing.T) {
261	resetState()
262	testing.SetRealm(testing.NewUserRealm(owner))
263	RegisterService(cross(cur), "sneaky", goodPath,
264		"desc` | fake | row\ninjected", "oracle", "")
265
266	out := Render("")
267	uassert.False(t, strings.Contains(out, "desc`"), "backticks must be sanitized")
268	uassert.False(t, strings.Contains(out, "| fake |"), "pipes must be sanitized")
269	uassert.False(t, strings.Contains(out, "\ninjected"), "newlines must be sanitized")
270}
271
272func TestRender_ListsEntries(cur realm, t *testing.T) {
273	resetState()
274	register(cur, "price_oracle")
275
276	out := Render("")
277	uassert.True(t, strings.Contains(out, "price_oracle"), "render must list the service")
278	uassert.True(t, strings.Contains(out, goodPath), "render must show the pkgpath")
279	uassert.False(t, strings.Contains(out, "Showing"),
280		"a short registry must not carry a truncation notice")
281}
282
283// ---------- re-audit regressions (2026-09-02) ----------
284
285// skipSeconds advances chain time (5s per height).
286func skipSeconds(sec int64) {
287	testing.SkipHeights((sec + 4) / 5)
288}
289
290func TestReservation_ExpiresAndFreesTheName(cur realm, t *testing.T) {
291	resetState()
292	register(cur, "price_oracle")
293	testing.SetRealm(testing.NewUserRealm(owner))
294	Deregister(cross(cur), "price_oracle")
295
296	// re-audit P1: eternal tombstones let an attacker lock the namespace
297	// forever; within the window the name stays reserved...
298	testing.SetRealm(testing.NewUserRealm(attacker))
299	uassert.AbortsWithMessage(t, cur,
300		"name is reserved for its former owner: price_oracle",
301		func() { RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "d", "oracle", "") },
302	)
303
304	// ...but after ReservationPeriod it frees for anyone.
305	// SkipHeights resets the whole test context, so the caller realm has
306	// to be re-armed after the jump.
307	skipSeconds(ReservationPeriod + 10)
308	testing.SetRealm(testing.NewUserRealm(attacker))
309	RegisterService(cross(cur), "price_oracle", "gno.land/r/other/oracle", "d", "oracle", "")
310	uassert.Equal(t, "gno.land/r/other/oracle", Resolve("price_oracle"))
311
312	// a LAPSED reservation confers nothing: the new registrant is stamped
313	// as registrant, and the dead tombstone is reclaimed
314	uassert.Equal(t, string(attacker), string(services["price_oracle"].Registrant),
315		"a lapsed reservation must not preserve the old registrant")
316	_, stillRetired := retired["price_oracle"]
317	uassert.False(t, stillRetired, "a lapsed tombstone must be reclaimed")
318}
319
320func TestReservation_OriginalRegistrantCanReclaim(cur realm, t *testing.T) {
321	resetState()
322	register(cur, "price_oracle")
323
324	// re-audit P2: hostile transferee deregisters — the ORIGINAL
325	// registrant can still reclaim during the reservation window
326	handOff(cur, owner, attacker, "price_oracle")
327	testing.SetRealm(testing.NewUserRealm(attacker))
328	Deregister(cross(cur), "price_oracle")
329
330	testing.SetRealm(testing.NewUserRealm(owner))
331	RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "")
332	uassert.Equal(t, string(owner), string(services["price_oracle"].Owner))
333}
334
335func TestPkgPath_StructuralValidation(cur realm, t *testing.T) {
336	resetState()
337	testing.SetRealm(testing.NewUserRealm(owner))
338
339	// re-audit P3: traversal, doubled slashes, trailing slash, hyphens,
340	// and non-r/p roots must all be refused
341	bad := []string{
342		"gno.land/r/../../etc/passwd",
343		"gno.land//r//demo//oracle",
344		"gno.land/r/demo/oracle/",
345		"gno.land/r/de-mo/oracle",
346		"gno.land/x/demo/oracle",
347		"gno.land/.",
348	}
349	for _, p := range bad {
350		uassert.AbortsContains(t, cur, "pkgpath",
351			func() { RegisterService(cross(cur), "svc", p, "d", "oracle", "") })
352	}
353	// legitimate r/ and p/ paths still pass
354	RegisterService(cross(cur), "svc_r", "gno.land/r/demo/oracle_v2", "d", "oracle", "")
355	RegisterService(cross(cur), "svc_p", "gno.land/p/nt/avl", "d", "oracle", "")
356}
357
358func TestTryResolve(cur realm, t *testing.T) {
359	resetState()
360	register(cur, "price_oracle")
361
362	got, ok := TryResolve("price_oracle")
363	uassert.True(t, ok, "known name must resolve")
364	uassert.Equal(t, goodPath, got)
365
366	_, ok = TryResolve("ghost")
367	uassert.False(t, ok, "unknown name must return ok=false, never panic")
368}
369
370// ---------- round-3 audit regressions (2026-09-02) ----------
371
372func TestReservation_RegistrantSurvivesHostileRecycle(cur realm, t *testing.T) {
373	resetState()
374	register(cur, "price_oracle")
375
376	// round-3 P2: hostile transferee deregisters and INSTANTLY
377	// re-registers — the original registrant must survive the recycle
378	handOff(cur, owner, attacker, "price_oracle")
379	testing.SetRealm(testing.NewUserRealm(attacker))
380	Deregister(cross(cur), "price_oracle")
381	RegisterService(cross(cur), "price_oracle", "gno.land/r/evil/oracle", "mine", "oracle", "")
382	uassert.Equal(t, string(owner), string(services["price_oracle"].Registrant),
383		"in-window re-register must preserve the original registrant")
384
385	// so when the attacker abandons again, the original can still reclaim
386	Deregister(cross(cur), "price_oracle")
387	testing.SetRealm(testing.NewUserRealm(owner))
388	RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "")
389	uassert.Equal(t, string(owner), string(services["price_oracle"].Owner))
390}
391
392func TestPkgPath_PinnedBehaviors(cur realm, t *testing.T) {
393	resetState()
394	testing.SetRealm(testing.NewUserRealm(owner))
395
396	// round-3 pins: top-level realm path accepted; digit-leading element
397	// and uppercase prefix rejected
398	RegisterService(cross(cur), "toplevel", "gno.land/r/x", "d", "oracle", "")
399	uassert.AbortsContains(t, cur, "invalid pkgpath element",
400		func() { RegisterService(cross(cur), "digits", "gno.land/r/9abc", "d", "oracle", "") })
401	uassert.AbortsContains(t, cur, "pkgpath must start with",
402		func() { RegisterService(cross(cur), "upper", "GNO.LAND/r/demo/x", "d", "oracle", "") })
403}
404
405// ---------- pearl audit regressions (2026-09-21) ----------
406
407// R1: one funded key could previously claim the ENTIRE namespace, since
408// the only bound was the global MaxServices. The per-owner cap makes a
409// namespace sweep cost MaxServices/MaxServicesPerOwner distinct funded
410// addresses instead of one.
411func TestRegister_PerOwnerQuota(cur realm, t *testing.T) {
412	resetState()
413	fillFor(cur, owner, "svc", MaxServicesPerOwner)
414	testing.SetRealm(testing.NewUserRealm(owner)) // fillFor's realm is frame-scoped
415
416	n, limit := OwnerServiceCount(owner)
417	uassert.Equal(t, MaxServicesPerOwner, n)
418	uassert.Equal(t, MaxServicesPerOwner, limit)
419
420	uassert.AbortsWithMessage(t, cur, quotaMsg(),
421		func() { RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") })
422
423	// a DIFFERENT address is completely unaffected — the cap is per-owner,
424	// not a global freeze
425	testing.SetRealm(testing.NewUserRealm(attacker))
426	RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "")
427	uassert.Equal(t, goodPath, Resolve("one_more"))
428
429	// freeing a slot restores headroom
430	testing.SetRealm(testing.NewUserRealm(owner))
431	Deregister(cross(cur), "svc0")
432	n, _ = OwnerServiceCount(owner)
433	uassert.Equal(t, MaxServicesPerOwner-1, n)
434	RegisterService(cross(cur), "fresh_name", goodPath, "d", "oracle", "")
435	n, _ = OwnerServiceCount(owner)
436	uassert.Equal(t, MaxServicesPerOwner, n)
437}
438
439// The quota index must not accumulate dead keys: an owner that drops to
440// zero is deleted outright.
441func TestQuota_IndexStaysZeroFree(cur realm, t *testing.T) {
442	resetState()
443	register(cur, "solo")
444	uassert.Equal(t, 1, len(ownerServices))
445
446	testing.SetRealm(testing.NewUserRealm(owner))
447	Deregister(cross(cur), "solo")
448	n, _ := OwnerServiceCount(owner)
449	uassert.Equal(t, 0, n)
450	uassert.Equal(t, 0, len(ownerServices), "a zeroed owner must leave no key behind")
451}
452
453// The global cap still backstops the per-owner cap. Reaching it through
454// the public API would take MaxServices/MaxServicesPerOwner distinct
455// funded addresses, so the state is staged directly (this test is
456// in-package) and only the guard is exercised.
457func TestRegister_GlobalCap(cur realm, t *testing.T) {
458	resetState()
459	for i := 0; i < MaxServices; i++ {
460		n := "pad" + strconv.Itoa(i)
461		services[n] = &Service{
462			Name: n, Owner: attacker, Registrant: attacker,
463			PkgPath: goodPath, Description: "d", ServiceType: "oracle",
464		}
465		names = append(names, n)
466	}
467	count, limit := ServiceCount()
468	uassert.Equal(t, MaxServices, count)
469	uassert.Equal(t, MaxServices, limit)
470
471	testing.SetRealm(testing.NewUserRealm(owner))
472	uassert.AbortsWithMessage(t, cur, "global service limit reached",
473		func() { RegisterService(cross(cur), "one_more", goodPath, "d", "oracle", "") })
474}
475
476// Y2: this realm holds no banker and exposes no withdrawal path, so coins
477// attached to any entrypoint would strand at the realm address forever.
478// Every crossing function must abort, which reverts the transfer.
479func TestStraySendRejected(cur realm, t *testing.T) {
480	resetState()
481	register(cur, "price_oracle")
482	testing.SetRealm(testing.NewUserRealm(owner))
483	TransferOwnership(cross(cur), "price_oracle", newOwner)
484
485	msg := "this realm does not accept coins"
486	testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
487	uassert.AbortsWithMessage(t, cur, msg,
488		func() { RegisterService(cross(cur), "paid", goodPath, "d", "oracle", "") })
489	uassert.AbortsWithMessage(t, cur, msg,
490		func() { UpdateService(cross(cur), "price_oracle", goodPath, "paid", "oracle", "") })
491	uassert.AbortsWithMessage(t, cur, msg,
492		func() { TransferOwnership(cross(cur), "price_oracle", third) })
493	uassert.AbortsWithMessage(t, cur, msg,
494		func() { CancelOwnershipTransfer(cross(cur), "price_oracle") })
495	uassert.AbortsWithMessage(t, cur, msg,
496		func() { Deregister(cross(cur), "price_oracle") })
497
498	testing.SetRealm(testing.NewUserRealm(newOwner))
499	testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})
500	uassert.AbortsWithMessage(t, cur, msg,
501		func() { AcceptOwnership(cross(cur), "price_oracle") })
502	testing.SetOriginSend(nil)
503
504	// nothing was mutated by any of the rejected calls
505	uassert.Equal(t, string(owner), string(GetOwner("price_oracle")))
506	uassert.Equal(t, string(newOwner), GetPendingOwner("price_oracle"))
507	uassert.Equal(t, "a price oracle", services["price_oracle"].Description)
508	_, ok := TryResolve("paid")
509	uassert.False(t, ok, "a rejected registration must not stick")
510}
511
512// Y3: Render is reachable by any viewer, so its cost lands on third
513// parties rather than on whoever grew the state. It must be bounded and
514// must say so.
515func TestRender_Bounded(cur realm, t *testing.T) {
516	resetState()
517	fillFor(cur, owner, "a", MaxServicesPerOwner)
518	extra := MaxRenderServices - MaxServicesPerOwner + 1
519	fillFor(cur, attacker, "b", extra)
520
521	total := MaxServicesPerOwner + extra
522	count, _ := ServiceCount()
523	uassert.Equal(t, total, count)
524
525	out := Render("")
526	uassert.True(t, strings.Contains(out, "**Total services:** "+strconv.Itoa(total)),
527		"the header must report the TRUE total, not the rendered count")
528	uassert.Equal(t, MaxRenderServices, strings.Count(out, "`"+goodPath+"`"),
529		"exactly MaxRenderServices rows may be emitted")
530	uassert.True(t, strings.Contains(out,
531		"_Showing "+strconv.Itoa(MaxRenderServices)+" of "+strconv.Itoa(total)+" services."),
532		"a truncated render must disclose the truncation")
533	uassert.True(t, strings.Contains(out, "Use ListServices, ListByType or GetService"),
534		"the notice must name the unbounded queries")
535}
536
537// Y5: the hand-rolled sanitizer replaced only backticks and pipes, leaving
538// `[`, `]`, `(`, `)` and `!` live — so any registrant could inject a
539// working markdown link or image into a table cell and phish every viewer.
540func TestRender_RejectsMarkdownLinkInjection(cur realm, t *testing.T) {
541	resetState()
542	testing.SetRealm(testing.NewUserRealm(owner))
543	RegisterService(cross(cur), "phish", goodPath,
544		"claim [here](https://evil.example/drain)", "oracle", "")
545	RegisterService(cross(cur), "tracker", goodPath,
546		"![x](https://evil.example/pixel.png)", "oracle", "")
547
548	out := Render("")
549	uassert.False(t, strings.Contains(out, "]("),
550		"no live markdown link/image syntax may survive into a table cell")
551	uassert.False(t, strings.Contains(out, "!["),
552		"no live markdown image opener may survive into a table cell")
553	// the text itself is still shown — it is neutered, not censored. The
554	// sanitizer escapes `.` too, so the raw host never appears verbatim.
555	uassert.True(t, strings.Contains(out, "evil"),
556		"the payload text must still be visible to the reader")
557	uassert.False(t, strings.Contains(out, "evil.example"),
558		"even the dot is escaped — no autolinkable host survives")
559
560	// the same class of payload must not survive the single-line summary
561	sum := GetService("phish")
562	uassert.False(t, strings.Contains(sum, "]("),
563		"GetService must escape markdown link syntax too")
564}
565
566// Y4: the one-step transfer this replaces was a permanent-brick hazard.
567// address.IsValid() only checks bech32 form, so a well-formed but unowned
568// destination committed immediately, after which the entry could never be
569// updated, transferred or deregistered — and because it could never be
570// deregistered it could never enter the reservation window either, so the
571// NAME became a permanent hole in a shared global namespace.
572func TestTransferOwnership_TwoStep(cur realm, t *testing.T) {
573	resetState()
574	register(cur, "price_oracle")
575
576	// nomination alone changes NOTHING
577	testing.SetRealm(testing.NewUserRealm(owner))
578	TransferOwnership(cross(cur), "price_oracle", newOwner)
579	uassert.Equal(t, string(owner), string(GetOwner("price_oracle")))
580	uassert.Equal(t, string(newOwner), GetPendingOwner("price_oracle"))
581
582	// the sitting owner keeps full control while the handoff is open
583	UpdateService(cross(cur), "price_oracle", goodPath, "still mine", "oracle", "")
584
585	// only the nominee may accept
586	testing.SetRealm(testing.NewUserRealm(attacker))
587	uassert.AbortsWithMessage(t, cur, "only the nominated owner can accept",
588		func() { AcceptOwnership(cross(cur), "price_oracle") })
589
590	testing.SetRealm(testing.NewUserRealm(newOwner))
591	AcceptOwnership(cross(cur), "price_oracle")
592	uassert.Equal(t, string(newOwner), string(GetOwner("price_oracle")))
593	uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
594
595	// the quota moved with the entry
596	n, _ := OwnerServiceCount(owner)
597	uassert.Equal(t, 0, n)
598	n, _ = OwnerServiceCount(newOwner)
599	uassert.Equal(t, 1, n)
600
601	// the former owner is now an outsider
602	testing.SetRealm(testing.NewUserRealm(owner))
603	uassert.AbortsWithMessage(t, cur, "only the owner can update this service",
604		func() { UpdateService(cross(cur), "price_oracle", goodPath, "back", "oracle", "") })
605}
606
607func TestTransferOwnership_Cancel(cur realm, t *testing.T) {
608	resetState()
609	register(cur, "price_oracle")
610
611	testing.SetRealm(testing.NewUserRealm(owner))
612	uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle",
613		func() { CancelOwnershipTransfer(cross(cur), "price_oracle") })
614
615	TransferOwnership(cross(cur), "price_oracle", newOwner)
616	testing.SetRealm(testing.NewUserRealm(attacker))
617	uassert.AbortsWithMessage(t, cur, "only the owner can cancel a transfer",
618		func() { CancelOwnershipTransfer(cross(cur), "price_oracle") })
619
620	testing.SetRealm(testing.NewUserRealm(owner))
621	CancelOwnershipTransfer(cross(cur), "price_oracle")
622	uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
623
624	testing.SetRealm(testing.NewUserRealm(newOwner))
625	uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle",
626		func() { AcceptOwnership(cross(cur), "price_oracle") })
627
628	// re-nominating and then clearing with the empty address is equivalent
629	testing.SetRealm(testing.NewUserRealm(owner))
630	TransferOwnership(cross(cur), "price_oracle", newOwner)
631	TransferOwnership(cross(cur), "price_oracle", address(""))
632	uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
633}
634
635// The quota is checked at CONSENT, not at nomination, so a nomination can
636// never push an account past its cap without that account agreeing.
637func TestTransferOwnership_QuotaCheckedAtConsent(cur realm, t *testing.T) {
638	resetState()
639	fillFor(cur, newOwner, "b", MaxServicesPerOwner)
640	register(cur, "extra")
641
642	// nomination succeeds even though the nominee is full...
643	testing.SetRealm(testing.NewUserRealm(owner))
644	TransferOwnership(cross(cur), "extra", newOwner)
645	uassert.Equal(t, string(newOwner), GetPendingOwner("extra"))
646
647	// ...but consent is refused, and the entry stays with its owner
648	testing.SetRealm(testing.NewUserRealm(newOwner))
649	uassert.AbortsWithMessage(t, cur, quotaMsg(),
650		func() { AcceptOwnership(cross(cur), "extra") })
651	uassert.Equal(t, string(owner), string(GetOwner("extra")))
652
653	// freeing one slot lets the same handoff complete
654	Deregister(cross(cur), "b0")
655	AcceptOwnership(cross(cur), "extra")
656	uassert.Equal(t, string(newOwner), string(GetOwner("extra")))
657	n, _ := OwnerServiceCount(newOwner)
658	uassert.Equal(t, MaxServicesPerOwner, n)
659}
660
661// A nomination must not outlive the entry it was made against — otherwise
662// a stale nominee could seize a name that was deregistered and reclaimed.
663func TestTransferOwnership_NominationDiesWithEntry(cur realm, t *testing.T) {
664	resetState()
665	register(cur, "price_oracle")
666
667	testing.SetRealm(testing.NewUserRealm(owner))
668	TransferOwnership(cross(cur), "price_oracle", attacker)
669	Deregister(cross(cur), "price_oracle")
670	RegisterService(cross(cur), "price_oracle", goodPath, "reclaimed", "oracle", "")
671	uassert.Equal(t, "none", GetPendingOwner("price_oracle"))
672
673	testing.SetRealm(testing.NewUserRealm(attacker))
674	uassert.AbortsWithMessage(t, cur, "no pending ownership transfer for: price_oracle",
675		func() { AcceptOwnership(cross(cur), "price_oracle") })
676	uassert.Equal(t, string(owner), string(GetOwner("price_oracle")))
677}