package reader_test import ( "strings" "testing" "github.com/lukaszraczylo/mcp-readitall/internal/reader" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============================================================================= // HTMLToMarkdown // ============================================================================= func TestHTMLToMarkdown_BasicElements(t *testing.T) { tests := []struct { name string html string domain string want []string // substrings that must all appear in the output, in order // optional: substrings that must NOT appear notWant []string }{ // ===== GOOD CASES ===== { name: "headings hierarchy", html: `

Title

Sub

SubSub

`, want: []string{"# Title", "## Sub", "### SubSub"}, }, { name: "paragraphs and emphasis", html: `

This is bold and italic text.

`, want: []string{"This is **bold** and _italic_ text."}, }, { name: "unordered list", html: ``, want: []string{"- alpha", "- beta", "- gamma"}, }, { name: "ordered list", html: `
  1. first
  2. second
`, want: []string{"1. first", "2. second"}, }, { name: "inline code", html: `

Use fmt.Println to print.

`, want: []string{"Use `fmt.Println` to print."}, }, { name: "fenced code block", html: `
package main
func main() {}
`, want: []string{"```", "package main", "func main()"}, }, { name: "table", html: `
NameAge
Alice30
Bob25
`, // Cells are right-padded to align columns, so we look for the // left-delimited portion (" 30 " / " 25 ") without pinning // trailing whitespace. want: []string{"| Name", "| Age", "| Alice", "| 30", "| Bob", "| 25"}, }, { name: "horizontal rule", html: `

before


after

`, want: []string{"before", "---", "after"}, }, // ===== RELATIVE URLS ===== { name: "absolute links stay absolute", html: `other`, domain: "https://this.test", want: []string{`[other](https://other.test/x)`}, }, { name: "relative link is resolved against domain", html: `about`, domain: "https://this.test", want: []string{`[about](https://this.test/about)`}, }, { name: "relative image is resolved", html: `logo`, domain: "https://this.test", want: []string{`![logo](https://this.test/logo.png)`}, }, { name: "no domain leaves relative links alone", html: `about`, domain: "", want: []string{`[about](/about)`}, }, // ===== STRIKETHROUGH ===== { name: "strikethrough", html: `

old removed new

`, want: []string{"old ~~removed~~ new"}, }, // ===== BAD / TRICKY INPUTS ===== { name: "script and style are stripped", html: `

visible

also visible

`, want: []string{"visible", "also visible"}, notWant: []string{ "alert(1)", "p{}", }, }, { name: "html comments are stripped", html: `

before

after

`, want: []string{"before", "after"}, notWant: []string{ "a comment", }, }, { name: "nested tags are flattened correctly", html: `

outer with nested emphasis

`, want: []string{"outer **with _nested_ emphasis**"}, }, // ===== EDGE CASES ===== { name: "empty string", html: "", want: []string{}, }, { name: "whitespace only", html: " \n\t ", want: []string{}, }, { name: "plain text without tags", html: `just plain text`, want: []string{"just plain text"}, }, { name: "html entities are decoded in text", html: `

5 < 10 & 10 > 5

`, // In EscapeModeSmart the library decodes & to & but // preserves </> as entities so the output cannot be // interpreted as raw HTML/Markdown by downstream renderers. want: []string{"5 < 10 & 10 > 5"}, }, { name: "deeply nested lists", html: ``, want: []string{"- one", "nested", "- two"}, }, { name: "self-closing void elements do not break parser", html: `

line one
line two

line four

`, want: []string{"line one", "line two", "line four"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := reader.HTMLToMarkdown(tt.html, tt.domain) require.NoError(t, err) // Trim trailing whitespace from each want so test data is // easier to read; markdown output may add trailing newlines. for _, w := range tt.want { assert.Contains(t, got, w, "expected substring %q in output:\n%s", w, got) } for _, nw := range tt.notWant { assert.NotContains(t, got, nw, "unexpected substring %q in output:\n%s", nw, got) } }) } } func TestHTMLToMarkdown_RealisticArticle(t *testing.T) { t.Parallel() html := ` Sample

How to Test

Testing is important. See the docs.

Setup

  1. Install Go
  2. Run go test ./...

Example

package main
import "fmt"
func main() { fmt.Println("hi") }
InputOutput
1one
` md, err := reader.HTMLToMarkdown(html, "https://example.test/blog") require.NoError(t, err) // Spot-check the key transformations mustContain := []string{ "# How to Test", "Testing is **important**", "[the docs](https://example.test/docs)", "## Setup", "1. Install Go", "2. Run `go test ./...`", "## Example", "```", "package main", "| Input | Output |", "| 1 | one |", } for _, s := range mustContain { assert.Contains(t, md, s, "missing %q in:\n%s", s, md) } } func TestHTMLToMarkdown_DoesNotLeakScriptOrStyle(t *testing.T) { t.Parallel() html := `

safe

also safe

` md, err := reader.HTMLToMarkdown(html, "") require.NoError(t, err) // Output should not contain the malicious payload assert.NotContains(t, md, "ABCDEFG") assert.NotContains(t, md, "evil.test") assert.NotContains(t, md, "document.cookie") // But it should still contain the visible text assert.Contains(t, strings.ToLower(md), "safe") assert.Contains(t, strings.ToLower(md), "also safe") }