BT  ·  2026-06-22

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}