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":"
ok
"}]`) 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 != "ok
" { 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) } }