237 lines
8.3 KiB
Go
237 lines
8.3 KiB
Go
package entry
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestRenderHTMLEscapesRawHTML ist ein Sicherheitstest.
|
|
//
|
|
// Das gerenderte Ergebnis wird in der Weboberfläche ohne weiteres Escaping
|
|
// eingesetzt. Der Schutz davor, dass jemand über einen Eintrag ein <script>
|
|
// einschleust, hängt damit allein daran, dass goldmark kein Roh-HTML durchreicht.
|
|
// Wird diese Einstellung je gelockert, schlägt dieser Test an.
|
|
func TestRenderHTMLEscapesRawHTML(t *testing.T) {
|
|
malicious := []string{
|
|
"<script>alert(1)</script>",
|
|
"<img src=x onerror=alert(1)>",
|
|
"<iframe src=\"https://fremd.example\"></iframe>",
|
|
}
|
|
|
|
for _, input := range malicious {
|
|
html, err := RenderHTML(input, "/p/test/e/")
|
|
if err != nil {
|
|
t.Fatalf("RenderHTML(%q): %v", input, err)
|
|
}
|
|
if strings.Contains(html, "<script") || strings.Contains(html, "onerror") || strings.Contains(html, "<iframe") {
|
|
t.Errorf("Roh-HTML kam durch: %q → %q", input, html)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRenderHTMLResolvesLinks prüft, dass [[bezeichner]] zu einem Link wird.
|
|
func TestRenderHTMLResolvesLinks(t *testing.T) {
|
|
html, err := RenderHTML("Siehe [[server-zugang]] für Details.", "/p/test/e/")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !strings.Contains(html, `href="/p/test/e/server-zugang"`) {
|
|
t.Errorf("der Verweis wurde nicht aufgelöst: %s", html)
|
|
}
|
|
}
|
|
|
|
// TestRenderHTMLKeepsMarkdown prüft, dass gewöhnliches Markdown funktioniert.
|
|
func TestRenderHTMLKeepsMarkdown(t *testing.T) {
|
|
html, err := RenderHTML("# Überschrift\n\n- Punkt eins\n- Punkt zwei\n\n`code`", "/p/test/e/")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, want := range []string{"<h1", "<ul>", "<li>", "<code>"} {
|
|
if !strings.Contains(html, want) {
|
|
t.Errorf("%s fehlt im Ergebnis: %s", want, html)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderPlainStripsLinkSyntax(t *testing.T) {
|
|
got := RenderPlain("Siehe [[server-zugang]] für Details.")
|
|
if strings.Contains(got, "[[") {
|
|
t.Errorf("die Verweis-Klammern blieben stehen: %q", got)
|
|
}
|
|
if !strings.Contains(got, "server-zugang") {
|
|
t.Errorf("der Bezeichner ging verloren: %q", got)
|
|
}
|
|
}
|
|
|
|
// ── Textausschnitte ──────────────────────────────────────────────────────────
|
|
|
|
func TestSnippetAroundMatch(t *testing.T) {
|
|
text := strings.Repeat("Vorlauf. ", 40) + "GESUCHT " + strings.Repeat("Nachlauf. ", 40)
|
|
|
|
got := Snippet(text, "GESUCHT")
|
|
|
|
if !strings.Contains(got, "GESUCHT") {
|
|
t.Error("der Treffer fehlt im Ausschnitt")
|
|
}
|
|
// Der Ausschnitt soll kurz sein — er dient dem Einordnen, nicht dem Lesen.
|
|
if len(got) > 400 {
|
|
t.Errorf("Ausschnitt ist %d Zeichen lang, das ist zu viel", len(got))
|
|
}
|
|
// Vorne und hinten wurde gekürzt, das muss sichtbar sein.
|
|
if !strings.HasPrefix(got, "…") || !strings.HasSuffix(got, "…") {
|
|
t.Errorf("die Auslassungszeichen fehlen: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestSnippetWithoutMatch(t *testing.T) {
|
|
// Kein wörtlicher Treffer — etwa weil der Fund über die Präfix-Suche kam.
|
|
got := Snippet("Ein kurzer Text ohne den Suchbegriff.", "deploy")
|
|
|
|
if got == "" {
|
|
t.Error("ohne Treffer kam gar kein Ausschnitt zurück")
|
|
}
|
|
if !strings.Contains(got, "kurzer Text") {
|
|
t.Errorf("es wurde nicht der Textanfang genommen: %q", got)
|
|
}
|
|
}
|
|
|
|
// TestSnippetForWholeQuestion prüft den Normalfall bei Agenten: Die Anfrage ist
|
|
// eine ganze Frage, die so nie im Text steht. Der Ausschnitt muss trotzdem an
|
|
// der Fundstelle ansetzen, nicht am Textanfang.
|
|
func TestSnippetForWholeQuestion(t *testing.T) {
|
|
text := strings.Repeat("Vorlauf. ", 40) + "Der Zertifikatswechsel ist im März. " + strings.Repeat("Nachlauf. ", 40)
|
|
|
|
got := Snippet(text, "wie läuft der Zertifikatswechsel")
|
|
|
|
if !strings.Contains(got, "Zertifikatswechsel") {
|
|
t.Errorf("der Ausschnitt setzt nicht an der Fundstelle an: %q", got)
|
|
}
|
|
}
|
|
|
|
// TestSnippetLongestWordFirst prüft die Wahl zwischen mehreren Wörtern:
|
|
// Das längere, aussagekräftigere gewinnt, auch wenn das kürzere früher steht.
|
|
func TestSnippetLongestWordFirst(t *testing.T) {
|
|
text := "Dienst. " + strings.Repeat("Füllwort. ", 60) + "Kubernetes-Cluster hier."
|
|
|
|
got := Snippet(text, "dienst kubernetes")
|
|
|
|
if !strings.Contains(got, "Kubernetes") {
|
|
t.Errorf("es wurde nicht das längste Wort gewählt: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestHitWord(t *testing.T) {
|
|
text := "Wir deployen über GitHub Actions."
|
|
|
|
cases := map[string]string{
|
|
"GitHub Actions": "GitHub Actions", // ganze Anfrage kommt vor
|
|
"wie deployen wir": "deployen",
|
|
"wie wir": "", // nur kurze Wörter
|
|
"gibtesnicht": "",
|
|
"": "",
|
|
"actions, github?": "actions", // gleich lang: Reihenfolge der Anfrage
|
|
}
|
|
|
|
for query, want := range cases {
|
|
if got := hitWord(text, query); got != want {
|
|
t.Errorf("trefferwort(%q) = %q, erwartet %q", query, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHighlightSnippetSingleWord prüft, dass bei einer ganzen Frage das Wort
|
|
// hervorgehoben wird, an dem sich der Ausschnitt ausrichtet.
|
|
func TestHighlightSnippetSingleWord(t *testing.T) {
|
|
got := HighlightSnippet("Wir deployen über GitHub Actions.", "wie deployen wir")
|
|
|
|
if !strings.Contains(got, "<mark>deployen</mark>") {
|
|
t.Errorf("das Fundwort wurde nicht hervorgehoben: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestSnippetShortText(t *testing.T) {
|
|
got := Snippet("Kurz.", "Kurz")
|
|
if got != "Kurz." {
|
|
t.Errorf("Snippet = %q, erwartet %q", got, "Kurz.")
|
|
}
|
|
}
|
|
|
|
// TestHighlightSnippetEscapesFirst ist ein Sicherheitstest: Der Inhalt muss
|
|
// escaped werden, BEVOR die Hervorhebung eingesetzt wird. Andersherum könnte ein
|
|
// Eintrag eigenes HTML in die Trefferliste einschleusen.
|
|
func TestHighlightSnippetEscapesFirst(t *testing.T) {
|
|
got := HighlightSnippet("Ein <script>alert(1)</script> im Text", "script")
|
|
|
|
if strings.Contains(got, "<script>") {
|
|
t.Errorf("unescaptes HTML im Ausschnitt: %q", got)
|
|
}
|
|
// Die spitzen Klammern des Inhalts müssen escaped sein. Geprüft wird auf
|
|
// "<" statt auf "<script>", weil die Hervorhebung mitten im
|
|
// escapten Text steht: "<<mark>script</mark>>".
|
|
if !strings.Contains(got, "<") || !strings.Contains(got, ">") {
|
|
t.Errorf("der Inhalt wurde nicht escaped: %q", got)
|
|
}
|
|
// Die Hervorhebung selbst muss echtes HTML sein.
|
|
if !strings.Contains(got, "<mark>") {
|
|
t.Errorf("der Treffer wurde nicht hervorgehoben: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestHighlightSnippetWithoutQuery(t *testing.T) {
|
|
got := HighlightSnippet("Text mit <Klammern>", "")
|
|
if strings.Contains(got, "<mark>") {
|
|
t.Error("ohne Suchbegriff wurde etwas hervorgehoben")
|
|
}
|
|
if strings.Contains(got, "<Klammern>") {
|
|
t.Errorf("der Inhalt wurde nicht escaped: %q", got)
|
|
}
|
|
}
|
|
|
|
// ── Titel raten ──────────────────────────────────────────────────────────────
|
|
|
|
func TestTitleFromContent(t *testing.T) {
|
|
cases := map[string]string{
|
|
"# Deployment\n\nText": "Deployment",
|
|
"## Unterüberschrift\nX": "Unterüberschrift",
|
|
"\n\nErste echte Zeile\nX": "Erste echte Zeile",
|
|
"": "Ohne Titel",
|
|
" \n \n": "Ohne Titel",
|
|
}
|
|
|
|
for input, want := range cases {
|
|
if got := TitleFromContent(input); got != want {
|
|
t.Errorf("TitleFromContent(%q) = %q, erwartet %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTitleFromContentIsShortened(t *testing.T) {
|
|
long := strings.Repeat("Wort ", 100)
|
|
got := TitleFromContent(long)
|
|
|
|
if len(got) > 130 {
|
|
t.Errorf("der Titel ist %d Zeichen lang — er wurde nicht gekürzt", len(got))
|
|
}
|
|
}
|
|
|
|
// ── Auch gesucht als ─────────────────────────────────────────────────────────
|
|
|
|
func TestAlsoSearchedAs(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Text.\n\nAuch gesucht als: release, rollout": "release, rollout",
|
|
"Text.\n*Auch gesucht als:* release": "release",
|
|
"- **Auch gesucht als:** release, auslieferung": "release, auslieferung",
|
|
"auch gesucht als: klein geschrieben": "klein geschrieben",
|
|
"Auch gesucht als: alt\nText\nAuch gesucht als: neu": "neu",
|
|
"Ein Text ohne die Zeile.": "",
|
|
"Wir haben auch gesucht als Team.": "", // nicht am Zeilenanfang
|
|
}
|
|
|
|
for content, want := range cases {
|
|
if got := AlsoSearchedAs(content); got != want {
|
|
t.Errorf("AuchGesuchtAls(%q) = %q, erwartet %q", content, got, want)
|
|
}
|
|
}
|
|
}
|