relay_test.gno
7.95 Kb · 245 lines
1package relay
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/moul/agents/msg/v0"
8 "gno.land/p/moul/x/daily/ringbuffer/v0"
9 "gno.land/p/nt/testutils/v0"
10 "gno.land/p/nt/uassert/v0"
11)
12
13var (
14 alice = testutils.TestAddress("alice")
15 bob = testutils.TestAddress("bob")
16)
17
18// reset puts the realm back to its init() state. Realm globals persist for the
19// whole test binary and examples run after every Test, so every test that
20// writes starts here and ExampleRender builds its own state.
21func reset() {
22 log = ringbuffer.New(Slots)
23 seq = 0
24 seen = 0
25}
26
27func TestPostAssignsSeqAndAuthor(cur realm, t *testing.T) {
28 reset()
29 testing.SetRealm(testing.NewUserRealm(alice))
30 got := Post(cross(cur), "agent-a", "build", "note", "pr/1", "first")
31
32 uassert.Equal(t, uint64(1), got)
33 uassert.Equal(t, 1, Len())
34 uassert.Equal(t, 1, Seen())
35
36 e := Latest(0)[0]
37 uassert.Equal(t, uint64(1), e.Msg.Seq)
38 uassert.Equal(t, "agent-a", e.Msg.From)
39 uassert.Equal(t, alice.String(), e.Author.String())
40}
41
42func TestSeqIsMonotonicAcrossAuthors(cur realm, t *testing.T) {
43 reset()
44 testing.SetRealm(testing.NewUserRealm(alice))
45 Post(cross(cur), "agent-a", "build", "", "", "one")
46 testing.SetRealm(testing.NewUserRealm(bob))
47 Post(cross(cur), "agent-b", "build", "", "", "two")
48
49 all := Latest(0) // newest first
50 uassert.Equal(t, uint64(2), all[0].Msg.Seq)
51 uassert.Equal(t, bob.String(), all[0].Author.String())
52 uassert.Equal(t, uint64(1), all[1].Msg.Seq)
53 uassert.Equal(t, alice.String(), all[1].Author.String())
54}
55
56func TestRingDropsTheOldestAndSeenKeepsCounting(cur realm, t *testing.T) {
57 reset()
58 testing.SetRealm(testing.NewUserRealm(alice))
59 for i := 0; i < Slots+5; i++ {
60 Post(cross(cur), "agent-a", "build", "", "", "body")
61 }
62 uassert.Equal(t, Slots, Len())
63 uassert.Equal(t, Slots+5, Seen())
64
65 // The five oldest fell off the back; the newest is the last one posted.
66 all := Latest(0)
67 uassert.Equal(t, uint64(Slots+5), all[0].Msg.Seq)
68 uassert.Equal(t, uint64(6), all[len(all)-1].Msg.Seq)
69}
70
71func TestSinceAndTopicFilter(cur realm, t *testing.T) {
72 reset()
73 testing.SetRealm(testing.NewUserRealm(alice))
74 Post(cross(cur), "agent-a", "build", "", "", "b1")
75 Post(cross(cur), "agent-a", "deploy", "", "", "d1")
76 Post(cross(cur), "agent-a", "build", "", "", "b2")
77
78 since := Since(1) // oldest first
79 uassert.Equal(t, 2, len(since))
80 uassert.Equal(t, uint64(2), since[0].Msg.Seq)
81 uassert.Equal(t, uint64(3), since[1].Msg.Seq)
82 uassert.Equal(t, 0, len(Since(3)))
83
84 build := Topic("build") // newest first
85 uassert.Equal(t, 2, len(build))
86 uassert.Equal(t, "b2", build[0].Msg.Body)
87 uassert.Equal(t, 0, len(Topic("nothing-here")))
88}
89
90func TestLatestRespectsN(cur realm, t *testing.T) {
91 reset()
92 testing.SetRealm(testing.NewUserRealm(alice))
93 for i := 0; i < 5; i++ {
94 Post(cross(cur), "agent-a", "build", "", "", "body")
95 }
96 uassert.Equal(t, 2, len(Latest(2)))
97 uassert.Equal(t, 5, len(Latest(0)))
98 uassert.Equal(t, 5, len(Latest(-1)))
99}
100
101func TestPostRejectsInvalidFields(cur realm, t *testing.T) {
102 reset()
103 testing.SetRealm(testing.NewUserRealm(alice))
104 uassert.AbortsWithMessage(t, cur, "empty topic", func() {
105 Post(cross(cur), "agent-a", "", "", "", "body")
106 })
107 uassert.AbortsWithMessage(t, cur, "topic must be lowercase a-z, 0-9, dash or dot", func() {
108 Post(cross(cur), "agent-a", "Build", "", "", "body")
109 })
110 uassert.AbortsWithMessage(t, cur, "empty body", func() {
111 Post(cross(cur), "agent-a", "build", "", "", "")
112 })
113 uassert.Equal(t, 0, Len())
114 uassert.Equal(t, 0, Seen())
115}
116
117// TestBodySurvivesTheEncoding is the regression test for the framing: a body
118// that looks like the encoding, or ends in the digits a length prefix is made
119// of, must come back unchanged with its author still attached.
120func TestBodySurvivesTheEncoding(cur realm, t *testing.T) {
121 reset()
122 testing.SetRealm(testing.NewUserRealm(alice))
123 nasty := []string{
124 "ends in 40",
125 "3:abc",
126 "has\na newline",
127 msg.Msg{Seq: 9, Height: 9, From: "x", Topic: "y", Kind: "z", Ref: "w", Body: "v"}.Encode(),
128 "| breaks | a | table |",
129 }
130 for _, body := range nasty {
131 Post(cross(cur), "agent-a", "build", "", "", body)
132 }
133 got := Latest(0) // newest first
134 for i, body := range nasty {
135 e := got[len(nasty)-1-i]
136 uassert.Equal(t, body, e.Msg.Body)
137 uassert.Equal(t, alice.String(), e.Author.String())
138 }
139}
140
141// TestRenderEscapesACallersString pins what guard-untrusted-render exists for:
142// a pipe in a body must not open a column in the rendered table.
143func TestRenderEscapesACallersString(cur realm, t *testing.T) {
144 reset()
145 testing.SetRealm(testing.NewUserRealm(alice))
146 Post(cross(cur), "agent-a", "build", "", "", "a | b")
147
148 out := Render("")
149 uassert.False(t, strings.Contains(out, "| a | b |"),
150 "an unescaped pipe opened a column in the rendered table")
151}
152
153// TestRenderEscapesABodyExactlyOnce is the regression test for the v0 bug.
154// ui.Cell(ui.Excerpt(s)) escaped twice, so a hyphen reached mainnet as
155// backslash-backslash-backslash-hyphen. Over-escaping is not a security hole,
156// which is exactly why the pipe test above passed while this was broken: a
157// doubly escaped pipe is still safe and still wrong.
158func TestRenderEscapesABodyExactlyOnce(cur realm, t *testing.T) {
159 reset()
160 testing.SetRealm(testing.NewUserRealm(alice))
161 Post(cross(cur), "agent-a", "build", "", "", "evicting in-call")
162
163 out := Render("")
164 uassert.True(t, strings.Contains(out, `evicting in\-call`),
165 "a hyphen in a short body should be escaped once")
166 uassert.False(t, strings.Contains(out, `in\\-call`),
167 "the body was escaped more than once")
168}
169
170// TestRenderEscapesALongBodyExactlyOnce covers the other branch of
171// excerptCell, where the body is cut before it is escaped.
172func TestRenderEscapesALongBodyExactlyOnce(cur realm, t *testing.T) {
173 reset()
174 testing.SetRealm(testing.NewUserRealm(alice))
175 long := "evicting in-call"
176 for len(long) < 200 {
177 long += " and-again"
178 }
179 Post(cross(cur), "agent-a", "build", "", "", long)
180
181 out := Render("")
182 uassert.True(t, strings.Contains(out, `evicting in\-call`),
183 "a hyphen in a cut body should be escaped once")
184 uassert.False(t, strings.Contains(out, `in\\-call`),
185 "the cut body was escaped more than once")
186}
187
188// TestPostRecordsNoSessionForAMasterKey pins the empty-session branch. A test
189// signs as a plain user realm, never through a session, so isSession is false
190// and Session must stay empty rather than inherit the author.
191func TestPostRecordsNoSessionForAMasterKey(cur realm, t *testing.T) {
192 reset()
193 testing.SetRealm(testing.NewUserRealm(alice))
194 Post(cross(cur), "agent-a", "build", "", "", "body")
195
196 e := Latest(0)[0]
197 uassert.Equal(t, alice.String(), e.Author.String())
198 uassert.Equal(t, "", e.Session.String())
199}
200
201// TestWireRoundTripsThroughAReader walks the wire format exactly as an
202// off-chain reader must: unframe one entry at a time, then unframe the author
203// and the session off the front, then decode the message. The bodies are the
204// ones that break a qeval parser.
205func TestWireRoundTripsThroughAReader(cur realm, t *testing.T) {
206 reset()
207 testing.SetRealm(testing.NewUserRealm(alice))
208 bodies := []string{
209 `a body with " string),( in it`,
210 "ends in 40",
211 "has\na newline",
212 "| and | pipes |",
213 }
214 for _, b := range bodies {
215 Post(cross(cur), "agent-a", "build", "k", "r", b)
216 }
217
218 rest := Wire(0)
219 for i, want := range bodies {
220 framed, tail, ok := msg.Unframe(rest)
221 uassert.True(t, ok, "entry did not unframe")
222 rest = tail
223
224 author, r2, ok := msg.Unframe(framed)
225 uassert.True(t, ok, "author did not unframe")
226 session, r3, ok := msg.Unframe(r2)
227 uassert.True(t, ok, "session did not unframe")
228 m, ok := msg.Decode(r3)
229 uassert.True(t, ok, "message did not decode")
230
231 uassert.Equal(t, want, m.Body)
232 uassert.Equal(t, uint64(i+1), m.Seq)
233 uassert.Equal(t, alice.String(), author)
234 uassert.Equal(t, "", session)
235 }
236 uassert.Equal(t, "", rest, "the wire had trailing bytes")
237}
238
239func TestWireIsEmptyWhenNothingIsNewer(cur realm, t *testing.T) {
240 reset()
241 testing.SetRealm(testing.NewUserRealm(alice))
242 Post(cross(cur), "agent-a", "build", "", "", "one")
243 uassert.Equal(t, "", Wire(1))
244 uassert.True(t, Wire(0) != "", "Wire(0) should carry the only entry")
245}