1package threads23import (4 "strconv"5 "strings"6 "testing"78 sc "gno.land/p/moul/x/social/coin/v0"9 th "gno.land/p/moul/x/social/threads/v0"10 "gno.land/p/nt/testutils/v0"11 "gno.land/p/nt/uassert/v0"12 "gno.land/p/nt/urequire/v0"13)1415const homePage = "gno.land/r/moul/home"1617var (18 alice = testutils.TestAddress("alice")19 bob = testutils.TestAddress("bob")20 carol = testutils.TestAddress("carol")21)2223// reset clears realm state. Realm globals live for the whole test binary, so24// everything a later test reads, including the ledger, starts here.25//26// The ledger is rebuilt through a same-realm cross because grc20.NewToken27// needs a code-realm frame, which a test's EOA-origin cur is not. It is NOT28// re-registered with grc20reg: the registry is another realm and its entry29// survives this one's state.30func reset(cur realm) {31 board = th.NewBoard()32 func(cur realm) { // the name is forced: a realm argument must be called cur33 points = sc.New("Thread Points", "THREAD", 0, 0, tokenPolicy, 0, cur)34 }(cross(cur))35}3637// earn gives who n THREAD the only way the realm allows: n distinct addresses38// replying to a thread they wrote.39func earn(cur realm, t *testing.T, who address, n int) int64 {40 t.Helper()41 testing.SetRealm(testing.NewUserRealm(who))42 id := Post(cross(cur), homePage, "a thread by "+who.String())43 for i := 0; i < n; i++ {44 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("replier" + strconv.Itoa(i))))45 Reply(cross(cur), id, "hello")46 }47 return id48}4950func TestAnAuthorEarnsPeopleAndNotMessages(cur realm, t *testing.T) {51 reset(cur)5253 testing.SetRealm(testing.NewUserRealm(alice))54 id := Post(cross(cur), homePage, "first thread")55 uassert.Equal(t, int64(0), BalanceOf(alice), "writing earns nothing on its own")5657 testing.SetRealm(testing.NewUserRealm(bob))58 Reply(cross(cur), id, "nice")59 uassert.Equal(t, int64(1), BalanceOf(alice))6061 testing.SetRealm(testing.NewUserRealm(bob))62 Reply(cross(cur), id, "still me")63 uassert.Equal(t, int64(1), BalanceOf(alice), "one person is one point, however much they say")6465 testing.SetRealm(testing.NewUserRealm(alice))66 Reply(cross(cur), id, "thanks")67 uassert.Equal(t, int64(1), BalanceOf(alice), "replying to yourself is not an audience")6869 testing.SetRealm(testing.NewUserRealm(carol))70 Reply(cross(cur), id, "hi")71 uassert.Equal(t, int64(2), BalanceOf(alice))7273 uassert.Equal(t, 2, Repliers(id))74 uassert.Equal(t, int64(2), TotalSupply())75 uassert.Equal(t, int64(0), BalanceOf(bob), "a replier earns nothing")76}7778func TestBoostIsTheOnlyThingThatBurns(cur realm, t *testing.T) {79 reset(cur)80 id := earn(cur, t, alice, 10)81 urequire.Equal(t, int64(10), BalanceOf(alice))8283 testing.SetRealm(testing.NewUserRealm(alice))84 Boost(cross(cur), id)8586 uassert.Equal(t, int64(0), BalanceOf(alice))87 uassert.Equal(t, int64(0), TotalSupply(), "the sink removes supply, it does not move it")88 uassert.Equal(t, int64(123+PinBlocks), PinnedUntil(id), "tests start at height 123")89}9091func TestBoostRefusesWhatTheCallerCannotAfford(cur realm, t *testing.T) {92 reset(cur)93 id := earn(cur, t, alice, 1)9495 testing.SetRealm(testing.NewUserRealm(alice))96 uassert.AbortsContains(t, cur, "balance 1 is short of 10", func() {97 Boost(cross(cur), id)98 })99 uassert.Equal(t, int64(0), PinnedUntil(id), "a refused boost pins nothing")100 uassert.Equal(t, int64(1), BalanceOf(alice))101}102103// A second boost extends from the current pin, not from now, so the thread104// never loses time it was already paid for.105func TestASecondBoostExtendsRatherThanRestarts(cur realm, t *testing.T) {106 reset(cur)107 id := earn(cur, t, alice, 20)108109 testing.SetRealm(testing.NewUserRealm(alice))110 Boost(cross(cur), id)111 first := PinnedUntil(id)112113 testing.SetRealm(testing.NewUserRealm(alice))114 Boost(cross(cur), id)115 uassert.Equal(t, first+PinBlocks, PinnedUntil(id))116 uassert.Equal(t, int64(0), BalanceOf(alice))117}118119// Anyone may boost anyone's thread, and the buyer has to get THREAD from120// somebody who earned it. That is the whole market.121func TestPlacementIsBoughtFromAnEarner(cur realm, t *testing.T) {122 reset(cur)123 id := earn(cur, t, alice, 10)124125 testing.SetRealm(testing.NewUserRealm(alice))126 Transfer(cross(cur), bob, 10)127 uassert.Equal(t, int64(0), BalanceOf(alice))128 uassert.Equal(t, int64(10), BalanceOf(bob))129130 testing.SetRealm(testing.NewUserRealm(bob))131 Boost(cross(cur), id)132 uassert.True(t, PinnedUntil(id) > 0, "bob pinned alice's thread")133 uassert.Equal(t, int64(0), TotalSupply())134}135136func TestPostRefusals(cur realm, t *testing.T) {137 reset(cur)138 testing.SetRealm(testing.NewUserRealm(alice))139 uassert.AbortsContains(t, cur, "not a page key", func() {140 Post(cross(cur), "nopath", "body")141 })142 testing.SetRealm(testing.NewUserRealm(alice))143 uassert.AbortsContains(t, cur, "body is empty", func() {144 Post(cross(cur), homePage, " ")145 })146 testing.SetRealm(testing.NewUserRealm(alice))147 uassert.AbortsContains(t, cur, "no such thread", func() {148 Reply(cross(cur), 99, "into the void")149 })150 uassert.Equal(t, 0, Total())151}152153func TestCountsAreScopedToTheirPage(cur realm, t *testing.T) {154 reset(cur)155 testing.SetRealm(testing.NewUserRealm(alice))156 Post(cross(cur), homePage, "on home")157 testing.SetRealm(testing.NewUserRealm(alice))158 Post(cross(cur), "gno.land/r/moul/blog", "on the blog")159160 uassert.Equal(t, 1, Count(homePage))161 uassert.Equal(t, 0, Count("gno.land/r/nobody/here"))162 uassert.Equal(t, 2, Total())163 uassert.Equal(t, 2, Pages())164}165166// RenderBlockFor is the embed, and it escapes what it shows: a raw pipe in a167// body would otherwise open a column in the host realm's page.168func TestRenderBlockForEscapesTheBody(cur realm, t *testing.T) {169 reset(cur)170 testing.SetRealm(testing.NewUserRealm(alice))171 Post(cross(cur), homePage, "a | pipe")172173 got := RenderBlockFor(homePage)174 uassert.True(t, strings.Contains(got, "func=Post"), "the block offers the transaction: "+got)175 uassert.True(t, strings.Contains(got, `| a \| pipe |`),176 "the body cell escapes the pipe rather than opening a column: "+got)177 uassert.False(t, strings.Contains(got, "| a | pipe"), "no raw pipe survives: "+got)178 uassert.True(t, strings.Contains(RenderBlockFor("gno.land/r/nobody/here"), "No discussion yet."))179}180181// TestRender uses uassert rather than an Example because every view contains182// consecutive blank lines, which gno collapses inside an // Output: block.183func TestRender(cur realm, t *testing.T) {184 reset(cur)185 testing.SetRealm(testing.NewUserRealm(alice))186 id := Post(cross(cur), homePage, "a thread about [links](x)")187 testing.SetRealm(testing.NewUserRealm(bob))188 Reply(cross(cur), id, "a reply with a | pipe")189190 index := Render("")191 uassert.True(t, strings.Contains(index, "| 1 | 1 | 1 |"), "threads, pages, supply: "+index)192 uassert.True(t, strings.Contains(index, tokenPolicy.Sink), "the index declares the sink")193 uassert.True(t, strings.Contains(index, `import "`+realmPath+`"`), "the index is the embed doc")194195 page := Render("page/" + homePage)196 uassert.True(t, strings.Contains(page, "/r/moul/home"), "it links the page itself: "+page)197 uassert.True(t, strings.Contains(page, "func=Post"))198199 sid := strconv.FormatInt(id, 10)200 thread := Render("thread/" + sid)201 uassert.True(t, strings.Contains(thread, "# Thread #"+sid))202 uassert.True(t, strings.Contains(thread, "a thread about \\[links\\]\\(x\\)"),203 "the body is escaped where it is shown: "+thread)204 uassert.True(t, strings.Contains(thread, "a reply with a \\| pipe"),205 "a reply cell cannot open a column: "+thread)206 uassert.True(t, strings.Contains(thread, "func=Boost"))207208 uassert.Equal(t, "# Threads\nNo thread #4242", Render("thread/4242"))209 uassert.Equal(t, "# Threads\nNot a thread id: nope", Render("thread/nope"))210 uassert.Equal(t, "# Threads\nNot a page key: nope", Render("page/nope"))211 uassert.True(t, strings.HasPrefix(Render("elsewhere"), "# Not found"))212}213214func TestTokenIsDiscoverableWithoutImportingThisRealm(t *testing.T) {215 uassert.True(t, strings.HasSuffix(TokenKey(), ".THREAD"), TokenKey())216 uassert.True(t, strings.Contains(TokenKey(), realmPath), TokenKey())217}218Allowance(owner string, spender string) int64
Approve(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spender string, amount int64)
BalanceOf(owner string) int64
Boost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, threadID int64)
Count(page string) int
Pages() int
PinnedUntil(threadID int64) int64
Post(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, page string, body string) int64
Render(path string) string
RenderBlock() string
RenderBlockFor(page string) string
Repliers(threadID int64) int
Reply(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, threadID int64, body string)
TokenKey() string
Total() int
TotalSupply() int64
Transfer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount int64)
TransferFrom(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, from string, to string, amount int64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.