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), "