package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
func newTestServer(t *testing.T, files map[string]string) (*httptest.Server, string) {
t.Helper()
dir := t.TempDir()
for name, src := range files {
writeFile(t, dir, name, src)
}
a, err := newApp(dir, "https://posts.example.com/")
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(a.routes())
t.Cleanup(srv.Close)
return srv, dir
}
func writeFile(t *testing.T, dir, name, src string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
}
func get(t *testing.T, srv *httptest.Server, path string) (int, http.Header, string) {
t.Helper()
resp, err := http.Get(srv.URL + path)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
return resp.StatusCode, resp.Header, string(body)
}
var sample = map[string]string{
"2026-01-01-oldest.md": "# Oldest\n",
"2026-02-01-middle.md": "---\ntitle: Middle\nsummary: The middle one.\n---\nMiddle body\n",
"2026-03-01-newest.md": "# Newest\n",
"2026-04-01-draft.md": "---\ndraft: true\n---\n# Secret draft\n",
"Bad Name.md": "# Broken\n",
"notes.txt": "not a post",
}
func TestIndex(t *testing.T) {
srv, _ := newTestServer(t, sample)
status, h, body := get(t, srv, "/")
if status != 200 {
t.Fatalf("status = %d", status)
}
if h.Get("Content-Security-Policy") == "" || h.Get("X-Content-Type-Options") != "nosniff" {
t.Errorf("missing security headers: %v", h)
}
n, m, o := strings.Index(body, "Newest"), strings.Index(body, "Middle"), strings.Index(body, "Oldest")
if n < 0 || m < 0 || o < 0 || !(n < m && m < o) {
t.Errorf("posts missing or out of order:\n%s", body)
}
if !strings.Contains(body, "The middle one.") {
t.Error("summary not shown")
}
for _, hidden := range []string{"Secret draft", "Broken"} {
if strings.Contains(body, hidden) {
t.Errorf("%q should not be listed", hidden)
}
}
}
func TestEmptyIndex(t *testing.T) {
srv, _ := newTestServer(t, nil)
if status, _, body := get(t, srv, "/"); status != 200 || !strings.Contains(body, "No posts yet") {
t.Errorf("status = %d, body:\n%s", status, body)
}
}
func TestPostPage(t *testing.T) {
srv, _ := newTestServer(t, sample)
status, _, body := get(t, srv, "/posts/middle")
if status != 200 {
t.Fatalf("status = %d", status)
}
for _, want := range []string{
"
Middle · posts.example.com",
"
Middle body
",
`← Oldest`,
`Newest →`,
} {
if !strings.Contains(body, want) {
t.Errorf("missing %q in:\n%s", want, body)
}
}
// Previous (older) on the left, next (newer) on the right.
if strings.Index(body, `class="older"`) > strings.Index(body, `class="newer"`) {
t.Error("older link should come before the newer link")
}
_, _, newest := get(t, srv, "/posts/newest")
if strings.Contains(newest, `class="newer"`) {
t.Error("newest post should have no newer link")
}
_, _, oldest := get(t, srv, "/posts/oldest")
if strings.Contains(oldest, `class="older"`) {
t.Error("oldest post should have no older link")
}
}
func TestNotFound(t *testing.T) {
srv, _ := newTestServer(t, sample)
for _, path := range []string{
"/posts/nope",
"/posts/draft",
"/posts/2026-01-01-oldest",
"/posts/..%2f..%2fetc%2fpasswd",
"/posts/",
"/static/",
"/static/missing.css",
"/whatever",
} {
status, _, body := get(t, srv, path)
if status != 404 {
t.Errorf("%s: status = %d, want 404", path, status)
}
if path != "/static/missing.css" && !strings.Contains(body, "