| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- package main
- import (
- "encoding/xml"
- "io"
- "net/http"
- "net/http/httptest"
- "os"
- "path/filepath"
- "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{
- "<title>Middle · posts.example.com</title>",
- "<p>Middle body</p>",
- `<a class="newer" href="/posts/newest">`,
- `<a class="older" href="/posts/oldest">`,
- } {
- if !strings.Contains(body, want) {
- t.Errorf("missing %q in:\n%s", want, body)
- }
- }
- _, _, 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, "<h1>404</h1>") {
- t.Errorf("%s: expected styled 404 page", path)
- }
- }
- }
- func TestStatic(t *testing.T) {
- srv, _ := newTestServer(t, nil)
- status, h, body := get(t, srv, "/static/style.css")
- if status != 200 || !strings.HasPrefix(h.Get("Content-Type"), "text/css") || !strings.Contains(body, ":root") {
- t.Errorf("status = %d, type = %q", status, h.Get("Content-Type"))
- }
- }
- func TestFeed(t *testing.T) {
- srv, _ := newTestServer(t, sample)
- status, h, body := get(t, srv, "/feed.xml")
- if status != 200 || !strings.HasPrefix(h.Get("Content-Type"), "application/rss+xml") {
- t.Fatalf("status = %d, type = %q", status, h.Get("Content-Type"))
- }
- var feed rss
- if err := xml.Unmarshal([]byte(body), &feed); err != nil {
- t.Fatalf("invalid XML: %v\n%s", err, body)
- }
- items := feed.Channel.Items
- if len(items) != 3 {
- t.Fatalf("got %d items, want 3", len(items))
- }
- if items[0].Link != "https://posts.example.com/posts/newest" {
- t.Errorf("first link = %q", items[0].Link)
- }
- if items[1].Description != "The middle one." {
- t.Errorf("description = %q", items[1].Description)
- }
- }
- func TestHeaderAndFooter(t *testing.T) {
- srv, dir := newTestServer(t, sample)
- for _, path := range []string{"/", "/posts/middle", "/about", "/nope"} {
- _, _, body := get(t, srv, path)
- for _, want := range []string{
- `<a class="home" href="/">Home</a>`,
- `<nav><a href="/about">About</a></nav>`,
- "<footer class=\"site\">\n <a href=\"/feed.xml\">RSS</a>\n</footer>",
- } {
- if !strings.Contains(body, want) {
- t.Errorf("%s: missing %q", path, want)
- }
- }
- }
- writeFile(t, dir, "site.json", `{"homeText": "All <posts>"}`)
- if _, _, body := get(t, srv, "/"); !strings.Contains(body, `<a class="home" href="/">All <posts></a>`) {
- t.Errorf("configured home text not used (or not escaped):\n%s", body)
- }
- if status, _, _ := get(t, srv, "/posts/site"); status != 404 {
- t.Error("site.json should not be treated as a post")
- }
- }
- func TestAboutPage(t *testing.T) {
- srv, _ := newTestServer(t, sample)
- status, _, body := get(t, srv, "/about")
- if status != 200 {
- t.Fatalf("status = %d", status)
- }
- for _, want := range []string{
- "<title>About · posts.example.com</title>",
- "<dt>Version</dt>\n <dd>" + version,
- "<dt>Posts</dt>\n <dd>3</dd>",
- `<dd><a href="/posts/newest">Newest</a>`,
- `<h2 id="the-header-block">The header block</h2>`,
- `<h2 id="what-markdown-works">What Markdown works</h2>`,
- } {
- if !strings.Contains(body, want) {
- t.Errorf("missing %q in:\n%s", want, body)
- }
- }
- empty, _ := newTestServer(t, nil)
- _, _, body = get(t, empty, "/about")
- if !strings.Contains(body, "<dd>0</dd>") || strings.Contains(body, "Latest post") {
- t.Errorf("empty site: expected 0 posts and no latest post:\n%s", body)
- }
- }
- func TestReloadsWhenContentChanges(t *testing.T) {
- srv, dir := newTestServer(t, map[string]string{"2026-01-01-one.md": "# One\n"})
- if _, _, body := get(t, srv, "/"); strings.Contains(body, "Two") {
- t.Fatal("unexpected post")
- }
- writeFile(t, dir, "2026-01-02-two.md", "# Two\n")
- if status, _, _ := get(t, srv, "/posts/two"); status != 200 {
- t.Errorf("new post not picked up: status = %d", status)
- }
- if err := os.Remove(filepath.Join(dir, "2026-01-01-one.md")); err != nil {
- t.Fatal(err)
- }
- if status, _, _ := get(t, srv, "/posts/one"); status != 404 {
- t.Errorf("removed post still served: status = %d", status)
- }
- }
|