package routing_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "github.com/mathiasbq/supervisor/internal/routing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFetcherGetReturnsPassRate(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method) assert.Equal(t, "/pass-rate", r.URL.Path) assert.Equal(t, "tdd", r.URL.Query().Get("skill")) assert.Equal(t, "7d", r.URL.Query().Get("window")) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{"skill": "tdd", "pass_rate": 0.94}) })) defer srv.Close() f := routing.NewFetcher(srv.URL, "7d", time.Minute) pr, err := f.Get(context.Background(), "tdd") require.NoError(t, err) require.NotNil(t, pr) assert.InDelta(t, 0.94, *pr, 1e-9) } func TestFetcherGetReturnsNilWhenNoData(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{"skill": "novel", "pass_rate": nil}) })) defer srv.Close() f := routing.NewFetcher(srv.URL, "7d", time.Minute) pr, err := f.Get(context.Background(), "novel") require.NoError(t, err) assert.Nil(t, pr) } func TestFetcherCachesWithinTTL(t *testing.T) { var calls int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { atomic.AddInt32(&calls, 1) _ = json.NewEncoder(w).Encode(map[string]any{"pass_rate": 0.5}) })) defer srv.Close() f := routing.NewFetcher(srv.URL, "7d", time.Minute) for i := 0; i < 5; i++ { _, err := f.Get(context.Background(), "tdd") require.NoError(t, err) } assert.Equal(t, int32(1), atomic.LoadInt32(&calls), "should hit upstream once and serve four times from cache") } func TestFetcherFetchesAgainAfterTTLExpires(t *testing.T) { var calls int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { atomic.AddInt32(&calls, 1) _ = json.NewEncoder(w).Encode(map[string]any{"pass_rate": 0.5}) })) defer srv.Close() // Tight TTL so the test stays fast. f := routing.NewFetcher(srv.URL, "7d", 5*time.Millisecond) _, err := f.Get(context.Background(), "tdd") require.NoError(t, err) assert.Equal(t, int32(1), atomic.LoadInt32(&calls)) // Sleep past TTL, then a second Get should hit upstream again. time.Sleep(15 * time.Millisecond) _, err = f.Get(context.Background(), "tdd") require.NoError(t, err) assert.Equal(t, int32(2), atomic.LoadInt32(&calls), "expected fresh upstream call after TTL expiry") } func TestFetcherSurfacesUpstreamError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "boom", http.StatusInternalServerError) })) defer srv.Close() f := routing.NewFetcher(srv.URL, "7d", time.Minute) pr, err := f.Get(context.Background(), "tdd") require.Error(t, err) assert.Nil(t, pr) }