- TestFaqContentDecode, TestFaqModel and TestFaqSerialize (Docker-free): content as list, JSON text or row-keyed object, required export fields, the model contract, output sanitizing, null description, question text and the slug shape - TestFaqAPI and TestFaqImport (Postgres): active duplicate behind an inactive lower id, default locale per is_default, a disabled default, malformed slugs, keyed content order, malformed rows aborting before any write
56 lines
2.2 KiB
Go
56 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-faq-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
)
|
|
|
|
// TestFaqSerialize pins the D-17 response object without a database: the
|
|
// description and every answer pass through the sanitizer on output, clean
|
|
// HTML byte for byte; a null description stays null; questions are plain
|
|
// text (numbers formatted, null empty); content is never null; and the
|
|
// slug pattern the handler accepts.
|
|
func TestFaqSerialize(t *testing.T) {
|
|
desc := `<p>About.</p><img src="/x.png" onerror="alert(1)">`
|
|
faq := models.FAQ{Name: "Shipping", Slug: "shipping", Description: &desc, Content: lagoon.Jsonable[models.Content]{Valid: true, Data: models.Content{
|
|
{"question": "How long?", "answer": "<p>Two <strong>days</strong> & more.</p>"},
|
|
{"question": 42.0, "answer": `<p onclick="x()">Here</p><script>alert(1)</script>`},
|
|
{"question": nil, "answer": nil},
|
|
}}}
|
|
out := serialize(faq, "en")
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw := strings.TrimSuffix(buf.String(), "\n")
|
|
want := `{"name":"Shipping","slug":"shipping","locale":"en","description":"<p>About.</p><img src=\"/x.png\">","content":[` +
|
|
`{"question":"How long?","answer":"<p>Two <strong>days</strong> & more.</p>"},` +
|
|
`{"question":"42","answer":"<p>Here</p>"},` +
|
|
`{"question":"","answer":""}]}`
|
|
if raw != want {
|
|
t.Fatalf("serialized:\n%s\nwant:\n%s", raw, want)
|
|
}
|
|
if strings.Contains(raw, "onerror") || strings.Contains(raw, "onclick") || strings.Contains(raw, "<script") {
|
|
t.Fatal("unsafe HTML served")
|
|
}
|
|
bare := serialize(models.FAQ{Name: "Empty", Slug: "empty"}, "pl")
|
|
if bare.Description != nil || bare.Content == nil || len(bare.Content) != 0 || bare.Locale != "pl" {
|
|
t.Fatalf("bare = %+v", bare)
|
|
}
|
|
for slug, ok := range map[string]bool{
|
|
"shipping": true, "a": true, "faq-2": true, "2024-faq": true,
|
|
"Shipping": false, "-faq": false, "faq_2": false, "faq.json": false, "zażółć": false, "": false, "a b": false,
|
|
} {
|
|
if slugPattern.MatchString(slug) != ok {
|
|
t.Errorf("slug %q accepted = %v", slug, !ok)
|
|
}
|
|
}
|
|
}
|