main_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package main
  2. import (
  3. "encoding/xml"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "testing"
  11. )
  12. func newTestServer(t *testing.T, files map[string]string) (*httptest.Server, string) {
  13. t.Helper()
  14. dir := t.TempDir()
  15. for name, src := range files {
  16. writeFile(t, dir, name, src)
  17. }
  18. a, err := newApp(dir, "https://posts.example.com/")
  19. if err != nil {
  20. t.Fatal(err)
  21. }
  22. srv := httptest.NewServer(a.routes())
  23. t.Cleanup(srv.Close)
  24. return srv, dir
  25. }
  26. func writeFile(t *testing.T, dir, name, src string) {
  27. t.Helper()
  28. if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil {
  29. t.Fatal(err)
  30. }
  31. }
  32. func get(t *testing.T, srv *httptest.Server, path string) (int, http.Header, string) {
  33. t.Helper()
  34. resp, err := http.Get(srv.URL + path)
  35. if err != nil {
  36. t.Fatal(err)
  37. }
  38. defer resp.Body.Close()
  39. body, err := io.ReadAll(resp.Body)
  40. if err != nil {
  41. t.Fatal(err)
  42. }
  43. return resp.StatusCode, resp.Header, string(body)
  44. }
  45. var sample = map[string]string{
  46. "2026-01-01-oldest.md": "# Oldest\n",
  47. "2026-02-01-middle.md": "---\ntitle: Middle\nsummary: The middle one.\n---\nMiddle body\n",
  48. "2026-03-01-newest.md": "# Newest\n",
  49. "2026-04-01-draft.md": "---\ndraft: true\n---\n# Secret draft\n",
  50. "Bad Name.md": "# Broken\n",
  51. "notes.txt": "not a post",
  52. }
  53. func TestIndex(t *testing.T) {
  54. srv, _ := newTestServer(t, sample)
  55. status, h, body := get(t, srv, "/")
  56. if status != 200 {
  57. t.Fatalf("status = %d", status)
  58. }
  59. if h.Get("Content-Security-Policy") == "" || h.Get("X-Content-Type-Options") != "nosniff" {
  60. t.Errorf("missing security headers: %v", h)
  61. }
  62. n, m, o := strings.Index(body, "Newest"), strings.Index(body, "Middle"), strings.Index(body, "Oldest")
  63. if n < 0 || m < 0 || o < 0 || !(n < m && m < o) {
  64. t.Errorf("posts missing or out of order:\n%s", body)
  65. }
  66. if !strings.Contains(body, "The middle one.") {
  67. t.Error("summary not shown")
  68. }
  69. for _, hidden := range []string{"Secret draft", "Broken"} {
  70. if strings.Contains(body, hidden) {
  71. t.Errorf("%q should not be listed", hidden)
  72. }
  73. }
  74. }
  75. func TestEmptyIndex(t *testing.T) {
  76. srv, _ := newTestServer(t, nil)
  77. if status, _, body := get(t, srv, "/"); status != 200 || !strings.Contains(body, "No posts yet") {
  78. t.Errorf("status = %d, body:\n%s", status, body)
  79. }
  80. }
  81. func TestPostPage(t *testing.T) {
  82. srv, _ := newTestServer(t, sample)
  83. status, _, body := get(t, srv, "/posts/middle")
  84. if status != 200 {
  85. t.Fatalf("status = %d", status)
  86. }
  87. for _, want := range []string{
  88. "<title>Middle · posts.example.com</title>",
  89. "<p>Middle body</p>",
  90. `<a class="newer" href="/posts/newest">`,
  91. `<a class="older" href="/posts/oldest">`,
  92. } {
  93. if !strings.Contains(body, want) {
  94. t.Errorf("missing %q in:\n%s", want, body)
  95. }
  96. }
  97. _, _, newest := get(t, srv, "/posts/newest")
  98. if strings.Contains(newest, `class="newer"`) {
  99. t.Error("newest post should have no newer link")
  100. }
  101. _, _, oldest := get(t, srv, "/posts/oldest")
  102. if strings.Contains(oldest, `class="older"`) {
  103. t.Error("oldest post should have no older link")
  104. }
  105. }
  106. func TestNotFound(t *testing.T) {
  107. srv, _ := newTestServer(t, sample)
  108. for _, path := range []string{
  109. "/posts/nope",
  110. "/posts/draft",
  111. "/posts/2026-01-01-oldest",
  112. "/posts/..%2f..%2fetc%2fpasswd",
  113. "/posts/",
  114. "/static/",
  115. "/static/missing.css",
  116. "/whatever",
  117. } {
  118. status, _, body := get(t, srv, path)
  119. if status != 404 {
  120. t.Errorf("%s: status = %d, want 404", path, status)
  121. }
  122. if path != "/static/missing.css" && !strings.Contains(body, "<h1>404</h1>") {
  123. t.Errorf("%s: expected styled 404 page", path)
  124. }
  125. }
  126. }
  127. func TestStatic(t *testing.T) {
  128. srv, _ := newTestServer(t, nil)
  129. status, h, body := get(t, srv, "/static/style.css")
  130. if status != 200 || !strings.HasPrefix(h.Get("Content-Type"), "text/css") || !strings.Contains(body, ":root") {
  131. t.Errorf("status = %d, type = %q", status, h.Get("Content-Type"))
  132. }
  133. }
  134. func TestFeed(t *testing.T) {
  135. srv, _ := newTestServer(t, sample)
  136. status, h, body := get(t, srv, "/feed.xml")
  137. if status != 200 || !strings.HasPrefix(h.Get("Content-Type"), "application/rss+xml") {
  138. t.Fatalf("status = %d, type = %q", status, h.Get("Content-Type"))
  139. }
  140. var feed rss
  141. if err := xml.Unmarshal([]byte(body), &feed); err != nil {
  142. t.Fatalf("invalid XML: %v\n%s", err, body)
  143. }
  144. items := feed.Channel.Items
  145. if len(items) != 3 {
  146. t.Fatalf("got %d items, want 3", len(items))
  147. }
  148. if items[0].Link != "https://posts.example.com/posts/newest" {
  149. t.Errorf("first link = %q", items[0].Link)
  150. }
  151. if items[1].Description != "The middle one." {
  152. t.Errorf("description = %q", items[1].Description)
  153. }
  154. }
  155. func TestReloadsWhenContentChanges(t *testing.T) {
  156. srv, dir := newTestServer(t, map[string]string{"2026-01-01-one.md": "# One\n"})
  157. if _, _, body := get(t, srv, "/"); strings.Contains(body, "Two") {
  158. t.Fatal("unexpected post")
  159. }
  160. writeFile(t, dir, "2026-01-02-two.md", "# Two\n")
  161. if status, _, _ := get(t, srv, "/posts/two"); status != 200 {
  162. t.Errorf("new post not picked up: status = %d", status)
  163. }
  164. if err := os.Remove(filepath.Join(dir, "2026-01-01-one.md")); err != nil {
  165. t.Fatal(err)
  166. }
  167. if status, _, _ := get(t, srv, "/posts/one"); status != 404 {
  168. t.Errorf("removed post still served: status = %d", status)
  169. }
  170. }