package build
import (
"fmt"
"image/png"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"git.coffeylabs.org/coffey-labs/hotdog-cms/internal/site"
"git.coffeylabs.org/coffey-labs/hotdog-cms/starter"
)
// writeStarter puts the starter site in a temporary folder.
func writeStarter(t *testing.T) string {
t.Helper()
dir := t.TempDir()
err := fs.WalkDir(starter.Files, "site", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
target := filepath.Join(dir, strings.TrimPrefix(p, "site"))
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
data, _ := starter.Files.ReadFile(p)
return os.WriteFile(target, []byte(strings.ReplaceAll(string(data), "{{SITE_NAME}}", "Test")), 0o644)
})
if err != nil {
t.Fatal(err)
}
return dir
}
func TestStarterBuilds(t *testing.T) {
dir := writeStarter(t)
res, err := Run(Options{SiteDir: dir})
if err != nil {
t.Fatal(err)
}
for _, f := range []string{"index.html", "about/index.html", "articles/index.html", "articles/hello-world/index.html",
"tags/hotdog-cms/index.html", "404.html", "sitemap.xml", "robots.txt", "articles/feed.xml", "llms.txt"} {
if _, err := os.Stat(filepath.Join(res.Out, f)); err != nil {
t.Errorf("missing %s", f)
}
}
home, _ := os.ReadFile(filepath.Join(res.Out, "index.html"))
if strings.Contains(string(home), `href="/styles.css"`) || !strings.Contains(string(home), `/styles.`) {
t.Error("stylesheet is not fingerprinted")
}
// A second build replaces the first.
if _, err := Run(Options{SiteDir: dir}); err != nil {
t.Fatalf("rebuild: %v", err)
}
}
func TestTemplatesEscape(t *testing.T) {
dir := writeStarter(t)
page := "---\ntitle: \"\"\nsummary: '\">
'\n---\n\n\n\n[x](javascript:alert(3))\n"
if err := os.WriteFile(filepath.Join(dir, "content", "evil.md"), []byte(page), 0o644); err != nil {
t.Fatal(err)
}
res, err := Run(Options{SiteDir: dir})
if err != nil {
t.Fatal(err)
}
out, _ := os.ReadFile(filepath.Join(res.Out, "evil", "index.html"))
for _, bad := range []string{"\n\n"
os.WriteFile(filepath.Join(dir, "content", "raw.md"), []byte(page), 0o644)
// Without trust_html or unsafe_html, an HTML body is refused.
if _, err := Run(Options{SiteDir: dir}); err == nil || !strings.Contains(err.Error(), "trust_html") {
t.Fatalf("HTML body accepted without permission: %v", err)
}
cfgPath := filepath.Join(dir, "site.yaml")
base, _ := os.ReadFile(cfgPath)
// Sanitized: the script goes, the indented paragraph stays a paragraph.
os.WriteFile(cfgPath, append(base, []byte("markdown:\n unsafe_html: true\n")...), 0o644)
res, err := Run(Options{SiteDir: dir})
if err != nil {
t.Fatal(err)
}
out, _ := os.ReadFile(filepath.Join(res.Out, "raw", "index.html"))
if strings.Contains(string(out), "