app_test.go
1package rtw
2
3import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14func TestStaticAssets(t *testing.T) {
15 tmpDir := t.TempDir()
16 cfg := &Config{
17 Server: Server{
18 Host: "127.0.0.1",
19 Port: 8080,
20 LogLevel: INFO,
21 PublicURL: "https://rtw.run",
22 DataDir: tmpDir,
23 },
24 YSweet: YSweet{
25 URL: "ws://127.0.0.1:9000",
26 },
27 Routing: Routing{
28 URL: "http://127.0.0.1:8989",
29 },
30 Geocoder: Geocoder{
31 URL: "http://127.0.0.1:8999",
32 },
33 }
34
35 app, err := NewApp(cfg)
36 if err != nil {
37 t.Fatalf("NewApp failed: %v", err)
38 }
39
40 for _, path := range []string{"/static/favicon.svg", "/static/404.jpg"} {
41 req := httptest.NewRequest("GET", path, nil)
42 rr := httptest.NewRecorder()
43 app.Router.ServeHTTP(rr, req)
44 if rr.Code != 200 {
45 t.Errorf("expected 200 for %s, got %d", path, rr.Code)
46 }
47 }
48}
49
50func TestStaticAssetsContent(t *testing.T) {
51 tmpDir := t.TempDir()
52 cfg := &Config{
53 Server: Server{
54 Host: "127.0.0.1",
55 Port: 8080,
56 LogLevel: INFO,
57 PublicURL: "https://rtw.run",
58 DataDir: tmpDir,
59 },
60 YSweet: YSweet{
61 URL: "ws://127.0.0.1:9000",
62 },
63 Routing: Routing{
64 URL: "http://127.0.0.1:8989",
65 },
66 Geocoder: Geocoder{
67 URL: "http://127.0.0.1:8999",
68 },
69 }
70
71 app, err := NewApp(cfg)
72 if err != nil {
73 t.Fatalf("NewApp failed: %v", err)
74 }
75
76 req := httptest.NewRequest("GET", "/static/favicon.svg", nil)
77 rr := httptest.NewRecorder()
78 app.Router.ServeHTTP(rr, req)
79
80 if rr.Code != 200 {
81 t.Fatalf("expected 200, got %d", rr.Code)
82 }
83 if rr.Body.Len() == 0 {
84 t.Error("expected non-empty body for favicon.svg")
85 }
86 if rr.Header().Get("Content-Type") == "" {
87 t.Error("expected Content-Type header")
88 }
89}
90
91func TestHealthHandler_WithWaldo(t *testing.T) {
92 waldoResp := map[string]interface{}{
93 "status": "healthy",
94 "version": "abc123",
95 "uptime": "1h0m0s",
96 "uptime_seconds": 3600.0,
97 }
98
99 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
100 if r.URL.Path != "/health" {
101 t.Errorf("expected /health, got %s", r.URL.Path)
102 }
103 w.Header().Set("Content-Type", "application/json")
104 w.WriteHeader(http.StatusOK)
105 json.NewEncoder(w).Encode(waldoResp)
106 }))
107 defer upstream.Close()
108
109 cfg := &Config{
110 Geocoder: Geocoder{URL: upstream.URL},
111 Routing: Routing{URL: ""},
112 YSweet: YSweet{URL: ""},
113 }
114 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
115
116 req := httptest.NewRequest(http.MethodGet, "/health", nil)
117 rr := httptest.NewRecorder()
118 handler.ServeHTTP(rr, req)
119
120 require.Equal(t, http.StatusOK, rr.Code)
121
122 var response map[string]interface{}
123 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
124
125 assert.Equal(t, "ok", response["status"])
126 assert.Equal(t, "test", response["vcs.revision"])
127
128 waldoField, ok := response["waldo"].(map[string]interface{})
129 require.True(t, ok)
130 assert.Equal(t, "healthy", waldoField["status"])
131 assert.Equal(t, "abc123", waldoField["version"])
132}
133
134func TestHealthHandler_WaldoUnreachable(t *testing.T) {
135 cfg := &Config{
136 Geocoder: Geocoder{URL: "http://127.0.0.1:1"},
137 Routing: Routing{URL: ""},
138 YSweet: YSweet{URL: ""},
139 }
140 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
141
142 req := httptest.NewRequest(http.MethodGet, "/health", nil)
143 rr := httptest.NewRecorder()
144 handler.ServeHTTP(rr, req)
145
146 require.Equal(t, http.StatusOK, rr.Code)
147
148 var response map[string]interface{}
149 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
150
151 assert.Equal(t, "ok", response["status"])
152
153 waldoField, ok := response["waldo"].(map[string]interface{})
154 require.True(t, ok)
155 assert.Equal(t, "unreachable", waldoField["status"])
156 assert.NotEmpty(t, waldoField["error"])
157}
158
159func TestHealthHandler_WaldoBadJSON(t *testing.T) {
160 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
161 w.Header().Set("Content-Type", "text/plain")
162 w.WriteHeader(http.StatusOK)
163 _, writeErr := w.Write([]byte("not json"))
164 assert.NoError(t, writeErr)
165 }))
166 defer upstream.Close()
167
168 cfg := &Config{
169 Geocoder: Geocoder{URL: upstream.URL},
170 Routing: Routing{URL: ""},
171 YSweet: YSweet{URL: ""},
172 }
173 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
174
175 req := httptest.NewRequest(http.MethodGet, "/health", nil)
176 rr := httptest.NewRecorder()
177 handler.ServeHTTP(rr, req)
178
179 require.Equal(t, http.StatusOK, rr.Code)
180
181 var response map[string]interface{}
182 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
183
184 waldoField, ok := response["waldo"].(map[string]interface{})
185 require.True(t, ok)
186 assert.Equal(t, "error", waldoField["status"])
187 assert.NotEmpty(t, waldoField["error"])
188}
189
190func TestHealthHandler_WaldoNotConfigured(t *testing.T) {
191 cfg := &Config{
192 Geocoder: Geocoder{URL: ""},
193 Routing: Routing{URL: ""},
194 YSweet: YSweet{URL: ""},
195 }
196 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
197
198 req := httptest.NewRequest(http.MethodGet, "/health", nil)
199 rr := httptest.NewRecorder()
200 handler.ServeHTTP(rr, req)
201
202 require.Equal(t, http.StatusOK, rr.Code)
203
204 var response map[string]interface{}
205 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
206
207 assert.Equal(t, "ok", response["status"])
208
209 waldoField, ok := response["waldo"].(map[string]interface{})
210 require.True(t, ok)
211 assert.Equal(t, "not_configured", waldoField["status"])
212}
213
214func TestHealthHandler_WithRouting(t *testing.T) {
215 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
216 if r.URL.Path != "/health" {
217 t.Errorf("expected /health, got %s", r.URL.Path)
218 }
219 w.Header().Set("Content-Type", "text/plain")
220 w.WriteHeader(http.StatusOK)
221 _, _ = w.Write([]byte("OK"))
222 }))
223 defer upstream.Close()
224
225 cfg := &Config{
226 Geocoder: Geocoder{URL: ""},
227 Routing: Routing{URL: upstream.URL},
228 YSweet: YSweet{URL: ""},
229 }
230 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
231
232 req := httptest.NewRequest(http.MethodGet, "/health", nil)
233 rr := httptest.NewRecorder()
234 handler.ServeHTTP(rr, req)
235
236 require.Equal(t, http.StatusOK, rr.Code)
237
238 var response map[string]interface{}
239 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
240
241 assert.Equal(t, "ok", response["status"])
242 assert.Equal(t, "OK", response["routing"])
243}
244
245func TestHealthHandler_WithDocuments(t *testing.T) {
246 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
247 if r.URL.Path != "/ready" {
248 t.Errorf("expected /ready, got %s", r.URL.Path)
249 }
250 w.Header().Set("Content-Type", "application/json")
251 w.WriteHeader(http.StatusOK)
252 _ = json.NewEncoder(w).Encode(map[string]bool{"ok": true})
253 }))
254 defer upstream.Close()
255
256 cfg := &Config{
257 Geocoder: Geocoder{URL: ""},
258 Routing: Routing{URL: ""},
259 YSweet: YSweet{URL: upstream.URL},
260 }
261 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
262
263 req := httptest.NewRequest(http.MethodGet, "/health", nil)
264 rr := httptest.NewRecorder()
265 handler.ServeHTTP(rr, req)
266
267 require.Equal(t, http.StatusOK, rr.Code)
268
269 var response map[string]interface{}
270 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
271
272 assert.Equal(t, "ok", response["status"])
273 assert.Equal(t, "OK", response["documents"])
274}
275
276func TestHealthHandler_RoutingUnreachable(t *testing.T) {
277 cfg := &Config{
278 Geocoder: Geocoder{URL: ""},
279 Routing: Routing{URL: "http://127.0.0.1:1"},
280 YSweet: YSweet{URL: ""},
281 }
282 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
283
284 req := httptest.NewRequest(http.MethodGet, "/health", nil)
285 rr := httptest.NewRecorder()
286 handler.ServeHTTP(rr, req)
287
288 require.Equal(t, http.StatusOK, rr.Code)
289
290 var response map[string]interface{}
291 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
292
293 assert.Equal(t, "unreachable", response["routing"])
294}
295
296func TestHealthHandler_DocumentsUnreachable(t *testing.T) {
297 cfg := &Config{
298 Geocoder: Geocoder{URL: ""},
299 Routing: Routing{URL: ""},
300 YSweet: YSweet{URL: "http://127.0.0.1:1"},
301 }
302 handler := healthHandler(time.Now(), map[string]string{"vcs.revision": "test"}, cfg)
303
304 req := httptest.NewRequest(http.MethodGet, "/health", nil)
305 rr := httptest.NewRecorder()
306 handler.ServeHTTP(rr, req)
307
308 require.Equal(t, http.StatusOK, rr.Code)
309
310 var response map[string]interface{}
311 require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
312
313 assert.Equal(t, "unreachable", response["documents"])
314}
315
316func TestAllServicesHealthy(t *testing.T) {
317 waldo := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
318 _ = json.NewEncoder(w).Encode(map[string]interface{}{"status": "healthy"})
319 }))
320 defer waldo.Close()
321
322 routing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
323 w.WriteHeader(http.StatusOK)
324 _, _ = w.Write([]byte("OK"))
325 }))
326 defer routing.Close()
327
328 docs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
329 _ = json.NewEncoder(w).Encode(map[string]bool{"ok": true})
330 }))
331 defer docs.Close()
332
333 cfg := &Config{
334 Geocoder: Geocoder{URL: waldo.URL},
335 Routing: Routing{URL: routing.URL},
336 YSweet: YSweet{URL: docs.URL},
337 }
338
339 assert.True(t, AllServicesHealthy(cfg))
340}
341
342func TestAllServicesHealthy_RoutingDown(t *testing.T) {
343 waldo := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
344 _ = json.NewEncoder(w).Encode(map[string]interface{}{"status": "healthy"})
345 }))
346 defer waldo.Close()
347
348 docs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
349 _ = json.NewEncoder(w).Encode(map[string]bool{"ok": true})
350 }))
351 defer docs.Close()
352
353 cfg := &Config{
354 Geocoder: Geocoder{URL: waldo.URL},
355 Routing: Routing{URL: "http://127.0.0.1:1"},
356 YSweet: YSweet{URL: docs.URL},
357 }
358
359 assert.False(t, AllServicesHealthy(cfg))
360}