Go · 7353 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 "github.com/go-chi/chi/v5"
14 )
15
16 func TestHandlers(t *testing.T) {
17 t.Parallel()
18
19 mux := http.NewServeMux()
20 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
21 if err := Register(mux, Deps{
22 Logger: logger,
23 TemplatesFS: testTemplatesFS(t),
24 StaticFS: testStaticFS(t),
25 LogoSVG: `<svg xmlns="http://www.w3.org/2000/svg"><title>shithub</title></svg>`,
26 }); err != nil {
27 t.Fatalf("Register: %v", err)
28 }
29
30 tests := []struct {
31 name string
32 path string
33 wantStatus int
34 wantBodyAny []string
35 wantHeader map[string]string
36 }{
37 {
38 name: "hello page",
39 path: "/",
40 wantStatus: http.StatusOK,
41 wantBodyAny: []string{"shithub", "GitHub. Open source. Without Copilot.", "Sprint 00", `<meta name="description"`, `<link rel="canonical"`},
42 wantHeader: map[string]string{"Content-Type": "text/html; charset=utf-8"},
43 },
44 {
45 name: "about page",
46 path: "/about",
47 wantStatus: http.StatusOK,
48 wantBodyAny: []string{"No hard feelings to GitHub", "AI training on my code", `<meta name="description"`},
49 wantHeader: map[string]string{"Content-Type": "text/html; charset=utf-8"},
50 },
51 {
52 name: "robots",
53 path: "/robots.txt",
54 wantStatus: http.StatusOK,
55 wantBodyAny: []string{"User-agent: *", "Allow: /", "Disallow: /admin", "Sitemap: http://example.com/sitemap.xml"},
56 wantHeader: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
57 },
58 {
59 name: "sitemap",
60 path: "/sitemap.xml",
61 wantStatus: http.StatusOK,
62 wantBodyAny: []string{`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`, "<loc>http://example.com/</loc>", "<loc>http://example.com/about</loc>"},
63 wantHeader: map[string]string{"Content-Type": "application/xml; charset=utf-8"},
64 },
65 {
66 name: "healthz",
67 path: "/healthz",
68 wantStatus: http.StatusOK,
69 wantBodyAny: []string{"ok"},
70 },
71 {
72 name: "readyz",
73 path: "/readyz",
74 wantStatus: http.StatusOK,
75 wantBodyAny: []string{"ready"},
76 },
77 {
78 name: "logo svg",
79 path: "/static/logo/shithub.svg",
80 wantStatus: http.StatusOK,
81 wantBodyAny: []string{"<svg", "shithub"},
82 },
83 {
84 name: "unknown route 404",
85 path: "/this-path-does-not-exist",
86 wantStatus: http.StatusNotFound,
87 },
88 }
89
90 for _, tc := range tests {
91 t.Run(tc.name, func(t *testing.T) {
92 t.Parallel()
93
94 req := httptest.NewRequest(http.MethodGet, tc.path, nil)
95 rec := httptest.NewRecorder()
96 mux.ServeHTTP(rec, req)
97
98 if rec.Code != tc.wantStatus {
99 t.Fatalf("status: got %d, want %d (body=%q)", rec.Code, tc.wantStatus, rec.Body.String())
100 }
101 body := rec.Body.String()
102 for _, want := range tc.wantBodyAny {
103 if !strings.Contains(body, want) {
104 t.Errorf("body missing %q\nbody=%q", want, body)
105 }
106 }
107 for k, want := range tc.wantHeader {
108 if got := rec.Header().Get(k); got != want {
109 t.Errorf("header %s: got %q, want %q", k, got, want)
110 }
111 }
112 })
113 }
114 }
115
116 // TestHealthzHEAD pins SR2 L8: HEAD /healthz must return 200, not
117 // 405. Strict probes (some k8s livenessProbes, certain monitoring
118 // tools) issue HEAD-only requests; chi only registers the methods
119 // you ask for, so the GET-only registration would 405 HEAD probes.
120 func TestHealthzHEAD(t *testing.T) {
121 t.Parallel()
122
123 mux := http.NewServeMux()
124 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
125 if err := Register(mux, Deps{
126 Logger: logger,
127 TemplatesFS: testTemplatesFS(t),
128 StaticFS: testStaticFS(t),
129 LogoSVG: `<svg xmlns="http://www.w3.org/2000/svg"><title>shithub</title></svg>`,
130 }); err != nil {
131 t.Fatalf("Register: %v", err)
132 }
133 req := httptest.NewRequest(http.MethodHead, "/healthz", nil)
134 rec := httptest.NewRecorder()
135 mux.ServeHTTP(rec, req)
136 if rec.Code != http.StatusOK {
137 t.Fatalf("HEAD /healthz: status %d, want 200", rec.Code)
138 }
139 }
140
141 func TestActionsLogStreamRouteBypassesCompressAndTimeout(t *testing.T) {
142 t.Parallel()
143
144 mux := http.NewServeMux()
145 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
146 if err := Register(mux, Deps{
147 Logger: logger,
148 TemplatesFS: testTemplatesFS(t),
149 StaticFS: testStaticFS(t),
150 LogoSVG: `<svg xmlns="http://www.w3.org/2000/svg"><title>shithub</title></svg>`,
151 RepoActionsStreamMounter: func(r chi.Router) {
152 r.Get("/{owner}/{repo}/actions/runs/{runIndex}/jobs/{jobIndex}/steps/{stepIndex}/log/stream", func(w http.ResponseWriter, r *http.Request) {
153 w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
154 if _, ok := r.Context().Deadline(); ok {
155 _, _ = io.WriteString(w, "deadline")
156 return
157 }
158 _, _ = io.WriteString(w, "no-deadline")
159 })
160 },
161 }); err != nil {
162 t.Fatalf("Register: %v", err)
163 }
164
165 req := httptest.NewRequest(http.MethodGet, "/octo/demo/actions/runs/1/jobs/0/steps/0/log/stream", nil)
166 req.Header.Set("Accept-Encoding", "gzip")
167 rec := httptest.NewRecorder()
168 mux.ServeHTTP(rec, req)
169
170 if rec.Code != http.StatusOK {
171 t.Fatalf("status: got %d, want %d body=%q", rec.Code, http.StatusOK, rec.Body.String())
172 }
173 if got := rec.Header().Get("Content-Encoding"); got != "" {
174 t.Fatalf("Content-Encoding: got %q, want empty", got)
175 }
176 if got := rec.Body.String(); got != "no-deadline" {
177 t.Fatalf("body: got %q, want no-deadline", got)
178 }
179 }
180
181 func TestActionsManagementRoutesStayBeforeProfileCatchAll(t *testing.T) {
182 t.Parallel()
183
184 mux := http.NewServeMux()
185 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
186 if err := Register(mux, Deps{
187 Logger: logger,
188 TemplatesFS: testTemplatesFS(t),
189 StaticFS: testStaticFS(t),
190 LogoSVG: `<svg xmlns="http://www.w3.org/2000/svg"><title>shithub</title></svg>`,
191 RepoHomeMounter: func(r chi.Router) {
192 r.Get("/{owner}/{repo}/actions/caches", writeRouteName("actions:caches"))
193 r.Get("/{owner}/{repo}/actions/attestations", writeRouteName("actions:attestations"))
194 r.Get("/{owner}/{repo}/actions/runners", writeRouteName("actions:runners"))
195 r.Get("/{owner}/{repo}/actions/metrics/usage", writeRouteName("actions:usage"))
196 r.Get("/{owner}/{repo}/actions/metrics/performance", writeRouteName("actions:performance"))
197 },
198 ProfileMounter: func(r chi.Router) {
199 r.Get("/{username}", writeRouteName("profile"))
200 },
201 }); err != nil {
202 t.Fatalf("Register: %v", err)
203 }
204
205 tests := map[string]string{
206 "/octo/demo/actions/caches": "actions:caches",
207 "/octo/demo/actions/attestations": "actions:attestations",
208 "/octo/demo/actions/runners": "actions:runners",
209 "/octo/demo/actions/metrics/usage": "actions:usage",
210 "/octo/demo/actions/metrics/performance": "actions:performance",
211 }
212 for path, want := range tests {
213 req := httptest.NewRequest(http.MethodGet, path, nil)
214 rec := httptest.NewRecorder()
215 mux.ServeHTTP(rec, req)
216 if rec.Code != http.StatusOK {
217 t.Fatalf("%s: status got %d want 200 body=%q", path, rec.Code, rec.Body.String())
218 }
219 if got := rec.Body.String(); got != want {
220 t.Fatalf("%s: body got %q want %q", path, got, want)
221 }
222 }
223 }
224
225 func writeRouteName(name string) http.HandlerFunc {
226 return func(w http.ResponseWriter, _ *http.Request) {
227 _, _ = io.WriteString(w, name)
228 }
229 }
230