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}