| 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 |