test(17-06): framework and admin SPA unit tests for the blog companions
- signpost: activation refusals, host overrides, D-02 pattern layers incl. PatternFrom, ReferenceOptions depths - seaglass: removal report, bluemonday bypass corpus, Check - pact Option.Depth omitempty and the menu item type contracts; party activates menu item types after Register and before the first Boot - cabana: repeater compile and decode, dependsOn compile and recheck, richeditor keys and notices, reorder and singleton boot checks (Docker-free); repeater save, dependent recheck, richeditor save, reorder and singleton routes on Postgres; conformance cases for the documented 404, 409 and 422 answers of the new routes - admin: richeditor value contract, link popover, notices and chunk failure; repeater bounds, remove and reorder, collapsed rows; dependent dropdown states and last request wins; reorder announcements, partition switch and tree rules; SingletonForm spec - gate: required names in the go, admin and postgres stages
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package seaglass_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/seaglass"
|
||||
@@ -48,3 +50,161 @@ func TestSanitizeByteStable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSanitizeReport pins the removal report: element, attribute and comment
|
||||
// names and counts, the ten-name cap with its exact counts, a repeated
|
||||
// attribute, a style that lost a property, and the sanitized output itself.
|
||||
func TestSanitizeReport(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in, out string
|
||||
elements []string
|
||||
elemCount int
|
||||
attributes []string
|
||||
attrCount int
|
||||
comments int
|
||||
}{
|
||||
{
|
||||
name: "element, attribute and comments",
|
||||
in: `<p onclick="a()" class="x">Hi</p><script>x</script><!-- c --><!-- d -->`,
|
||||
out: `<p class="x">Hi</p>`, elements: []string{"script"}, elemCount: 1, attributes: []string{"onclick"}, attrCount: 1, comments: 2,
|
||||
},
|
||||
{
|
||||
name: "names are distinct in first-seen order, counts are exact",
|
||||
in: `<p onclick="a()">1</p><p onclick="b()" onmouseover="c()">2</p><script>1</script><script>2</script>`,
|
||||
out: `<p>1</p><p>2</p>`, elements: []string{"script"}, elemCount: 2, attributes: []string{"onclick", "onmouseover"}, attrCount: 3,
|
||||
},
|
||||
{
|
||||
name: "ten names then an ellipsis, the count stays exact",
|
||||
in: `<iframe></iframe><object></object><embed><form></form><input><base><meta><link><svg></svg><math></math><button></button><select></select>`,
|
||||
out: "", elements: []string{"iframe", "object", "embed", "form", "input", "base", "meta", "link", "svg", "math", "…"}, elemCount: 12,
|
||||
},
|
||||
{
|
||||
name: "a repeated attribute counts as removed",
|
||||
in: `<a href="/a" href="/b">x</a>`, out: `<a href="/a">x</a>`, attributes: []string{"href"}, attrCount: 1,
|
||||
},
|
||||
{
|
||||
name: "a style that lost a property counts under style",
|
||||
in: `<p style="color: red; position: fixed">x</p>`, out: `<p style="color: red">x</p>`, attributes: []string{"style"}, attrCount: 1,
|
||||
},
|
||||
{
|
||||
name: "target and rel are removed",
|
||||
in: `<a href="https://ok" target="_blank" rel="opener">x</a>`, out: `<a href="https://ok">x</a>`, attributes: []string{"target", "rel"}, attrCount: 2,
|
||||
},
|
||||
{
|
||||
name: "a link without an allowed href loses its tag, not its text",
|
||||
in: `<p><a href="javascript:alert(1)">x</a> y</p>`, out: `<p>x y</p>`, elements: []string{"a"}, elemCount: 1,
|
||||
},
|
||||
{
|
||||
name: "only comments",
|
||||
in: `<p>a</p><!-- one -->`, out: `<p>a</p>`, comments: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, r := seaglass.SanitizeReport(tc.in)
|
||||
if got != tc.out {
|
||||
t.Fatalf("output = %q, want %q", got, tc.out)
|
||||
}
|
||||
if !r.Changed {
|
||||
t.Fatal("Changed = false")
|
||||
}
|
||||
if !slices.Equal(r.Elements, tc.elements) || r.ElementCount != tc.elemCount {
|
||||
t.Fatalf("elements = %v (%d), want %v (%d)", r.Elements, r.ElementCount, tc.elements, tc.elemCount)
|
||||
}
|
||||
if !slices.Equal(r.Attributes, tc.attributes) || r.AttributeCount != tc.attrCount {
|
||||
t.Fatalf("attributes = %v (%d), want %v (%d)", r.Attributes, r.AttributeCount, tc.attributes, tc.attrCount)
|
||||
}
|
||||
if r.Comments != tc.comments {
|
||||
t.Fatalf("comments = %d, want %d", r.Comments, tc.comments)
|
||||
}
|
||||
if seaglass.Sanitize(tc.in) != tc.out {
|
||||
t.Fatal("Sanitize differs from SanitizeReport")
|
||||
}
|
||||
// The sanitized output is itself clean and stable.
|
||||
again, r2 := seaglass.SanitizeReport(got)
|
||||
if again != got || r2.Changed {
|
||||
t.Fatalf("sanitizing the output changed it: %q %+v", again, r2)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSanitizeBypassCorpus runs the payloads the bluemonday decision notes
|
||||
// list as bypasses of the regex gate it replaced (no space before an event
|
||||
// handler, entity-encoded and whitespace-split javascript schemes, svg
|
||||
// onload, MathML xlink:href, base, meta refresh, form, data and vbscript
|
||||
// URLs, a repeated href) plus the ordinary unsafe elements: each comes back
|
||||
// without the dangerous construct, with Changed set, and Check refuses it.
|
||||
func TestSanitizeBypassCorpus(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
payload string
|
||||
banned []string
|
||||
}{
|
||||
{`<img src="x"onerror="alert(1)">`, []string{"onerror", "alert"}},
|
||||
{`<img src=x onerror=alert(1)>`, []string{"onerror", "alert"}},
|
||||
{`<p onmouseover=alert(1)>x</p>`, []string{"onmouseover", "alert"}},
|
||||
{`<svg/onload=alert(1)>`, []string{"<svg", "onload", "alert"}},
|
||||
{`<a href="javascript:alert(1)">x</a>`, []string{"href", "javascript", "alert"}},
|
||||
{`<a href="javascript:alert(1)">x</a>`, []string{"href", "javascript", "alert"}},
|
||||
{`<a href="javascript:alert(1)">x</a>`, []string{"href", "avascript", "alert"}},
|
||||
{"<a href=\"java\tscript:alert(1)\">x</a>", []string{"href", "script", "alert"}},
|
||||
{`<a HREF="JaVaScRiPt:alert(1)">x</a>`, []string{"href", "javascript", "alert"}},
|
||||
{`<math><mi xlink:href="javascript:alert(1)">x</mi></math>`, []string{"<math", "<mi", "xlink", "javascript"}},
|
||||
{`<base href="https://evil.example/">`, []string{"<base", "evil.example"}},
|
||||
{`<meta http-equiv="refresh" content="0;url=https://evil.example">`, []string{"<meta", "refresh", "evil.example"}},
|
||||
{`<form action="https://evil.example"><input name="q"></form>`, []string{"<form", "<input", "evil.example"}},
|
||||
{`<a href="data:text/html;base64,PHNjcmlwdD4=">x</a>`, []string{"href", "data:"}},
|
||||
{`<img src="data:image/svg+xml;base64,PHN2Zz4=">`, []string{"<img", "data:"}},
|
||||
{`<a href="vbscript:msgbox(1)">x</a>`, []string{"href", "vbscript"}},
|
||||
{`<a href="https://a" href="javascript:alert(1)">x</a>`, []string{"javascript", "alert"}},
|
||||
{`<div style="background:url(javascript:alert(1))">x</div>`, []string{"style", "javascript"}},
|
||||
{`<iframe src="https://evil.example"></iframe>`, []string{"<iframe", "evil.example"}},
|
||||
{`<p>x</p><script>alert(1)</script>`, []string{"<script", "alert"}},
|
||||
{`<p>x</p><style>p{display:none}</style>`, []string{"<style", "display"}},
|
||||
{`<object data="x.swf"></object><embed src="x.swf">`, []string{"<object", "<embed", "swf"}},
|
||||
} {
|
||||
t.Run(tc.payload, func(t *testing.T) {
|
||||
got, r := seaglass.SanitizeReport(tc.payload)
|
||||
lower := strings.ToLower(got)
|
||||
for _, bad := range tc.banned {
|
||||
if strings.Contains(lower, strings.ToLower(bad)) {
|
||||
t.Fatalf("%q survived: %q", bad, got)
|
||||
}
|
||||
}
|
||||
if !r.Changed {
|
||||
t.Fatalf("Changed = false for %q -> %q", tc.payload, got)
|
||||
}
|
||||
if err := seaglass.Check(tc.payload); err == nil {
|
||||
t.Fatal("Check accepted the payload")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheck pins Check: nil when the policy keeps the HTML whole (including
|
||||
// text that only looks like a scheme, the WR-01 false refusals), an error
|
||||
// prefixed "seaglass: " when it would change anything.
|
||||
func TestCheck(t *testing.T) {
|
||||
for _, html := range []string{
|
||||
"",
|
||||
"<p>Data: 12 maja 2024, metadata: none</p>",
|
||||
"<p>javascript: is just a word here</p>",
|
||||
`<p>It's "fine"</p>`,
|
||||
`<a href="tel:+48123">call</a>`,
|
||||
} {
|
||||
if err := seaglass.Check(html); err != nil {
|
||||
t.Errorf("Check(%q) = %v", html, err)
|
||||
}
|
||||
}
|
||||
for _, html := range []string{
|
||||
"<!-- a comment -->",
|
||||
`<a href="https://a" href="https://b">dup</a>`,
|
||||
`<p style="position: fixed">x</p>`,
|
||||
`<a href="ftp://example.com/x">ftp</a>`,
|
||||
} {
|
||||
err := seaglass.Check(html)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "seaglass: ") {
|
||||
t.Errorf("Check(%q) = %v, want a seaglass error", html, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user