Go · 4029 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package handlers
4
5 import (
6 "io"
7 "log/slog"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12 )
13
14 func TestHandlers(t *testing.T) {
15 t.Parallel()
16
17 mux := http.NewServeMux()
18 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
19 if err := Register(mux, Deps{
20 Logger: logger,
21 TemplatesFS: testTemplatesFS(t),
22 StaticFS: testStaticFS(t),
23 LogoSVG: `<svg xmlns="http://www.w3.org/2000/svg"><title>shithub</title></svg>`,
24 }); err != nil {
25 t.Fatalf("Register: %v", err)
26 }
27
28 tests := []struct {
29 name string
30 path string
31 wantStatus int
32 wantBodyAny []string
33 wantHeader map[string]string
34 }{
35 {
36 name: "hello page",
37 path: "/",
38 wantStatus: http.StatusOK,
39 wantBodyAny: []string{"shithub", "GitHub. Open source. Without Copilot.", "Sprint 00", `<meta name="description"`, `<link rel="canonical"`},
40 wantHeader: map[string]string{"Content-Type": "text/html; charset=utf-8"},
41 },
42 {
43 name: "about page",
44 path: "/about",
45 wantStatus: http.StatusOK,
46 wantBodyAny: []string{"No hard feelings to GitHub", "AI training on my code", `<meta name="description"`},
47 wantHeader: map[string]string{"Content-Type": "text/html; charset=utf-8"},
48 },
49 {
50 name: "robots",
51 path: "/robots.txt",
52 wantStatus: http.StatusOK,
53 wantBodyAny: []string{"User-agent: *", "Allow: /", "Disallow: /admin", "Sitemap: http://example.com/sitemap.xml"},
54 wantHeader: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
55 },
56 {
57 name: "sitemap",
58 path: "/sitemap.xml",
59 wantStatus: http.StatusOK,
60 wantBodyAny: []string{`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`, "<loc>http://example.com/</loc>", "<loc>http://example.com/about</loc>"},
61 wantHeader: map[string]string{"Content-Type": "application/xml; charset=utf-8"},
62 },
63 {
64 name: "healthz",
65 path: "/healthz",
66 wantStatus: http.StatusOK,
67 wantBodyAny: []string{"ok"},
68 },
69 {
70 name: "readyz",
71 path: "/readyz",
72 wantStatus: http.StatusOK,
73 wantBodyAny: []string{"ready"},
74 },
75 {
76 name: "logo svg",
77 path: "/static/logo/shithub.svg",
78 wantStatus: http.StatusOK,
79 wantBodyAny: []string{"<svg", "shithub"},
80 },
81 {
82 name: "unknown route 404",
83 path: "/this-path-does-not-exist",
84 wantStatus: http.StatusNotFound,
85 },
86 }
87
88 for _, tc := range tests {
89 t.Run(tc.name, func(t *testing.T) {
90 t.Parallel()
91
92 req := httptest.NewRequest(http.MethodGet, tc.path, nil)
93 rec := httptest.NewRecorder()
94 mux.ServeHTTP(rec, req)
95
96 if rec.Code != tc.wantStatus {
97 t.Fatalf("status: got %d, want %d (body=%q)", rec.Code, tc.wantStatus, rec.Body.String())
98 }
99 body := rec.Body.String()
100 for _, want := range tc.wantBodyAny {
101 if !strings.Contains(body, want) {
102 t.Errorf("body missing %q\nbody=%q", want, body)
103 }
104 }
105 for k, want := range tc.wantHeader {
106 if got := rec.Header().Get(k); got != want {
107 t.Errorf("header %s: got %q, want %q", k, got, want)
108 }
109 }
110 })
111 }
112 }
113
114 // TestHealthzHEAD pins SR2 L8: HEAD /healthz must return 200, not
115 // 405. Strict probes (some k8s livenessProbes, certain monitoring
116 // tools) issue HEAD-only requests; chi only registers the methods
117 // you ask for, so the GET-only registration would 405 HEAD probes.
118 func TestHealthzHEAD(t *testing.T) {
119 t.Parallel()
120
121 mux := http.NewServeMux()
122 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
123 if err := Register(mux, Deps{
124 Logger: logger,
125 TemplatesFS: testTemplatesFS(t),
126 StaticFS: testStaticFS(t),
127 LogoSVG: `<svg xmlns="http://www.w3.org/2000/svg"><title>shithub</title></svg>`,
128 }); err != nil {
129 t.Fatalf("Register: %v", err)
130 }
131 req := httptest.NewRequest(http.MethodHead, "/healthz", nil)
132 rec := httptest.NewRecorder()
133 mux.ServeHTTP(rec, req)
134 if rec.Code != http.StatusOK {
135 t.Fatalf("HEAD /healthz: status %d, want 200", rec.Code)
136 }
137 }
138