denied_reason_test.gno
4.51 Kb · 95 lines
1package impl
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/uassert/v0"
8 "gno.land/r/gov/dao/memberstore/v0"
9)
10
11// DeniedReason is "execution failed: " + err.Error() (govdao.gno), and that
12// error comes from the proposal's executor callback — which any third-party
13// realm can supply (see govdao_execute_reject_proposal.txtar, where the
14// executor lives in gno.land/r/test/request). Rendered raw it let a failing
15// proposal write markdown straight beneath the "PROPOSAL HAS BEEN DENIED"
16// line, including a forged heading and vote tally.
17//
18// Operates on proposalStatus.String directly rather than through a real
19// proposal, so it does not disturb the proposal ids and counts that the other
20// tests in this package assert against shared realm state.
21func TestDeniedReasonCannotForgePageStructure(cur realm, t *testing.T) {
22 ps := newProposalStatus([]string{memberstore.T1})
23 ps.Denied = true
24 ps.DeniedReason = "execution failed: boom\n\n### Stats\n\n- **PROPOSAL HAS BEEN ACCEPTED**\n\n---\n<div>swallow the tally"
25
26 out := ps.String(0, cur)
27
28 uassert.False(t, strings.Contains(out, "\n### Stats"),
29 "an injected heading must not survive into the rendered stats block")
30 uassert.False(t, strings.Contains(out, "\n- **PROPOSAL HAS BEEN ACCEPTED**"),
31 "an injected list item must not forge an acceptance line")
32 uassert.False(t, strings.Contains(out, "\n---\n"),
33 "an injected horizontal rule must not survive")
34
35 // Emphasis and raw HTML are residue that sanitize.Block would have left
36 // live; InlineText escapes both. A reader must not see a bold
37 // "ACCEPTED" on a denied proposal, and a <div> must not open an HTML
38 // block that swallows the vote tally rendered after it.
39 uassert.False(t, strings.Contains(out, "**PROPOSAL HAS BEEN ACCEPTED**"),
40 "injected bold text must not forge an acceptance line")
41 uassert.False(t, strings.Contains(out, "<div>"),
42 "injected raw HTML must not open a block that swallows the tally")
43
44 // Folding is the property InlineText adds over the alternatives: the whole
45 // reason must land on the REASON: line, so nothing after a newline can be
46 // read as new top-level markdown. Asserted by requiring the payload's LAST
47 // segment on the same line as its first.
48 reason := out[strings.Index(out, "REASON: "):]
49 firstLine := reason[:strings.Index(reason, "\n")]
50 uassert.True(t, strings.Contains(firstLine, "swallow the tally"),
51 "the entire reason must be folded onto the REASON line")
52}
53
54// ...and ordinary error text stays readable. InlineText backslash-escapes
55// markdown punctuation, so the raw output carries "Boom\!" — but a
56// backslash-escaped punctuation mark renders as the bare character, so a
57// reader sees "Boom!". Words and spacing are untouched.
58func TestDeniedReasonKeepsPlainTextIntact(cur realm, t *testing.T) {
59 ps := newProposalStatus([]string{memberstore.T1})
60 ps.Denied = true
61 ps.DeniedReason = "execution failed: Boom!"
62
63 uassert.True(t, strings.Contains(ps.String(0, cur), `REASON: execution failed: Boom\!`),
64 "a plain denial reason must stay on the REASON line, escaped but readable")
65}
66
67// The reason reaches an unauthenticated render path and InlineText costs
68// ~6,990 gas/byte, so it must be bounded before it is escaped, not after.
69// Asserts the clamp at the call site, not clampField in isolation.
70//
71// The payload is exclamation marks, not letters, and that choice is the whole
72// test. InlineText escapes "!" to "\!" but leaves letters alone, so with a
73// letter payload the escaped text is the same length as the raw text and both
74// orderings produce identical output — the test would pass either way and
75// prove nothing. With punctuation, escaping doubles the length, and the two
76// orderings become tellable apart.
77func TestDeniedReasonIsClampedBeforeSanitizing(cur realm, t *testing.T) {
78 ps := newProposalStatus([]string{memberstore.T1})
79 ps.Denied = true
80 ps.DeniedReason = strings.Repeat("!", 50000)
81
82 out := ps.String(0, cur)
83
84 // The clamp bounds what goes INTO the escaper, so a full maxRenderedReason
85 // characters survive and the escaped output is about twice that. Clamping
86 // the escaped text instead would leave only half as many, and the escaper
87 // would still have processed all 50,000 characters — exactly the cost the
88 // clamp exists to avoid.
89 uassert.True(t, strings.Count(out, "!") >= maxRenderedReason,
90 "the clamp must bound the escaper's input, not its output")
91 uassert.True(t, len(out) < 4*maxRenderedReason,
92 "the rendered stats block must still be bounded by the clamp")
93 uassert.True(t, strings.Contains(out, "… truncated"),
94 "a clamped reason must be marked as cut")
95}