- 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
139 lines
6.3 KiB
Go
139 lines
6.3 KiB
Go
package faq_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-faq-plugin/console"
|
|
"git.golem15.com/golem15/sm-faq-plugin/models"
|
|
)
|
|
|
|
// TestFaqAPI pins the D-17 rules of GET /_faq/api/v1/faqs/{slug} beyond the
|
|
// smoke test: an inactive lower id does not hide an active duplicate, the
|
|
// default locale follows is_default and a disabled default answers 404,
|
|
// questions are text (numbers formatted, null empty), empty content is [],
|
|
// the answer is JSON with no CORS headers of its own, and every malformed
|
|
// slug is the JSON 404.
|
|
func TestFaqAPI(t *testing.T) {
|
|
e := bootFAQ(t)
|
|
en, pl := e.localeID(t, "en"), e.localeID(t, "pl")
|
|
e.insertFAQ(t, "Old", "help", en, false, nil, `[{"question":"old","answer":"old"}]`)
|
|
e.insertFAQ(t, "Help", "help", en, true, nil, `[{"question":42,"answer":null},{"question":null,"answer":"<p>ok</p>"}]`)
|
|
e.insertFAQ(t, "Pomoc", "help", pl, true, nil, `[]`)
|
|
|
|
got := e.getFAQ(t, "/_faq/api/v1/faqs/help")
|
|
if got.Data.Name != "Help" || got.Data.Locale != "en" || len(got.Data.Content) != 2 ||
|
|
got.Data.Content[0].Question != "42" || got.Data.Content[0].Answer != "" || got.Data.Content[1].Question != "" || got.Data.Content[1].Answer != "<p>ok</p>" {
|
|
t.Fatalf("en = %+v", got.Data)
|
|
}
|
|
rec := e.publicGet(t, "/_faq/api/v1/faqs/help?locale=pl&x=1", "")
|
|
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"content":[]`) || !strings.Contains(rec.Body.String(), `"name":"Pomoc"`) {
|
|
t.Fatalf("pl = %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") || rec.Header().Get("Access-Control-Allow-Origin") != "" {
|
|
t.Fatalf("headers = %v", rec.Header())
|
|
}
|
|
var envelope map[string]json.RawMessage
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil || len(envelope) != 1 || envelope["data"] == nil {
|
|
t.Fatalf("envelope = %s", rec.Body.String())
|
|
}
|
|
|
|
// The default locale follows is_default.
|
|
if err := e.gdb.Exec(`UPDATE golem15_translate_locales SET is_default = (code = 'pl')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := e.getFAQ(t, "/_faq/api/v1/faqs/help"); got.Data.Name != "Pomoc" || got.Data.Locale != "pl" {
|
|
t.Fatalf("pl default = %+v", got.Data)
|
|
}
|
|
// A disabled default locale serves nothing without ?locale; en still answers.
|
|
if err := e.gdb.Exec(`UPDATE golem15_translate_locales SET is_enabled = (code <> 'pl')`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for path, status := range map[string]int{
|
|
"/_faq/api/v1/faqs/help": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/help?locale=pl": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/help?locale=en": http.StatusOK,
|
|
"/_faq/api/v1/faqs/HELP": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/-help": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/help_2": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/help.json": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/pomo%C4%87": http.StatusNotFound,
|
|
"/_faq/api/v1/faqs/%27or%271": http.StatusNotFound,
|
|
} {
|
|
rec := e.publicGet(t, path, "")
|
|
if rec.Code != status {
|
|
t.Errorf("GET %s: %d %s, want %d", path, rec.Code, rec.Body.String(), status)
|
|
continue
|
|
}
|
|
if status == http.StatusNotFound && rec.Body.String() != `{"error":"FAQ not found"}` {
|
|
t.Errorf("GET %s body = %s", path, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFaqImport pins the faq:import-winter edge cases beyond the smoke
|
|
// test: content keyed by row number imported in order, content as JSON
|
|
// text, a malformed row aborting before any write, and the option and file
|
|
// refusals; the dry run reports each FAQ with its question count.
|
|
func TestFaqImport(t *testing.T) {
|
|
e := bootFAQ(t)
|
|
ctx := t.Context()
|
|
dir := t.TempDir()
|
|
pl := e.localeID(t, "pl")
|
|
keyed := writeFile(t, dir, "keyed.jsonl",
|
|
`{"id": 3, "name": "Pomoc", "slug": "pomoc", "locale_code": "pl", "is_active": "1", "content": {"2":{"question":"drugie","answer":"b"},"1":{"question":"pierwsze","answer":"a"},"10":{"question":"dziesiąte","answer":"c"}}}`+"\n"+
|
|
`{"id": 5, "name": "Text", "slug": "text", "locale_code": "pl", "is_active": 1, "content": "[{\"question\":\"q\",\"answer\":\"a\"}]"}`+"\n")
|
|
|
|
out, buf := testOutput()
|
|
report, err := console.ImportWinterFile(ctx, e.gdb, console.WinterOptions{File: keyed, DryRun: true}, out)
|
|
if err != nil || report.FAQs != 2 || !strings.Contains(buf.String(), "FAQ 3 'pomoc' (pl): 3 questions") || !strings.Contains(buf.String(), "FAQ 5 'text' (pl): 1 questions") {
|
|
t.Fatalf("dry run: %+v %v\n%s", report, err, buf.String())
|
|
}
|
|
if n := e.countFAQs(t); n != 0 {
|
|
t.Fatalf("the dry run wrote %d rows", n)
|
|
}
|
|
out, buf = testOutput()
|
|
if _, err := console.ImportWinterFile(ctx, e.gdb, console.WinterOptions{File: keyed}, out); err != nil {
|
|
t.Fatalf("import: %v\n%s", err, buf.String())
|
|
}
|
|
var rows []models.FAQ
|
|
if err := e.gdb.Order("id").Find(&rows).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 2 || rows[0].LocaleID != pl || !rows[0].IsActive {
|
|
t.Fatalf("rows = %+v", rows)
|
|
}
|
|
var questions []string
|
|
for _, item := range rows[0].Items() {
|
|
questions = append(questions, item["question"].(string))
|
|
}
|
|
if strings.Join(questions, ",") != "pierwsze,drugie,dziesiąte" {
|
|
t.Fatalf("keyed content order = %v", questions)
|
|
}
|
|
if items := rows[1].Items(); len(items) != 1 || items[0]["answer"] != "a" {
|
|
t.Fatalf("text content = %v", items)
|
|
}
|
|
|
|
for name, tc := range map[string]struct {
|
|
opts console.WinterOptions
|
|
want string
|
|
}{
|
|
"no file": {console.WinterOptions{}, "give --file"},
|
|
"missing file": {console.WinterOptions{File: dir + "/missing.jsonl", Force: true}, "missing.jsonl"},
|
|
"bad row": {console.WinterOptions{File: writeFile(t, dir, "bad.jsonl",
|
|
`{"id": 7, "name": "Good", "slug": "good", "locale_code": "pl", "content": null}`+"\n"+
|
|
`{"id": 8, "slug": "nameless", "locale_code": "pl", "content": null}`+"\n"), Force: true}, "row 2"},
|
|
"bad content": {console.WinterOptions{File: writeFile(t, dir, "badcontent.jsonl", `{"id": 9, "name": "X", "slug": "x", "locale_code": "pl", "content": 5}`+"\n"), Force: true}, "content"},
|
|
} {
|
|
out, buf := testOutput()
|
|
if _, err := console.ImportWinterFile(ctx, e.gdb, tc.opts, out); err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Errorf("%s: err = %v, want %q\n%s", name, err, tc.want, buf.String())
|
|
}
|
|
}
|
|
if n := e.countFAQs(t); n != 2 {
|
|
t.Fatalf("refused imports changed the table: %d rows", n)
|
|
}
|
|
}
|