1package threads23import (4 "strings"5 "testing"67 "gno.land/p/nt/testutils/v0"8 "gno.land/p/nt/uassert/v0"9 "gno.land/p/nt/urequire/v0"10)1112var (13 alice = testutils.TestAddress("alice")14 bob = testutils.TestAddress("bob")15 carol = testutils.TestAddress("carol")1617 home = "gno.land/r/moul/home"18)1920func TestValidPage(t *testing.T) {21 tests := []struct {22 page string23 want bool24 }{25 {"gno.land/r/moul/home", true},26 {"gno.land/r/gov/dao:proposal/42", true},27 {"a/b", true},28 {"", false},29 {"nopath", false},30 {"/leading", false},31 {"trailing/", false},32 {"double//slash", false},33 {"gno.land/r/Moul/home", false},34 {"gno.land/r/moul/home?x=1", false},35 {"gno.land/r/moul/" + strings.Repeat("x", MaxPageLen), false},36 }37 for _, tt := range tests {38 uassert.Equal(t, tt.want, ValidPage(tt.page), tt.page)39 }40}4142func TestValidBody(t *testing.T) {43 tests := []struct {44 name string45 body string46 want bool47 }{48 {"plain", "hello", true},49 {"newlines and tabs are content", "a\nb\tc", true},50 {"markdown is allowed and escaped later", "[x](y) | z", true},51 {"empty", "", false},52 {"blank", " \n ", false},53 {"too long", strings.Repeat("x", MaxBodyLen+1), false},54 {"control character", "a\x01b", false},55 {"delete character", "a\x7fb", false},56 }57 for _, tt := range tests {58 uassert.Equal(t, tt.want, ValidBody(tt.body), tt.name)59 }60}6162func TestPostRejectsWhatItCannotStore(t *testing.T) {63 b := NewBoard()6465 _, err := b.Post("nopath", alice, "hi", 100)66 uassert.ErrorIs(t, err, ErrBadPage)6768 _, err = b.Post(home, alice, "", 100)69 uassert.ErrorIs(t, err, ErrBadBody)7071 uassert.Equal(t, 0, b.Len())72 uassert.Equal(t, 0, b.Pages())73}7475func TestReplyCountsPeopleAndNotMessages(t *testing.T) {76 b := NewBoard()77 id, err := b.Post(home, alice, "root", 100)78 urequire.NoError(t, err)7980 // A new address is the mint signal, exactly once.81 isNew, err := b.Reply(id, bob, "first", 101)82 urequire.NoError(t, err)83 uassert.True(t, isNew, "bob is a new replier")8485 isNew, err = b.Reply(id, bob, "again", 102)86 urequire.NoError(t, err)87 uassert.False(t, isNew, "bob replying twice is not two people")8889 // The author replying to their own thread never counts.90 isNew, err = b.Reply(id, alice, "mine", 103)91 urequire.NoError(t, err)92 uassert.False(t, isNew, "the author is not an audience")9394 isNew, err = b.Reply(id, carol, "hello", 104)95 urequire.NoError(t, err)96 uassert.True(t, isNew, "carol is a new replier")9798 th, ok := b.Get(id)99 urequire.True(t, ok, "thread exists")100 uassert.Equal(t, 4, len(th.Replies))101 uassert.Equal(t, 2, th.Repliers())102 uassert.True(t, th.HasReplied(bob))103 uassert.False(t, th.HasReplied(alice), "the author is never in the replier set")104}105106func TestReplyToNothing(t *testing.T) {107 b := NewBoard()108 _, err := b.Reply(42, bob, "hi", 100)109 uassert.ErrorIs(t, err, ErrNoThread)110}111112func TestPinOnlyEverMovesForward(t *testing.T) {113 b := NewBoard()114 id, _ := b.Post(home, alice, "root", 100)115116 urequire.NoError(t, b.Pin(id, 1000))117 th, _ := b.Get(id)118 uassert.True(t, th.Pinned(999))119 uassert.False(t, th.Pinned(1000), "the pin is exclusive at its own height")120121 // A cheaper pin cannot cut an expensive one short.122 uassert.ErrorIs(t, b.Pin(id, 500), ErrPinIsPast)123 uassert.ErrorIs(t, b.Pin(id, 1000), ErrPinIsPast)124 uassert.Equal(t, int64(1000), th.PinnedUntil)125126 urequire.NoError(t, b.Pin(id, 1200))127 uassert.Equal(t, int64(1200), th.PinnedUntil)128}129130func TestListIsPinnedThenNewest(t *testing.T) {131 b := NewBoard()132 first, _ := b.Post(home, alice, "first", 100)133 second, _ := b.Post(home, bob, "second", 101)134 third, _ := b.Post(home, carol, "third", 102)135136 // Newest first, nothing pinned.137 got := b.List(home, 200, 0)138 urequire.Equal(t, 3, len(got))139 uassert.Equal(t, uint64(third), uint64(got[0].ID))140 uassert.Equal(t, uint64(second), uint64(got[1].ID))141 uassert.Equal(t, uint64(first), uint64(got[2].ID))142143 // Pinning the oldest moves it to the top and leaves the rest in order.144 urequire.NoError(t, b.Pin(first, 500))145 got = b.List(home, 200, 0)146 uassert.Equal(t, uint64(first), uint64(got[0].ID))147 uassert.Equal(t, uint64(third), uint64(got[1].ID))148 uassert.Equal(t, uint64(second), uint64(got[2].ID))149150 // The pin expires on its own, with nothing to sweep.151 got = b.List(home, 500, 0)152 uassert.Equal(t, uint64(third), uint64(got[0].ID))153154 // limit truncates after the partition, so a pin is never cut off.155 got = b.List(home, 200, 1)156 urequire.Equal(t, 1, len(got))157 uassert.Equal(t, uint64(first), uint64(got[0].ID))158}159160func TestPagesAreIndependent(t *testing.T) {161 b := NewBoard()162 b.Post(home, alice, "on home", 100)163 b.Post("gno.land/r/moul/blog", bob, "on the blog", 101)164165 uassert.Equal(t, 2, b.Len())166 uassert.Equal(t, 2, b.Pages())167 uassert.Equal(t, 1, b.PageLen(home))168 uassert.Equal(t, 0, b.PageLen("gno.land/r/nobody/here"))169 uassert.Equal(t, 0, len(b.List("gno.land/r/nobody/here", 100, 0)))170}171172func TestRecentCrossesPages(t *testing.T) {173 b := NewBoard()174 b.Post(home, alice, "one", 100)175 b.Post("gno.land/r/moul/blog", bob, "two", 101)176 third, _ := b.Post(home, carol, "three", 102)177178 got := b.Recent(2)179 urequire.Equal(t, 2, len(got))180 uassert.Equal(t, uint64(third), uint64(got[0].ID), "newest first")181}182183func TestBlockEscapesTheBodyItShows(t *testing.T) {184 b := NewBoard()185 id, _ := b.Post(home, alice, "a | pipe and [a link](x)", 100)186187 out := Block("gno.land/r/moul/x/social/threads/v0", home, b.List(home, 200, 10), 1, 200)188189 // The assertion has to be on what IS there, not on what is not: an190 // escaped cell contains the backslash form, and a row that silently lost191 // the body entirely would satisfy any "does not contain" check.192 uassert.True(t, strings.Contains(out, `a \| pipe and \[a link\]\(x\)`),193 "the body is cut and escaped once, pipe included: "+out)194 uassert.False(t, strings.Contains(out, "| a | pipe"), "a raw pipe would open a column: "+out)195 uassert.True(t, strings.Contains(out, ThreadURL("gno.land/r/moul/x/social/threads/v0", id)))196 uassert.True(t, strings.Contains(out, "func=Post"), "the block offers the transaction")197}198199func TestBlockOnAnEmptyPageSaysSo(t *testing.T) {200 out := Block("gno.land/r/moul/x/social/threads/v0", home, nil, 0, 200)201 uassert.True(t, strings.Contains(out, "No discussion yet."), out)202}203204func TestURLsStripTheChainDomain(t *testing.T) {205 uassert.Equal(t, "/r/moul/x/social/threads/v0", RealmURL("gno.land/r/moul/x/social/threads/v0"))206 uassert.Equal(t, "/r/a/b:thread/7", ThreadURL("gno.land/r/a/b", 7))207 uassert.Equal(t, "/r/a/b:page/gno.land/r/moul/home", PageURL("gno.land/r/a/b", home))208}209Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.