package server import ( "context" "net/http" "net/http/httptest" "sync/atomic" "testing" "git.eeqj.de/sneak/routewatch/internal/database" "git.eeqj.de/sneak/routewatch/internal/logger" "git.eeqj.de/sneak/routewatch/internal/metrics" "git.eeqj.de/sneak/routewatch/internal/streamer" ) // countingStatsDB embeds database.Store (left nil) and overrides only // GetStatsContext, counting how many times it runs. The stats handlers read // their database statistics through the cache, which calls this; every other // Store method is unused on the stats path and would panic if called. type countingStatsDB struct { database.Store calls *atomic.Int64 } func (d countingStatsDB) GetStatsContext(_ context.Context) (database.Stats, error) { d.calls.Add(1) return database.Stats{}, nil } // TestStatsHandlersServeFromCache drives both stats handlers many times and // checks that they answer 200 while the database statistics are computed at most // once within the refresh interval. Before the fix each request ran the counts // and MIN/MAX scans itself, which took the full timeout and returned 500 once // the database grew large. func TestStatsHandlersServeFromCache(t *testing.T) { var calls atomic.Int64 db := countingStatsDB{calls: &calls} s := New(db, streamer.New(logger.New(), metrics.New()), logger.New()) handlers := []http.HandlerFunc{s.handleStatusJSON(), s.handleStats()} const iterations = 20 for _, handler := range handlers { for range iterations { req := httptest.NewRequest(http.MethodGet, "/", nil) rec := httptest.NewRecorder() handler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("handler returned %d, want %d", rec.Code, http.StatusOK) } } } if got := calls.Load(); got != 1 { t.Fatalf("GetStatsContext ran %d times, want 1 within the interval", got) } }