wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-25 22:26:34 +04:00
parent 65fc5ecc7f
commit 1682a3513f
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
13 changed files with 292 additions and 89 deletions

View File

@ -55,6 +55,13 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
OnlyOnce: true,
Config: trim,
}
plainTextFormatFlag = cli.StringFlag{
Name: "plaintext-format",
Usage: "override the default error page response in plain text format (Go templates are supported)",
Sources: env("RESPONSE_PLAINTEXT_FORMAT"),
OnlyOnce: true,
Config: trim,
}
templateNameFlag = cli.StringFlag{
Name: "template-name",
Aliases: []string{"t"},
@ -162,6 +169,23 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
for _, templatePath := range add {
if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
} else {
log.Info("Template added",
logger.String("name", addedName),
logger.String("path", templatePath),
)
}
}
}
if !cfg.Templates.Has(cfg.TemplateName) {
return fmt.Errorf("template %s not found and cannot be used", cfg.TemplateName)
}
if c.IsSet(proxyHeadersListFlag.Name) {
var m = make(map[string]struct{}) // map is used to avoid duplicates
@ -176,19 +200,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
}
}
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
for _, templatePath := range add {
if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
} else {
log.Info("Template added",
logger.String("name", addedName),
logger.String("path", templatePath),
)
}
}
}
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 { // add custom HTTP codes
for code, msgAndDesc := range add {
var (
@ -216,11 +227,15 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
{ // override default JSON and XML formats
if c.IsSet(jsonFormatFlag.Name) {
cfg.Formats.JSON = c.String(jsonFormatFlag.Name)
cfg.Formats.JSON = strings.TrimSpace(c.String(jsonFormatFlag.Name))
}
if c.IsSet(xmlFormatFlag.Name) {
cfg.Formats.XML = c.String(xmlFormatFlag.Name)
cfg.Formats.XML = strings.TrimSpace(c.String(xmlFormatFlag.Name))
}
if c.IsSet(plainTextFormatFlag.Name) {
cfg.Formats.PlainText = strings.TrimSpace(c.String(plainTextFormatFlag.Name))
}
}
@ -246,6 +261,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
&addCodeFlag,
&jsonFormatFlag,
&xmlFormatFlag,
&plainTextFormatFlag,
&templateNameFlag,
&disableL10nFlag,
&defaultCodeToRenderFlag,

View File

@ -115,7 +115,7 @@ func TestAddHTTPCodeFlag(t *testing.T) {
assert.Equal(t, "add-http-code", flag.Name)
for name, _tt := range map[string]struct {
for name, tt := range map[string]struct {
giveValue map[string]string
wantErrMsg string
}{
@ -152,8 +152,6 @@ func TestAddHTTPCodeFlag(t *testing.T) {
wantErrMsg: "missing message for HTTP code [200]",
},
} {
var tt = _tt
t.Run(name, func(t *testing.T) {
if err := flag.Validator(tt.giveValue); tt.wantErrMsg != "" {
assert.ErrorContains(t, err, tt.wantErrMsg)

View File

@ -66,7 +66,7 @@ func TestCodes_Find(t *testing.T) {
"*": {Message: "Single"},
}
for name, _tt := range map[string]struct {
for name, tt := range map[string]struct {
giveCodes config.Codes
giveCode uint16
@ -114,8 +114,6 @@ func TestCodes_Find(t *testing.T) {
"empty map": {giveCodes: config.Codes{}, giveCode: 404, wantNotFound: true},
"zero code": {giveCodes: common, giveCode: 0, wantNotFound: true},
} {
var tt = _tt
t.Run(name, func(t *testing.T) {
for i := 0; i < 100; i++ { // repeat the test to ensure the function is idempotent
var desc, found = tt.giveCodes.Find(tt.giveCode)

View File

@ -16,8 +16,9 @@ type Config struct {
// Formats contain alternative response formats (e.g., if a client requests a response in one of these formats,
// we will render the response using the specified format instead of HTML; Go templates are supported).
Formats struct {
JSON string
XML string
JSON string
XML string
PlainText string
}
// Codes hold descriptions for HTTP codes (e.g., 404: "Not Found / The server can not find the requested page").
@ -59,7 +60,7 @@ type Config struct {
const defaultJSONFormat string = `{
"error": true,
"Code": {{ Code | json }},
"code": {{ code | json }},
"message": {{ message | json }},
"description": {{ description | json }}{{ if show_details }},
"details": {
@ -77,7 +78,7 @@ const defaultJSONFormat string = `{
const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
<error>
<Code>{{ Code }}</Code>
<code>{{ code }}</code>
<message>{{ message }}</message>
<description>{{ description }}</description>{{ if show_details }}
<details>
@ -93,6 +94,19 @@ const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
</details>{{ end }}
</error>`
const defaultPlainTextFormat string = `Error {{ code }}: {{ message }}{{ if description }}
{{ description }}{{ end }}{{ if show_details }}
Host: {{ host }}
Original URI: {{ original_uri }}
Forwarded For: {{ forwarded_for }}
Namespace: {{ namespace }}
Ingress Name: {{ ingress_name }}
Service Name: {{ service_name }}
Service Port: {{ service_port }}
Request ID: {{ request_id }}
Timestamp: {{ now.Unix }}{{ end }}`
//nolint:lll
var defaultCodes = Codes{ //nolint:gochecknoglobals
"400": {"Bad Request", "The server did not understand the request"},
@ -134,6 +148,7 @@ func New() Config {
cfg.Formats.JSON = defaultJSONFormat
cfg.Formats.XML = defaultXMLFormat
cfg.Formats.PlainText = defaultPlainTextFormat
// add built-in templates
for name, content := range builtinTemplates.BuiltIn() {

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/template"
)
func TestNew(t *testing.T) {
@ -17,6 +18,7 @@ func TestNew(t *testing.T) {
assert.NotEmpty(t, cfg.Formats.XML)
assert.NotEmpty(t, cfg.Formats.JSON)
assert.NotEmpty(t, cfg.Formats.PlainText)
assert.True(t, len(cfg.Codes) >= 19)
assert.True(t, len(cfg.Templates) >= 2)
assert.NotEmpty(t, cfg.TemplateName)
@ -35,4 +37,21 @@ func TestNew(t *testing.T) {
assert.NotEqual(t, cfg1.ProxyHeaders, cfg2.ProxyHeaders)
})
t.Run("render default format templates", func(t *testing.T) {
var cfg = config.New()
for _, content := range []string{cfg.Formats.JSON, cfg.Formats.XML, cfg.Formats.PlainText} {
var result, err = template.Render(content, template.Props{
ShowRequestDetails: true,
Code: 404,
Message: "Not Found",
})
assert.NotEmpty(t, result)
assert.NoError(t, err)
t.Log(result)
}
})
}

View File

@ -1,13 +1,19 @@
package error_page
import (
"encoding/json"
"fmt"
"net/http"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/logger"
"gh.tarampamp.am/error-pages/internal/template"
)
func New(cfg *config.Config) http.Handler {
const contentTypeHeader = "Content-Type"
// New creates a new handler that returns an error page with the specified status code and format.
func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,gocognit
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var code uint16
@ -29,36 +35,132 @@ func New(cfg *config.Config) http.Handler {
var format = detectPreferredFormatForClient(r.Header)
switch headerName := "Content-Type"; format {
case jsonFormat:
w.Header().Set(headerName, "application/json; charset=utf-8")
case xmlFormat:
w.Header().Set(headerName, "application/xml; charset=utf-8")
case htmlFormat:
w.Header().Set(headerName, "text/html; charset=utf-8")
case plainTextFormat:
w.Header().Set(headerName, "text/plain; charset=utf-8")
default:
w.Header().Set(headerName, "text/html; charset=utf-8")
}
{ // deal with the headers
switch format {
case jsonFormat:
w.Header().Set(contentTypeHeader, "application/json; charset=utf-8")
case xmlFormat:
w.Header().Set(contentTypeHeader, "application/xml; charset=utf-8")
case htmlFormat:
w.Header().Set(contentTypeHeader, "text/html; charset=utf-8")
default:
w.Header().Set(contentTypeHeader, "text/plain; charset=utf-8") // plainTextFormat as default
}
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
// disallow indexing of the error pages
w.Header().Set("X-Robots-Tag", "noindex")
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
// disallow indexing of the error pages
w.Header().Set("X-Robots-Tag", "noindex")
if code >= 500 && code < 600 {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
// tell the client (search crawler) to retry the request after 120 seconds, it makes sense for the 5xx errors
w.Header().Set("Retry-After", "120")
}
if code >= 500 && code < 600 {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
// tell the client (search crawler) to retry the request after 120 seconds, it makes sense for the 5xx errors
w.Header().Set("Retry-After", "120")
}
for _, proxyHeader := range cfg.ProxyHeaders {
if value := r.Header.Get(proxyHeader); value != "" {
w.Header().Set(proxyHeader, value)
// proxy the headers from the incoming request to the error page response if they are defined in the config
for _, proxyHeader := range cfg.ProxyHeaders {
if value := r.Header.Get(proxyHeader); value != "" {
w.Header().Set(proxyHeader, value)
}
}
}
w.WriteHeader(httpCode)
_, _ = w.Write([]byte(fmt.Sprintf("<html>error page for the code %d</html>", code)))
// prepare the template properties for rendering
var tplProps = template.Props{
Code: code, // http status code
ShowRequestDetails: cfg.ShowDetails, // status message
L10nDisabled: cfg.L10n.Disable, // status description
}
//nolint:lll
if cfg.ShowDetails { // https://kubernetes.github.io/ingress-nginx/user-guide/custom-errors/
tplProps.OriginalURI = r.Header.Get("X-Original-URI") // (ingress-nginx) URI that caused the error
tplProps.Namespace = r.Header.Get("X-Namespace") // (ingress-nginx) namespace where the backend Service is located
tplProps.IngressName = r.Header.Get("X-Ingress-Name") // (ingress-nginx) name of the Ingress where the backend is defined
tplProps.ServiceName = r.Header.Get("X-Service-Name") // (ingress-nginx) name of the Service backing the backend
tplProps.ServicePort = r.Header.Get("X-Service-Port") // (ingress-nginx) port number of the Service backing the backend
tplProps.RequestID = r.Header.Get("X-Request-Id") // (ingress-nginx) unique ID that identifies the request - same as for backend service
tplProps.ForwardedFor = r.Header.Get("X-Forwarded-For") // the value of the `X-Forwarded-For` header
tplProps.Host = r.Header.Get("Host") // the value of the `Host` header
}
// try to find the code message and description in the config and if not - use the standard status text or fallback
if desc, found := cfg.Codes.Find(code); found {
tplProps.Message = desc.Message
tplProps.Description = desc.Description
} else if stdlibStatusText := http.StatusText(int(code)); stdlibStatusText != "" {
tplProps.Message = stdlibStatusText
} else {
tplProps.Message = "Unknown Status Code" // fallback
}
switch {
case format == jsonFormat && cfg.Formats.JSON != "":
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
j, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
write(w, log, j)
} else {
write(w, log, content)
}
case format == xmlFormat && cfg.Formats.XML != "":
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
write(w, log, fmt.Sprintf(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>", err.Error(),
))
} else {
write(w, log, content)
}
case format == htmlFormat:
if tpl, found := cfg.Templates.Get(cfg.TemplateName); found {
if content, err := template.Render(tpl, tplProps); err != nil {
write(w, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>",
cfg.TemplateName,
err.Error(),
))
} else {
write(w, log, content)
}
} else {
write(w, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", cfg.TemplateName,
))
}
default: // plainTextFormat as default
if cfg.Formats.PlainText != "" {
if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil {
write(w, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error()))
} else {
write(w, log, content)
}
} else {
write(w, log, `The requested content format is not supported.
Please create an issue on the project's GitHub page to request support for it.
Supported formats: JSON, XML, HTML, Plain Text`)
}
}
})
}
func write[T string | []byte](w http.ResponseWriter, log *logger.Logger, content T) {
var data []byte
if s, ok := any(content).(string); ok {
data = []byte(s)
} else {
data = any(content).([]byte)
}
if _, err := w.Write(data); err != nil && log != nil {
log.Error("failed to write the response body",
logger.String("content", string(data)),
logger.Error(err),
)
}
}

View File

@ -49,7 +49,7 @@ func (s *Server) Register(cfg *config.Config) error {
var (
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
errorPagesHandler = ep.New(cfg)
errorPagesHandler = ep.New(cfg, s.log)
)
s.server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@ -7,7 +7,6 @@ import (
"io"
"net"
"net/http"
"strconv"
"testing"
"time"
@ -19,12 +18,33 @@ import (
"gh.tarampamp.am/error-pages/internal/logger"
)
// TestRouting in fact is a test for the whole server, because it tests all the routes and their handlers.
func TestRouting(t *testing.T) {
var (
srv = appHttp.NewServer(context.Background(), logger.NewNop())
cfg = config.New()
)
assert.NoError(t, cfg.Templates.Add("unit-test", `<!DOCTYPE html>
<html lang="en">
<h1>Error {{ code }}: {{ message }}</h1>{{ if description }}
<h2>{{ description }}</h2>{{ end }}{{ if show_details }}
<pre>
Host: {{ host }}
Original URI: {{ original_uri }}
Forwarded For: {{ forwarded_for }}
Namespace: {{ namespace }}
Ingress Name: {{ ingress_name }}
Service Name: {{ service_name }}
Service Port: {{ service_port }}
Request ID: {{ request_id }}
Timestamp: {{ now.Unix }}
</pre>{{ end }}
</html>`))
cfg.TemplateName = "unit-test"
require.NoError(t, srv.Register(&cfg))
var baseUrl, stopServer = startServer(t, &srv)
@ -101,78 +121,106 @@ func TestRouting(t *testing.T) {
t.Run("error page", func(t *testing.T) {
t.Run("success", func(t *testing.T) {
var assertErrorPage = func(t *testing.T, wantErrorPageCode int, body []byte, headers http.Header) {
t.Helper()
var bodyStr = string(body)
assert.NotEmpty(t, bodyStr)
assert.Contains(t, bodyStr, "error page") // FIXME
assert.Contains(t, bodyStr, strconv.Itoa(wantErrorPageCode)) // FIXME?
assert.Contains(t, headers.Get("Content-Type"), "text/html") // FIXME
}
t.Run("index, default", func(t *testing.T) {
t.Run("index, default (plain text by default)", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/")
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("index, default (json format)", func(t *testing.T) {
var status, body, headers = sendRequest(t,
http.MethodGet, baseUrl+"/", map[string]string{"Accept": "application/json"},
)
assert.Equal(t, http.StatusOK, status)
assert.Contains(t, string(body), `"code": 404`)
assert.Contains(t, headers.Get("Content-Type"), "application/json")
})
t.Run("index, default (xml format)", func(t *testing.T) {
var status, body, headers = sendRequest(t,
http.MethodGet, baseUrl+"/", map[string]string{"Accept": "application/xml"},
)
assert.Equal(t, http.StatusOK, status)
assert.Contains(t, string(body), `<code>404</code>`)
assert.Contains(t, headers.Get("Content-Type"), "application/xml")
})
t.Run("index, default (html format)", func(t *testing.T) {
var status, body, headers = sendRequest(t,
http.MethodGet, baseUrl+"/", map[string]string{"Content-Type": "text/html"},
)
assert.Equal(t, http.StatusOK, status)
assert.Contains(t, string(body), `<h1>Error 404: Not Found</h1>`)
assert.Contains(t, headers.Get("Content-Type"), "text/html")
})
t.Run("index, code in HTTP header", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "404"})
assert.Equal(t, http.StatusOK, status) // because of [cfg.RespondWithSameHTTPCode] is false by default
assertErrorPage(t, 404, body, headers)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("code in URL, .html", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/500.html")
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, 500, body, headers)
assert.Contains(t, string(body), "500: Internal Server Error")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("code in URL, .htm", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/409.htm")
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, 409, body, headers)
assert.Contains(t, string(body), "409: Conflict")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("code in URL, without extension", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405")
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, 405, body, headers)
assert.Contains(t, string(body), "405: Method Not Allowed")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("code in the URL have higher priority than in the headers", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/405", map[string]string{"X-Code": "404"})
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, 405, body, headers)
assert.Contains(t, string(body), "405: Method Not Allowed")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("invalid code in HTTP header (with a string)", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "foobar"})
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("invalid code in HTTP header (too small)", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "0"})
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
t.Run("invalid code in HTTP header (too big)", func(t *testing.T) {
var status, body, headers = sendRequest(t, http.MethodGet, baseUrl+"/", map[string]string{"X-Code": "1000"})
assert.Equal(t, http.StatusOK, status)
assertErrorPage(t, int(cfg.DefaultCodeToRender), body, headers)
assert.Contains(t, string(body), "404: Not Found")
assert.Contains(t, headers.Get("Content-Type"), "text/plain")
})
})

View File

@ -38,5 +38,8 @@ func Time(key string, v time.Time) Attr { return slog.Time(key, v) }
// Duration returns an Attr for a [time.Duration].
func Duration(key string, v time.Duration) Attr { return slog.Duration(key, v) }
// Error returns an Attr for an error.
func Error(err error) Attr { return slog.String("error", err.Error()) }
// Any returns an Attr for any value.
func Any(key string, v any) Attr { return slog.Any(key, v) }

View File

@ -1,6 +1,8 @@
package logger_test
import (
"errors"
"fmt"
"testing"
"time"
@ -12,9 +14,12 @@ import (
func TestAttrs(t *testing.T) {
t.Parallel()
var someTime, _ = time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
var (
someTime, _ = time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
someErr = fmt.Errorf("foo: %w", errors.New("bar"))
)
for name, _tt := range map[string]struct {
for name, tt := range map[string]struct {
giveAttr logger.Attr
wantKey string
@ -30,10 +35,9 @@ func TestAttrs(t *testing.T) {
"Bool": {logger.Bool("key", true), "key", true},
"Time": {logger.Time("key", someTime), "key", someTime},
"Duration": {logger.Duration("key", time.Second), "key", time.Second},
"Error": {logger.Error(someErr), "error", "foo: bar"},
"Any": {logger.Any("key", "value"), "key", "value"},
} {
tt := _tt
t.Run(name, func(t *testing.T) {
t.Parallel()

View File

@ -4,7 +4,7 @@ import "reflect"
//nolint:lll
type Props struct {
Code string `token:"code"` // http status code
Code uint16 `token:"code"` // http status code
Message string `token:"message"` // status message
Description string `token:"description"` // status description
OriginalURI string `token:"original_uri"` // (ingress-nginx) URI that caused the error

View File

@ -12,7 +12,7 @@ func TestProps_Values(t *testing.T) {
t.Parallel()
assert.Equal(t, template.Props{
Code: "a",
Code: 1,
Message: "b",
Description: "c",
OriginalURI: "d",
@ -25,7 +25,7 @@ func TestProps_Values(t *testing.T) {
L10nDisabled: true,
ShowRequestDetails: false,
}.Values(), map[string]any{
"code": "a",
"code": uint16(1),
"message": "b",
"description": "c",
"original_uri": "d",

View File

@ -84,33 +84,33 @@ func TestRender(t *testing.T) {
}{
"common case": {
giveTemplate: "{{code}}: {{ message }} {{description}}",
giveProps: template.Props{Code: "404", Message: "Not found", Description: "Blah"},
giveProps: template.Props{Code: 404, Message: "Not found", Description: "Blah"},
wantResult: "404: Not found Blah",
},
"html markup": {
giveTemplate: "<!-- comment --><html><body>{{code}}: {{ message }} {{description}}</body></html>",
giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
wantResult: "<!-- comment --><html><body>201: lorem ipsum </body></html>",
},
"with line breakers": {
giveTemplate: "\t {{code}}: {{ message }} {{description}}\n",
giveTemplate: "\t {{code | json}}: {{ message }} {{description}}\n",
giveProps: template.Props{},
wantResult: "\t : \n",
wantResult: "\t 0: \n",
},
"golang template": {
giveTemplate: "\t {{code}} {{ .Code }}{{ if .Message }} Yeah {{end}}",
giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
wantResult: "\t 201 201 Yeah ",
},
"json common case": {
giveTemplate: `{"code": {{code | json}}, "message": {"here":[ {{ message | json }} ]}, "desc": "{{description}}"}`,
giveProps: template.Props{Code: `404'"{`, Message: "Not found\t\r\n"},
wantResult: `{"code": "404'\"{", "message": {"here":[ "Not found\t\r\n" ]}, "desc": ""}`,
giveProps: template.Props{Code: 404, Message: "'\"{Not found\t\r\n"},
wantResult: `{"code": 404, "message": {"here":[ "'\"{Not found\t\r\n" ]}, "desc": ""}`,
},
"json golang template": {
giveTemplate: `{"code": "{{code}}", "message": {"here":[ "{{ if .Message }} Yeah {{end}}" ]}}`,
giveProps: template.Props{Code: "201", Message: "lorem ipsum"},
giveProps: template.Props{Code: 201, Message: "lorem ipsum"},
wantResult: `{"code": "201", "message": {"here":[ " Yeah " ]}}`,
},
@ -127,7 +127,7 @@ func TestRender(t *testing.T) {
"complete example with every property and function": {
giveProps: template.Props{
Code: "404",
Code: 404,
Message: "Not found",
Description: "Blah",
OriginalURI: "/test",