Files
sandwich-hime/examples/eql-shaped/views/render_test.go
T
2026-08-11 20:15:06 -04:00

132 lines
3.8 KiB
Go

// SPDX-License-Identifier: 0BSD
package views
import (
"bytes"
"context"
"errors"
"html/template"
"io"
"strings"
"testing"
"gamertan.com/sandwich-hime/sando"
)
func TestBrowseResultsEscapesUntrustedValues(t *testing.T) {
t.Parallel()
component := BrowseResults(BrowseView{
Query: `"><script>alert(1)</script>`,
Records: []RecordView{{
URL: `/item?q=" onclick="alert(1)`,
Title: `<img src=x onerror=alert(1)>`,
Kind: `" aria-label="injected`,
}},
})
var output bytes.Buffer
if err := sando.Render(context.Background(), &output, component); err != nil {
t.Fatal(err)
}
got := output.String()
if strings.Contains(got, "<script>") || strings.Contains(got, "<img") || strings.Contains(got, `" onclick=`) {
t.Fatalf("untrusted markup survived escaping: %s", got)
}
}
func TestBrowseResultsRejectsDangerousURL(t *testing.T) {
t.Parallel()
component := BrowseResults(BrowseView{Records: []RecordView{{
URL: " JaVaScRiPt:\talert(1)",
Title: "unsafe",
}}})
var output bytes.Buffer
err := sando.Render(context.Background(), &output, component)
if !errors.Is(err, sando.ErrUnsafeURL) {
t.Fatalf("expected unsafe URL error, got %v", err)
}
}
func TestGeneratedComponentPropagatesWriterFailures(t *testing.T) {
t.Parallel()
component := Badge("proof")
if err := sando.Render(context.Background(), shortWriter{}, component); !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("short writer error = %v, want io.ErrShortWrite", err)
}
want := errors.New("connection closed")
if err := sando.Render(context.Background(), errorWriter{err: want}, component); !errors.Is(err, want) {
t.Fatalf("writer error = %v, want %v", err, want)
}
}
func TestContextEscapingAgainstHTMLTemplateBaseline(t *testing.T) {
t.Parallel()
baseline := template.Must(template.New("probe").Parse(`<a href="{{ .URL }}" title="{{ .Title }}">{{ .Text }}</a>`))
tests := []struct {
name string
url string
title string
text string
}{
{name: "entities", url: "/items?q=one&kind=book", title: `say "hello" & goodbye`, text: `<strong>not markup</strong>`},
{name: "unicode", url: "/items/cole", title: "Café & 雪", text: "Cole's archive ♥"},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
data := struct{ URL, Title, Text string }{test.url, test.title, test.text}
var legacy bytes.Buffer
if err := baseline.Execute(&legacy, data); err != nil {
t.Fatal(err)
}
var generated bytes.Buffer
if err := sando.Render(context.Background(), &generated, SecurityProbe(test.url, test.title, test.text)); err != nil {
t.Fatal(err)
}
if got, want := strings.TrimSpace(generated.String()), legacy.String(); got != want {
t.Fatalf("generated escaping differs from html/template baseline\n got: %s\nwant: %s", got, want)
}
})
}
}
func TestDangerousURLIsBlockedAtLeastAsStrictlyAsHTMLTemplate(t *testing.T) {
t.Parallel()
const dangerous = " JaVaScRiPt:\talert(1)"
baseline := template.Must(template.New("probe").Parse(`<a href="{{ . }}">link</a>`))
var legacy bytes.Buffer
if err := baseline.Execute(&legacy, dangerous); err != nil {
t.Fatal(err)
}
if !strings.Contains(legacy.String(), "#ZgotmplZ") {
t.Fatalf("html/template baseline unexpectedly accepted dangerous URL: %s", legacy.String())
}
var generated bytes.Buffer
err := sando.Render(context.Background(), &generated, SecurityProbe(dangerous, "title", "link"))
if !errors.Is(err, sando.ErrUnsafeURL) {
t.Fatalf("generated renderer error = %v, want ErrUnsafeURL", err)
}
}
type shortWriter struct{}
func (shortWriter) Write(value []byte) (int, error) {
if len(value) == 0 {
return 0, nil
}
return len(value) - 1, nil
}
type errorWriter struct{ err error }
func (writer errorWriter) Write([]byte) (int, error) { return 0, writer.err }