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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

subscriptions_test.gno

27.58 Kb · 827 lines
  1package subscriptions
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime"
  7	"strings"
  8	"testing"
  9
 10	"gno.land/p/nt/testutils/v0"
 11	"gno.land/p/nt/uassert/v0"
 12)
 13
 14var (
 15	testAdmin = testutils.TestAddress("subadmin")
 16	prov1     = testutils.TestAddress("subprov1")
 17	prov2     = testutils.TestAddress("subprov2")
 18	alice     = testutils.TestAddress("subalice")
 19	bob       = testutils.TestAddress("subbob")
 20	mallory   = testutils.TestAddress("submallory")
 21)
 22
 23// Established harness facts, carried from the sibling realms' suites:
 24// init runs with an empty origin context (the real deploy captures the
 25// origin caller); the banker resets between tests while realm globals
 26// persist, so conservation is asserted in DELTA form after re-seeding
 27// carried liabilities; testing.SetRealm affects only the frame that
 28// calls it; and testing.SkipHeights WIPES the origin/realm context, so
 29// every caller is re-armed after a skip.
 30func init() {
 31	admin = testAdmin
 32	feeRecipient = testAdmin
 33	self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions")
 34}
 35
 36type baseline struct{ held, users, fees int64 }
 37
 38func begin() baseline {
 39	if l := Liabilities(); l > 0 {
 40		testing.IssueCoins(Address(), ugnot(l))
 41	}
 42	return snap()
 43}
 44
 45func snap() baseline {
 46	return baseline{held: Held(), users: UsersTotal(), fees: FeesAccrued()}
 47}
 48
 49// conserved asserts ΔH == ΔU + ΔF + extraSurplus — the realm's monetary
 50// invariant in delta form. There is no escrow pool in this realm.
 51func (b baseline) conserved(t *testing.T, extraSurplus int64) {
 52	t.Helper()
 53	dH := Held() - b.held
 54	dU := UsersTotal() - b.users
 55	dF := FeesAccrued() - b.fees
 56	if dH != dU+dF+extraSurplus {
 57		t.Fatalf("conservation violated: ΔH=%d ΔU=%d ΔF=%d ΔS=%d", dH, dU, dF, extraSurplus)
 58	}
 59	if Liabilities() != UsersTotal()+FeesAccrued() {
 60		t.Fatalf("Liabilities() = %d, want U+F", Liabilities())
 61	}
 62}
 63
 64func coinsOf(addr address) int64 {
 65	return banker.NewReadonlyBanker().GetCoin(addr, Denom)
 66}
 67
 68func ugnot(n int64) chain.Coins {
 69	return chain.NewCoins(chain.NewCoin(Denom, n))
 70}
 71
 72// NOTE ON THE HARNESS. testing.SetRealm applies to the frame that CALLS
 73// it, so the caller identity must be armed in the SAME frame as the
 74// entrypoint invocation — the three lines are inlined at every site,
 75// matching the sibling suites.
 76
 77// planAs publishes a plan (non-payable: envelope explicitly cleared).
 78func planAs(cur realm, from address, title string, price, period int64) int64 {
 79	testing.SetRealm(testing.NewUserRealm(from))
 80	testing.SetOriginCaller(from)
 81	testing.SetOriginSend(nil)
 82	return CreatePlan(cross(cur), title, "d", price, period, MaxFeeBps)
 83}
 84
 85// subscribeAs performs a chain-faithful subscribe: envelope staged AND landed.
 86func subscribeAs(cur realm, from address, planID, pay int64) int64 {
 87	testing.SetRealm(testing.NewUserRealm(from))
 88	testing.SetOriginCaller(from)
 89	testing.SetOriginSend(ugnot(pay))
 90	testing.IssueCoins(Address(), ugnot(pay))
 91	return Subscribe(cross(cur), planID)
 92}
 93
 94// renewAs performs a chain-faithful renewal.
 95func renewAs(cur realm, from address, subID, pay int64) {
 96	testing.SetRealm(testing.NewUserRealm(from))
 97	testing.SetOriginCaller(from)
 98	testing.SetOriginSend(ugnot(pay))
 99	testing.IssueCoins(Address(), ugnot(pay))
100	Renew(cross(cur), subID)
101}
102
103func TestInitialConfig(t *testing.T) {
104	if Admin() != testAdmin || FeeRecipient() != testAdmin {
105		t.Fatal("seeded roles mismatch")
106	}
107	var zero address
108	if PendingAdmin() != zero {
109		t.Fatal("pending admin not zero")
110	}
111	if FeeBps() != 0 || NumPlans() != 0 || NumSubs() != 0 {
112		t.Fatal("initial counters not zero")
113	}
114}
115
116func TestCreatePlanValidation(cur realm, t *testing.T) {
117	testing.SetRealm(testing.NewUserRealm(prov1))
118	testing.SetOriginCaller(prov1)
119	testing.SetOriginSend(ugnot(5))
120	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
121		CreatePlan(cross(cur), "x", "d", 1, MinPeriodBlocks, MaxFeeBps)
122	})
123
124	testing.SetRealm(testing.NewUserRealm(prov1))
125	testing.SetOriginCaller(prov1)
126	testing.SetOriginSend(nil)
127	uassert.AbortsContains(t, cur, "title must be", func() {
128		CreatePlan(cross(cur), "", "d", 1, MinPeriodBlocks, MaxFeeBps)
129	})
130	uassert.AbortsContains(t, cur, "title must be", func() {
131		CreatePlan(cross(cur), strings.Repeat("t", MaxTitleLen+1), "d", 1, MinPeriodBlocks, MaxFeeBps)
132	})
133	uassert.AbortsContains(t, cur, "description too long", func() {
134		CreatePlan(cross(cur), "x", strings.Repeat("d", MaxDescLen+1), 1, MinPeriodBlocks, MaxFeeBps)
135	})
136	uassert.AbortsContains(t, cur, "price must be at least", func() {
137		CreatePlan(cross(cur), "x", "d", 0, MinPeriodBlocks, MaxFeeBps)
138	})
139	uassert.AbortsContains(t, cur, "periodBlocks must be in", func() {
140		CreatePlan(cross(cur), "x", "d", 1, MinPeriodBlocks-1, MaxFeeBps)
141	})
142	uassert.AbortsContains(t, cur, "periodBlocks must be in", func() {
143		CreatePlan(cross(cur), "x", "d", 1, MaxPeriodBlocks+1, MaxFeeBps)
144	})
145}
146
147func TestFeeConsentCeiling(cur realm, t *testing.T) {
148	testing.SetRealm(testing.NewUserRealm(testAdmin))
149	testing.SetOriginCaller(testAdmin)
150	testing.SetOriginSend(nil)
151	SetFeeBps(cross(cur), 250)
152
153	// A provider who signed for at most 100 bps is refused while the
154	// fee is 250.
155	testing.SetRealm(testing.NewUserRealm(prov1))
156	testing.SetOriginCaller(prov1)
157	testing.SetOriginSend(nil)
158	uassert.AbortsContains(t, cur, "exceeds the caller's maximum", func() {
159		CreatePlan(cross(cur), "capped", "d", 100, MinPeriodBlocks, 100)
160	})
161
162	// A consenting provider snapshots 250 into the plan; a later fee
163	// change does not touch it.
164	id := planAs(cur, prov1, "snap", 1000, MinPeriodBlocks)
165	testing.SetRealm(testing.NewUserRealm(testAdmin))
166	testing.SetOriginCaller(testAdmin)
167	testing.SetOriginSend(nil)
168	SetFeeBps(cross(cur), 0)
169	_, _, _, _, planFee, _, _ := PlanInfo(id)
170	if planFee != 250 {
171		t.Fatalf("plan feeBps = %d, want the 250 snapshot", planFee)
172	}
173
174	// The subscription charges the SNAPSHOT, not the current fee.
175	b := begin()
176	subscribeAs(cur, alice, id, 1000)
177	if got := FeesAccrued() - b.fees; got != 25 {
178		t.Fatalf("fee charged = %d, want 25 (250 bps of 1000)", got)
179	}
180	if got := BalanceOf(prov1); got == 0 {
181		t.Fatal("provider not credited")
182	}
183	b.conserved(t, 0)
184}
185
186func TestPlanQuota(cur realm, t *testing.T) {
187	for i := 0; i < MaxPlansPerProvider; i++ {
188		planAs(cur, prov2, "q", 1, MinPeriodBlocks)
189	}
190	testing.SetRealm(testing.NewUserRealm(prov2))
191	testing.SetOriginCaller(prov2)
192	testing.SetOriginSend(nil)
193	uassert.AbortsWithMessage(t, cur, "per-provider plan limit reached", func() {
194		CreatePlan(cross(cur), "q", "d", 1, MinPeriodBlocks, MaxFeeBps)
195	})
196	// Retiring one frees the slot.
197	RetirePlan(cross(cur), NumPlans()-1)
198	planAs(cur, prov2, "q2", 1, MinPeriodBlocks)
199}
200
201func TestRetirePlanAuthority(cur realm, t *testing.T) {
202	id := planAs(cur, prov1, "mine", 5, MinPeriodBlocks)
203	testing.SetRealm(testing.NewUserRealm(mallory))
204	testing.SetOriginCaller(mallory)
205	testing.SetOriginSend(nil)
206	uassert.AbortsWithMessage(t, cur, "only the provider may retire a plan", func() {
207		RetirePlan(cross(cur), id)
208	})
209	testing.SetRealm(testing.NewUserRealm(prov1))
210	testing.SetOriginCaller(prov1)
211	testing.SetOriginSend(nil)
212	RetirePlan(cross(cur), id)
213	uassert.AbortsWithMessage(t, cur, "plan is not active", func() {
214		RetirePlan(cross(cur), id)
215	})
216	// No new subscriptions on a retired plan.
217	testing.SetRealm(testing.NewUserRealm(alice))
218	testing.SetOriginCaller(alice)
219	testing.SetOriginSend(ugnot(5))
220	testing.IssueCoins(Address(), ugnot(5))
221	uassert.AbortsWithMessage(t, cur, "plan is not active", func() {
222		Subscribe(cross(cur), id)
223	})
224}
225
226func TestSubscribeLifecycle(cur realm, t *testing.T) {
227	id := planAs(cur, prov1, "basic", 100, 50)
228	b := begin()
229	h0 := runtime.ChainHeight()
230	subID := subscribeAs(cur, alice, id, 100)
231
232	planID, subscriber, provider, price, paidThrough, periods, status := SubInfo(subID)
233	if planID != id || subscriber != alice || provider != prov1 || price != 100 ||
234		periods != 1 || status != SubActive {
235		t.Fatal("subscription fields wrong")
236	}
237	if paidThrough != h0+50 {
238		t.Fatalf("paidThrough = %d, want %d", paidThrough, h0+50)
239	}
240	if !Entitled(subID) || !EntitledFor(id, alice) {
241		t.Fatal("fresh subscription not entitled")
242	}
243	if got, ok := ActiveSubID(id, alice); !ok || got != subID {
244		t.Fatal("active index wrong")
245	}
246	b.conserved(t, 0)
247
248	// Exact envelope: under- and overpayment refused.
249	for _, bad := range []int64{99, 101} {
250		testing.SetRealm(testing.NewUserRealm(bob))
251		testing.SetOriginCaller(bob)
252		testing.SetOriginSend(ugnot(bad))
253		testing.IssueCoins(Address(), ugnot(bad))
254		uassert.AbortsContains(t, cur, "send exactly the plan price", func() {
255			Subscribe(cross(cur), id)
256		})
257	}
258
259	// A second live subscription to the same plan is refused — the
260	// identity-level duplicate-payment guard.
261	testing.SetRealm(testing.NewUserRealm(alice))
262	testing.SetOriginCaller(alice)
263	testing.SetOriginSend(ugnot(100))
264	testing.IssueCoins(Address(), ugnot(100))
265	uassert.AbortsContains(t, cur, "renew it instead", func() {
266		Subscribe(cross(cur), id)
267	})
268}
269
270func TestRenewalWindow(cur realm, t *testing.T) {
271	id := planAs(cur, prov1, "window", 100, 50)
272	subID := subscribeAs(cur, bob, id, 100)
273	_, _, _, _, pt0, _, _ := SubInfo(subID)
274
275	from, until := RenewableFrom(subID)
276	if from != pt0-50 || until != pt0+49 {
277		t.Fatalf("window [%d,%d], want [%d,%d]", from, until, pt0-50, pt0+49)
278	}
279
280	// Straight after Subscribe, remaining entitlement equals exactly
281	// one period (the running one), so ONE prepay renewal is inside
282	// the early bound — and extension is FROM paidThrough.
283	b := begin()
284	renewAs(cur, bob, subID, 100)
285	_, _, _, _, pt1, periods, _ := SubInfo(subID)
286	if pt1 != pt0+50 || periods != 2 {
287		t.Fatalf("paidThrough = %d periods = %d, want %d and 2", pt1, periods, pt0+50)
288	}
289	b.conserved(t, 0)
290
291	// A SECOND renewal in the same block is the accidental duplicate,
292	// and the early bound refuses it: more than one unstarted period
293	// would be prepaid.
294	testing.SetRealm(testing.NewUserRealm(bob))
295	testing.SetOriginCaller(bob)
296	testing.SetOriginSend(ugnot(100))
297	testing.IssueCoins(Address(), ugnot(100))
298	uassert.AbortsContains(t, cur, "too early", func() {
299		Renew(cross(cur), subID)
300	})
301
302	// Wrong payer: a third party cannot extend someone else's
303	// subscription even inside the window.
304	testing.SkipHeights(50)
305	testing.SetRealm(testing.NewUserRealm(mallory))
306	testing.SetOriginCaller(mallory)
307	testing.SetOriginSend(ugnot(100))
308	testing.IssueCoins(Address(), ugnot(100))
309	uassert.AbortsWithMessage(t, cur, "only the subscriber may renew", func() {
310		Renew(cross(cur), subID)
311	})
312
313	// Wrong amount inside the window.
314	testing.SetRealm(testing.NewUserRealm(bob))
315	testing.SetOriginCaller(bob)
316	testing.SetOriginSend(ugnot(99))
317	testing.IssueCoins(Address(), ugnot(99))
318	uassert.AbortsContains(t, cur, "send exactly the subscription price", func() {
319		Renew(cross(cur), subID)
320	})
321}
322
323func TestGraceBoundary(cur realm, t *testing.T) {
324	id := planAs(cur, prov1, "grace", 100, 50)
325	subID := subscribeAs(cur, alice, id, 100)
326	_, _, _, _, pt, _, _ := SubInfo(subID)
327
328	// Move to the LAST renewable height: h == paidThrough +
329	// periodBlocks - 1 (the late bound is exclusive — audit Y2).
330	testing.SkipHeights(pt + 49 - runtime.ChainHeight())
331	if Entitled(subID) {
332		t.Fatal("entitlement must have lapsed past paidThrough")
333	}
334	// Renewal at the boundary is accepted, extends FROM paidThrough —
335	// and buys at least one block: the Y2 regression.
336	renewAs(cur, alice, subID, 100)
337	_, _, _, _, pt1, _, _ := SubInfo(subID)
338	if pt1 != pt+50 {
339		t.Fatalf("paidThrough = %d, want %d (extension from paidThrough)", pt1, pt+50)
340	}
341	if !Entitled(subID) {
342		t.Fatal("a boundary renewal must buy at least one block of entitlement")
343	}
344
345	// At the grace end itself renewal is refused, Expire opens: the
346	// two sets partition exactly.
347	testing.SkipHeights(pt1 + 50 - runtime.ChainHeight())
348	testing.SetRealm(testing.NewUserRealm(alice))
349	testing.SetOriginCaller(alice)
350	testing.SetOriginSend(ugnot(100))
351	testing.IssueCoins(Address(), ugnot(100))
352	uassert.AbortsContains(t, cur, "grace window over", func() {
353		Renew(cross(cur), subID)
354	})
355
356	// Expire is permissionless: mallory may.
357	testing.SetRealm(testing.NewUserRealm(mallory))
358	testing.SetOriginCaller(mallory)
359	testing.SetOriginSend(nil)
360	Expire(cross(cur), subID)
361	_, _, _, _, _, _, status := SubInfo(subID)
362	if status != SubExpired {
363		t.Fatal("not expired")
364	}
365	if EntitledFor(id, alice) {
366		t.Fatal("expired subscription still indexed")
367	}
368	// Terminal: renewal refused by status.
369	testing.SetRealm(testing.NewUserRealm(alice))
370	testing.SetOriginCaller(alice)
371	testing.SetOriginSend(ugnot(100))
372	testing.IssueCoins(Address(), ugnot(100))
373	uassert.AbortsContains(t, cur, "subscription is expired", func() {
374		Renew(cross(cur), subID)
375	})
376	// The slot is free: a fresh Subscribe works.
377	subscribeAs(cur, alice, id, 100)
378}
379
380func TestExpireGuards(cur realm, t *testing.T) {
381	id := planAs(cur, prov1, "expguard", 100, 50)
382	subID := subscribeAs(cur, bob, id, 100)
383	testing.SetRealm(testing.NewUserRealm(mallory))
384	testing.SetOriginCaller(mallory)
385	testing.SetOriginSend(nil)
386	uassert.AbortsContains(t, cur, "not expirable", func() {
387		Expire(cross(cur), subID)
388	})
389	// One block before the grace end it is still not expirable...
390	_, _, _, _, pt, _, _ := SubInfo(subID)
391	testing.SkipHeights(pt + 49 - runtime.ChainHeight())
392	testing.SetRealm(testing.NewUserRealm(mallory))
393	testing.SetOriginCaller(mallory)
394	testing.SetOriginSend(nil)
395	uassert.AbortsContains(t, cur, "not expirable", func() {
396		Expire(cross(cur), subID)
397	})
398	// ...and AT the grace end it is: the sets partition exactly.
399	testing.SkipHeights(1)
400	testing.SetRealm(testing.NewUserRealm(mallory))
401	testing.SetOriginCaller(mallory)
402	testing.SetOriginSend(nil)
403	Expire(cross(cur), subID)
404	testing.SetRealm(testing.NewUserRealm(mallory))
405	testing.SetOriginCaller(mallory)
406	testing.SetOriginSend(nil)
407	uassert.AbortsContains(t, cur, "subscription is expired", func() {
408		Expire(cross(cur), subID)
409	})
410}
411
412func TestSubscribeCollapsesExpiredIncumbent(cur realm, t *testing.T) {
413	id := planAs(cur, prov1, "collapse", 100, 50)
414	oldID := subscribeAs(cur, alice, id, 100)
415	_, _, _, _, pt, _, _ := SubInfo(oldID)
416	testing.SkipHeights(pt + 50 - runtime.ChainHeight())
417
418	// No Expire call happened; Subscribe collapses the incumbent
419	// itself, at the first expirable height.
420	newID := subscribeAs(cur, alice, id, 100)
421	if newID == oldID {
422		t.Fatal("expected a fresh subscription id")
423	}
424	_, _, _, _, _, _, oldStatus := SubInfo(oldID)
425	if oldStatus != SubExpired {
426		t.Fatalf("incumbent status = %q, want expired", oldStatus)
427	}
428	if got, ok := ActiveSubID(id, alice); !ok || got != newID {
429		t.Fatal("index does not point at the fresh subscription")
430	}
431}
432
433func TestCancel(cur realm, t *testing.T) {
434	id := planAs(cur, prov1, "cancel", 100, 50)
435	subID := subscribeAs(cur, bob, id, 100)
436
437	testing.SetRealm(testing.NewUserRealm(mallory))
438	testing.SetOriginCaller(mallory)
439	testing.SetOriginSend(nil)
440	uassert.AbortsWithMessage(t, cur, "only the subscriber may cancel", func() {
441		Cancel(cross(cur), subID)
442	})
443
444	testing.SetRealm(testing.NewUserRealm(bob))
445	testing.SetOriginCaller(bob)
446	testing.SetOriginSend(nil)
447	Cancel(cross(cur), subID)
448	_, _, _, _, _, _, status := SubInfo(subID)
449	if status != SubCancelled {
450		t.Fatal("not cancelled")
451	}
452	// Paid entitlement survives cancellation until paidThrough — on
453	// both surfaces (audit Y1)...
454	if !Entitled(subID) || !EntitledFor(id, bob) {
455		t.Fatal("cancelled subscription lost its paid entitlement")
456	}
457	// ...but the ACTIVE slot is gone and renewal is refused forever.
458	if _, ok := ActiveSubID(id, bob); ok {
459		t.Fatal("cancelled subscription still holds the active slot")
460	}
461	testing.SetRealm(testing.NewUserRealm(bob))
462	testing.SetOriginCaller(bob)
463	testing.SetOriginSend(ugnot(100))
464	testing.IssueCoins(Address(), ugnot(100))
465	uassert.AbortsContains(t, cur, "subscription is cancelled", func() {
466		Renew(cross(cur), subID)
467	})
468	testing.SetRealm(testing.NewUserRealm(bob))
469	testing.SetOriginCaller(bob)
470	testing.SetOriginSend(nil)
471	uassert.AbortsContains(t, cur, "subscription is cancelled", func() {
472		Cancel(cross(cur), subID)
473	})
474	// The slot freed: bob may start a fresh subscription at once.
475	subscribeAs(cur, bob, id, 100)
476}
477
478func TestRenewRefusedOnRetiredPlan(cur realm, t *testing.T) {
479	id := planAs(cur, prov1, "retire-mid", 100, 50)
480	subID := subscribeAs(cur, alice, id, 100)
481	testing.SetRealm(testing.NewUserRealm(prov1))
482	testing.SetOriginCaller(prov1)
483	testing.SetOriginSend(nil)
484	RetirePlan(cross(cur), id)
485	// Entitlement continues...
486	if !Entitled(subID) {
487		t.Fatal("entitlement must survive plan retirement")
488	}
489	// ...but renewal is refused even inside the window.
490	_, _, _, _, pt, _, _ := SubInfo(subID)
491	testing.SkipHeights(pt - runtime.ChainHeight())
492	testing.SetRealm(testing.NewUserRealm(alice))
493	testing.SetOriginCaller(alice)
494	testing.SetOriginSend(ugnot(100))
495	testing.IssueCoins(Address(), ugnot(100))
496	uassert.AbortsContains(t, cur, "plan is retired", func() {
497		Renew(cross(cur), subID)
498	})
499}
500
501func TestSubscriberQuota(cur realm, t *testing.T) {
502	// Fill carol's quota with one-plan-each subscriptions, then refuse.
503	// A dedicated provider: prov2's plan quota is already saturated by
504	// TestPlanQuota, and realm globals persist across tests.
505	carol := testutils.TestAddress("subcarol")
506	provQ := testutils.TestAddress("subprovq")
507	var lastPlan int64
508	for i := 0; i < MaxSubsPerSubscriber; i++ {
509		lastPlan = planAs(cur, provQ, "quota", 1, MinPeriodBlocks)
510		subscribeAs(cur, carol, lastPlan, 1)
511		// Free the provider quota slot for the next plan.
512		testing.SetRealm(testing.NewUserRealm(provQ))
513		testing.SetOriginCaller(provQ)
514		testing.SetOriginSend(nil)
515		RetirePlan(cross(cur), lastPlan)
516	}
517	extra := planAs(cur, provQ, "quota-x", 1, MinPeriodBlocks)
518	testing.SetRealm(testing.NewUserRealm(carol))
519	testing.SetOriginCaller(carol)
520	testing.SetOriginSend(ugnot(1))
521	testing.IssueCoins(Address(), ugnot(1))
522	uassert.AbortsWithMessage(t, cur, "per-subscriber subscription limit reached", func() {
523		Subscribe(cross(cur), extra)
524	})
525	// Cancelling one frees a slot.
526	sid, ok := ActiveSubID(lastPlan, carol)
527	if !ok {
528		t.Fatal("expected an active sub to free")
529	}
530	testing.SetRealm(testing.NewUserRealm(carol))
531	testing.SetOriginCaller(carol)
532	testing.SetOriginSend(nil)
533	Cancel(cross(cur), sid)
534	subscribeAs(cur, carol, extra, 1)
535	testing.SetRealm(testing.NewUserRealm(provQ))
536	testing.SetOriginCaller(provQ)
537	testing.SetOriginSend(nil)
538	RetirePlan(cross(cur), extra)
539}
540
541func TestClaimsAndFees(cur realm, t *testing.T) {
542	testing.SetRealm(testing.NewUserRealm(testAdmin))
543	testing.SetOriginCaller(testAdmin)
544	testing.SetOriginSend(nil)
545	SetFeeBps(cross(cur), 500) // 5% into the next plan
546	id := planAs(cur, prov1, "money", 1000, 50)
547	testing.SetRealm(testing.NewUserRealm(testAdmin))
548	testing.SetOriginCaller(testAdmin)
549	testing.SetOriginSend(nil)
550	SetFeeBps(cross(cur), 0)
551
552	b := begin()
553	subscribeAs(cur, alice, id, 1000)
554	if FeesAccrued()-b.fees != 50 {
555		t.Fatalf("fee = %d, want 50", FeesAccrued()-b.fees)
556	}
557	credited := BalanceOf(prov1)
558
559	// Mallory has no balance: claims fail.
560	testing.SetRealm(testing.NewUserRealm(mallory))
561	testing.SetOriginCaller(mallory)
562	testing.SetOriginSend(nil)
563	uassert.AbortsContains(t, cur, "insufficient balance", func() { Claim(cross(cur), 1) })
564	testing.SetRealm(testing.NewUserRealm(mallory))
565	testing.SetOriginCaller(mallory)
566	testing.SetOriginSend(nil)
567	uassert.AbortsWithMessage(t, cur, "nothing to claim", func() { ClaimAll(cross(cur)) })
568
569	// Provider claims all; coins actually move.
570	before := coinsOf(prov1)
571	testing.SetRealm(testing.NewUserRealm(prov1))
572	testing.SetOriginCaller(prov1)
573	testing.SetOriginSend(nil)
574	ClaimAll(cross(cur))
575	if coinsOf(prov1)-before != credited {
576		t.Fatalf("provider received %d, want %d", coinsOf(prov1)-before, credited)
577	}
578	if BalanceOf(prov1) != 0 {
579		t.Fatal("balance not drained")
580	}
581
582	// Fees: only the recipient, and only the pot.
583	testing.SetRealm(testing.NewUserRealm(mallory))
584	testing.SetOriginCaller(mallory)
585	testing.SetOriginSend(nil)
586	uassert.AbortsWithMessage(t, cur, "fee recipient only", func() { WithdrawFees(cross(cur)) })
587	potBefore := coinsOf(testAdmin)
588	testing.SetRealm(testing.NewUserRealm(testAdmin))
589	testing.SetOriginCaller(testAdmin)
590	testing.SetOriginSend(nil)
591	WithdrawFees(cross(cur))
592	if coinsOf(testAdmin)-potBefore == 0 {
593		t.Fatal("fee pot did not move")
594	}
595	testing.SetRealm(testing.NewUserRealm(testAdmin))
596	testing.SetOriginCaller(testAdmin)
597	testing.SetOriginSend(nil)
598	uassert.AbortsWithMessage(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) })
599	b.conserved(t, 0)
600}
601
602func TestSweepReserve(cur realm, t *testing.T) {
603	id := planAs(cur, prov1, "sweep", 500, 50)
604	begin()
605	subscribeAs(cur, bob, id, 500)
606
607	// Nothing above the reserve: sweep refuses.
608	testing.SetRealm(testing.NewUserRealm(testAdmin))
609	testing.SetOriginCaller(testAdmin)
610	testing.SetOriginSend(nil)
611	uassert.AbortsContains(t, cur, "no surplus to sweep", func() {
612		SweepDenom(cross(cur), Denom)
613	})
614	// Out-of-band surplus lands; only the surplus is sweepable.
615	testing.IssueCoins(Address(), ugnot(77))
616	liabBefore := Liabilities()
617	got := coinsOf(testAdmin)
618	testing.SetRealm(testing.NewUserRealm(testAdmin))
619	testing.SetOriginCaller(testAdmin)
620	testing.SetOriginSend(nil)
621	SweepDenom(cross(cur), Denom)
622	if coinsOf(testAdmin)-got != 77 {
623		t.Fatalf("swept %d, want 77", coinsOf(testAdmin)-got)
624	}
625	if Liabilities() != liabBefore {
626		t.Fatal("sweep touched the reserve")
627	}
628	testing.SetRealm(testing.NewUserRealm(mallory))
629	testing.SetOriginCaller(mallory)
630	testing.SetOriginSend(nil)
631	uassert.AbortsWithMessage(t, cur, "fee recipient only", func() {
632		SweepDenom(cross(cur), Denom)
633	})
634}
635
636func TestAdminHandover(cur realm, t *testing.T) {
637	testing.SetRealm(testing.NewUserRealm(mallory))
638	testing.SetOriginCaller(mallory)
639	testing.SetOriginSend(nil)
640	uassert.AbortsWithMessage(t, cur, "admin only", func() {
641		TransferAdmin(cross(cur), mallory)
642	})
643	testing.SetRealm(testing.NewUserRealm(testAdmin))
644	testing.SetOriginCaller(testAdmin)
645	testing.SetOriginSend(nil)
646	TransferAdmin(cross(cur), prov2)
647	// Wrong acceptor refused; authority has not moved yet.
648	testing.SetRealm(testing.NewUserRealm(mallory))
649	testing.SetOriginCaller(mallory)
650	testing.SetOriginSend(nil)
651	uassert.AbortsWithMessage(t, cur, "only the staged successor may accept", func() {
652		AcceptAdmin(cross(cur))
653	})
654	if Admin() != testAdmin {
655		t.Fatal("authority moved before acceptance")
656	}
657	testing.SetRealm(testing.NewUserRealm(prov2))
658	testing.SetOriginCaller(prov2)
659	testing.SetOriginSend(nil)
660	AcceptAdmin(cross(cur))
661	if Admin() != prov2 {
662		t.Fatal("authority did not move")
663	}
664	// The former admin is locked out.
665	testing.SetRealm(testing.NewUserRealm(testAdmin))
666	testing.SetOriginCaller(testAdmin)
667	testing.SetOriginSend(nil)
668	uassert.AbortsWithMessage(t, cur, "admin only", func() {
669		SetFeeBps(cross(cur), 1)
670	})
671	// Hand it back for the remaining tests.
672	testing.SetRealm(testing.NewUserRealm(prov2))
673	testing.SetOriginCaller(prov2)
674	testing.SetOriginSend(nil)
675	TransferAdmin(cross(cur), testAdmin)
676	testing.SetRealm(testing.NewUserRealm(testAdmin))
677	testing.SetOriginCaller(testAdmin)
678	testing.SetOriginSend(nil)
679	AcceptAdmin(cross(cur))
680}
681
682func TestFeeBounds(cur realm, t *testing.T) {
683	testing.SetRealm(testing.NewUserRealm(testAdmin))
684	testing.SetOriginCaller(testAdmin)
685	testing.SetOriginSend(nil)
686	uassert.AbortsContains(t, cur, "fee must be in", func() {
687		SetFeeBps(cross(cur), MaxFeeBps+1)
688	})
689	testing.SetRealm(testing.NewUserRealm(testAdmin))
690	testing.SetOriginCaller(testAdmin)
691	testing.SetOriginSend(nil)
692	uassert.AbortsContains(t, cur, "fee must be in", func() {
693		SetFeeBps(cross(cur), -1)
694	})
695	var zero address
696	testing.SetRealm(testing.NewUserRealm(testAdmin))
697	testing.SetOriginCaller(testAdmin)
698	testing.SetOriginSend(nil)
699	uassert.AbortsWithMessage(t, cur, "fee recipient must not be the zero address", func() {
700		SetFeeRecipient(cross(cur), zero)
701	})
702	testing.SetRealm(testing.NewUserRealm(testAdmin))
703	testing.SetOriginCaller(testAdmin)
704	testing.SetOriginSend(nil)
705	uassert.AbortsWithMessage(t, cur, "successor must not be the zero address", func() {
706		TransferAdmin(cross(cur), zero)
707	})
708}
709
710func TestStraySendRejected(cur realm, t *testing.T) {
711	id := planAs(cur, prov1, "stray", 100, 50)
712	subID := subscribeAs(cur, alice, id, 100)
713	arm := func(from address) {
714		testing.SetRealm(testing.NewUserRealm(from))
715		testing.SetOriginCaller(from)
716		testing.SetOriginSend(ugnot(3))
717	}
718	arm(prov1)
719	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
720		RetirePlan(cross(cur), id)
721	})
722	arm(alice)
723	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
724		Cancel(cross(cur), subID)
725	})
726	arm(mallory)
727	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
728		Expire(cross(cur), subID)
729	})
730	arm(prov1)
731	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
732		ClaimAll(cross(cur))
733	})
734	arm(testAdmin)
735	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
736		WithdrawFees(cross(cur))
737	})
738	arm(testAdmin)
739	uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {
740		SetFeeBps(cross(cur), 0)
741	})
742}
743
744func TestRenderSanitizes(cur realm, t *testing.T) {
745	planAs(cur, prov1, "[evil](https://x) <script>alert(1)</script>", 1, MinPeriodBlocks)
746	out := Render("")
747	if strings.Contains(out, "<script>") {
748		t.Fatal("unsanitized title reached Render")
749	}
750	if !strings.Contains(out, "plans:") {
751		t.Fatal("render missing counters")
752	}
753	if Render("nope") == renderHome() {
754		t.Fatal("unknown path fell through to home")
755	}
756}
757
758func TestUnknownIDs(t *testing.T) {
759	defer func() {
760		if recover() == nil {
761			t.Fatal("expected panic")
762		}
763	}()
764	PlanInfo(99999)
765}
766
767// Y1 regression: EntitledFor follows the most recent subscription and
768// honors paid entitlement across cancellation.
769func TestEntitledForSurvivesCancel(cur realm, t *testing.T) {
770	id := planAs(cur, prov1, "y1", 100, 50)
771	subID := subscribeAs(cur, alice, id, 100)
772	testing.SetRealm(testing.NewUserRealm(alice))
773	testing.SetOriginCaller(alice)
774	testing.SetOriginSend(nil)
775	Cancel(cross(cur), subID)
776	if !EntitledFor(id, alice) {
777		t.Fatal("cancelled-but-paid subscriber lost EntitledFor")
778	}
779	// A fresh Subscribe repoints the surface at the new subscription.
780	// Skip a few blocks first so ptNew > ptOld: at a height in
781	// [ptOld, ptNew) a STALE pointer would answer false while the
782	// repointed one answers true — the distinguishing assertion.
783	_, _, _, _, ptOld, _, _ := SubInfo(subID)
784	testing.SkipHeights(5)
785	newID := subscribeAs(cur, alice, id, 100)
786	_, _, _, _, ptNew, _, _ := SubInfo(newID)
787	if ptNew <= ptOld {
788		t.Fatal("fixture broken: ptNew must exceed ptOld")
789	}
790	if !EntitledFor(id, alice) || !Entitled(newID) || !Entitled(subID) {
791		t.Fatal("both paid entitlements should be live right after re-subscribe")
792	}
793	testing.SkipHeights(ptOld - runtime.ChainHeight())
794	if Entitled(subID) {
795		t.Fatal("old span should have run out")
796	}
797	if !EntitledFor(id, alice) {
798		t.Fatal("EntitledFor answered the stale id, not the repointed one")
799	}
800	// After the new paid span runs out too, EntitledFor goes false.
801	testing.SkipHeights(ptNew - runtime.ChainHeight())
802	if EntitledFor(id, alice) {
803		t.Fatal("EntitledFor true past paidThrough")
804	}
805}
806
807// Y2 companion: a lapsed-in-grace incumbent blocks Subscribe with a
808// message that states both options.
809func TestSubscribeMessageInGrace(cur realm, t *testing.T) {
810	id := planAs(cur, prov1, "y2msg", 100, 50)
811	subID := subscribeAs(cur, bob, id, 100)
812	_, _, _, _, pt, _, _ := SubInfo(subID)
813	testing.SkipHeights(pt + 10 - runtime.ChainHeight())
814	testing.SetRealm(testing.NewUserRealm(bob))
815	testing.SetOriginCaller(bob)
816	testing.SetOriginSend(ugnot(100))
817	testing.IssueCoins(Address(), ugnot(100))
818	uassert.AbortsContains(t, cur, "lapsed but renewable", func() {
819		Subscribe(cross(cur), id)
820	})
821	// Cancel + Subscribe is the documented fresh-start path.
822	testing.SetRealm(testing.NewUserRealm(bob))
823	testing.SetOriginCaller(bob)
824	testing.SetOriginSend(nil)
825	Cancel(cross(cur), subID)
826	subscribeAs(cur, bob, id, 100)
827}