Files
Gitea/modules/templates/helper_test.go
T
silverwind 8abda62312 test: consolidate Go tests (#39962)
Consolidate all Go tests. Coverage is identical to before. Go tests are
around 5% faster while being 7% smaller. No non-test code is touched.
2026-10-24 13:48:19 +00:00

167 lines
6.9 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package templates
import (
"html/template"
"net/url"
"strings"
"testing"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
)
func TestSubjectBodySeparator(t *testing.T) {
for _, testCase := range []struct {
input, subject, body string
}{
{"Simple\n---------------\nCase", "Simple\n", "\nCase"},
{"Only\nBody", "", "Only\nBody"},
{"Minimal\n---\nseparator", "Minimal\n", "\nseparator"},
{"False --- separator", "", "False --- separator"},
{"False\n--- separator", "", "False\n--- separator"},
{"False ---\nseparator", "", "False ---\nseparator"},
{"With extra spaces\n----- \t \nBody", "With extra spaces\n", "\nBody"},
{"With leading spaces\n -------\nOnly body", "", "With leading spaces\n -------\nOnly body"},
{"Multiple\n---\n-------\n---\nSeparators", "Multiple\n", "\n-------\n---\nSeparators"},
{"Insufficient\n--\nSeparators", "", "Insufficient\n--\nSeparators"},
} {
loc := mailSubjectSplit.FindStringIndex(testCase.input)
if loc == nil {
assert.Empty(t, testCase.subject, "no subject found, but one expected")
assert.Equal(t, testCase.body, testCase.input)
} else {
assert.Equal(t, testCase.subject, testCase.input[0:loc[0]])
assert.Equal(t, testCase.body, testCase.input[loc[1]:])
}
}
}
func TestSanitizeHTML(t *testing.T) {
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), sanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
}
func TestTemplateIif(t *testing.T) {
tmpl := template.New("test")
tmpl.Funcs(template.FuncMap{"Iif": iif})
template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
cases := []any{nil, false, true, "", "string", 0, 1}
w := &strings.Builder{}
truthyCount := 0
for i, v := range cases {
w.Reset()
assert.NoError(t, tmpl.Execute(w, struct{ Value any }{v}), "case %d (%T) %#v fails", i, v, v)
out := w.String()
truthyCount += util.Iif(out == "true:true", 1, 0)
truthyMatches := out == "true:true" || out == "false:false"
assert.True(t, truthyMatches, "case %d (%T) %#v fail: %s", i, v, v, out)
}
assert.True(t, truthyCount != 0 && truthyCount != len(cases))
}
func TestTemplateEscape(t *testing.T) {
execTmpl := func(code string) string {
tmpl := template.New("test")
tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlFormat})
template.Must(tmpl.Parse(code))
w := &strings.Builder{}
assert.NoError(t, tmpl.Execute(w, nil))
return w.String()
}
t.Run("Golang URL Escape", func(t *testing.T) {
// HINT: GOLANG-HTML-TEMPLATE-URL-ESCAPING: demo cases (html/template/attr.go):
// Golang template considers "href", "data-href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping
actual := execTmpl(`<a href="?a={{"%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
actual = execTmpl(`<a data-href="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-href="?a=%25"></a>`, actual)
actual = execTmpl(`<a data-xxx-url="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-xxx-url="?a=%25"></a>`, actual)
})
t.Run("Golang URL No-escape", func(t *testing.T) {
// non-URL content isn't auto-escaped
actual := execTmpl(`<a data-link="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-link="?a=%"></a>`, actual)
// the attr names like "data-href" and "data-action" are treated as URL (as the "data-" prefix is stripped)
// but "data-xxx-href" and "data-xxx-action" are not, so no escaping.
actual = execTmpl(`<a data-xxx-href="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-xxx-href="?a=%"></a>`, actual)
})
t.Run("QueryBuild", func(t *testing.T) {
actual := execTmpl(`<a href="{{QueryBuild "?" "a" "%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
actual = execTmpl(`<a href="?{{QueryBuild "a" "%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
})
t.Run("HTMLFormat", func(t *testing.T) {
actual := execTmpl("{{HTMLFormat `<a k=\"%s\">%s</a>` `\"` `<>`}}")
assert.Equal(t, `<a k="&#34;">&lt;&gt;</a>`, actual)
})
}
func TestQueryBuild(t *testing.T) {
t.Run("construct", func(t *testing.T) {
assert.Empty(t, string(QueryBuild()))
assert.Empty(t, string(QueryBuild("a", nil, "b", false, "c", 0, "d", "")))
assert.Equal(t, "a=1&b=true", string(QueryBuild("a", 1, "b", "true")))
// path with query parameters
assert.Equal(t, "/?k=1", string(QueryBuild("/", "k", 1)))
assert.Equal(t, "/", string(QueryBuild("/?k=a", "k", 0)))
// no path but question mark with query parameters
assert.Equal(t, "?k=1", string(QueryBuild("?", "k", 1)))
assert.Equal(t, "?", string(QueryBuild("?", "k", 0)))
assert.Equal(t, "path?k=1", string(QueryBuild("path?", "k", 1)))
assert.Equal(t, "path", string(QueryBuild("path?", "k", 0)))
// only query parameters
assert.Equal(t, "&k=1", string(QueryBuild("&", "k", 1)))
assert.Empty(t, string(QueryBuild("&", "k", 0)))
assert.Empty(t, string(QueryBuild("&k=a", "k", 0)))
assert.Empty(t, string(QueryBuild("k=a&", "k", 0)))
assert.Equal(t, "a=1&b=2", string(QueryBuild("a=1", "b", 2)))
assert.Equal(t, "&a=1&b=2", string(QueryBuild("&a=1", "b", 2)))
assert.Equal(t, "a=1&b=2&", string(QueryBuild("a=1&", "b", 2)))
})
for _, prefix := range []string{"", "&"} {
t.Run("replace"+prefix, func(t *testing.T) {
assert.Equal(t, prefix+"a=1&c=d&e=f", string(QueryBuild(prefix+"a=b&c=d&e=f", "a", 1)))
assert.Equal(t, prefix+"a=b&c=1&e=f", string(QueryBuild(prefix+"a=b&c=d&e=f", "c", 1)))
assert.Equal(t, prefix+"a=b&c=d&e=1", string(QueryBuild(prefix+"a=b&c=d&e=f", "e", 1)))
assert.Equal(t, prefix+"a=b&c=d&e=f&k=1", string(QueryBuild(prefix+"a=b&c=d&e=f", "k", 1)))
})
t.Run("delete"+prefix, func(t *testing.T) {
assert.Equal(t, prefix+"c=d&e=f", string(QueryBuild(prefix+"a=b&c=d&e=f", "a", "")))
assert.Equal(t, prefix+"a=b&e=f", string(QueryBuild(prefix+"a=b&c=d&e=f", "c", "")))
assert.Equal(t, prefix+"a=b&c=d", string(QueryBuild(prefix+"a=b&c=d&e=f", "e", "")))
assert.Equal(t, prefix+"a=b&c=d&e=f", string(QueryBuild(prefix+"a=b&c=d&e=f", "k", "")))
})
}
}
const queryNonASCII = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" // all non-letter & non-number chars
func TestQueryEscape(t *testing.T) {
// this test is a reference for "urlQueryEscape" in JS
// Special case for space encoding:
// * RFC 3986: Uniform Resource Identifier (URI): %20
// * WHATWG HTML: application/x-www-form-urlencoded: +
// * JavaScript: encodeURIComponent() uses "%20". URLSearchParams uses "+"
// * Golang: QueryEscape uses "+"
expected := "+%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~"
assert.Equal(t, expected, url.QueryEscape(queryNonASCII))
}
func TestPathEscape(t *testing.T) {
// this test is a reference for "pathEscape" in JS
expected := "%20%21%22%23$%25&%27%28%29%2A+%2C-.%2F:%3B%3C=%3E%3F@%5B%5C%5D%5E_%60%7B%7C%7D~"
assert.Equal(t, expected, url.PathEscape(queryNonASCII))
}