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

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}