Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}