package server import ( "context" "net/http" "net/http/httptest" "strings" "testing" "dance-lessons-coach/pkg/config" "github.com/stretchr/testify/assert" ) // TestV2EnabledGate_BlocksWhenDisabled verifies the ADR-0023 Phase 4 // hot-reload security property: when api.v2_enabled is false, ANY request // to /api/v2/* returns 404 with a JSON body, not a 200, not a panic. func TestV2EnabledGate_BlocksWhenDisabled(t *testing.T) { cfg := &config.Config{} cfg.API.V2Enabled = false // explicit, even though it is the zero value s := NewServer(cfg, context.Background()) req := httptest.NewRequest(http.MethodPost, "/api/v2/greet", strings.NewReader(`{"name":"world"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code, "v2 disabled should 404") assert.Contains(t, w.Body.String(), "v2 API is currently disabled", "response should explain why") assert.Equal(t, "application/json", w.Header().Get("Content-Type")) } // TestV2EnabledGate_PassesWhenEnabled verifies the gate lets requests // through to the actual v2 handler when api.v2_enabled is true. We use // a v2 endpoint that exists and responds with a 2xx so we can assert // "got past the gate, hit the handler". func TestV2EnabledGate_PassesWhenEnabled(t *testing.T) { cfg := &config.Config{} cfg.API.V2Enabled = true s := NewServer(cfg, context.Background()) req := httptest.NewRequest(http.MethodPost, "/api/v2/greet", strings.NewReader(`{"name":"world"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.router.ServeHTTP(w, req) // 200 = v2 handler executed. Anything other than 404 with the gate's // message proves the gate let the request through. assert.NotEqual(t, http.StatusNotFound, w.Code, "v2 enabled should not return 404 from gate") assert.NotContains(t, w.Body.String(), "v2 API is currently disabled", "gate message must NOT appear when enabled") } // TestV2EnabledGate_HotReloadEffect simulates the ADR-0023 Phase 4 // scenario: the same Server (same router) sees opposite responses // before and after a config flip — proving the gate reads the live // config rather than a snapshot from setupRoutes. func TestV2EnabledGate_HotReloadEffect(t *testing.T) { cfg := &config.Config{} cfg.API.V2Enabled = false s := NewServer(cfg, context.Background()) // Round 1: disabled req1 := httptest.NewRequest(http.MethodPost, "/api/v2/greet", strings.NewReader(`{"name":"a"}`)) req1.Header.Set("Content-Type", "application/json") w1 := httptest.NewRecorder() s.router.ServeHTTP(w1, req1) assert.Equal(t, http.StatusNotFound, w1.Code, "round 1 (disabled) should 404") // Flip the config. In production, Config.WatchAndApply does this on // file change; here we set the field directly to simulate the result. cfg.API.V2Enabled = true // Round 2: enabled — same Server, same router, just the config flipped req2 := httptest.NewRequest(http.MethodPost, "/api/v2/greet", strings.NewReader(`{"name":"b"}`)) req2.Header.Set("Content-Type", "application/json") w2 := httptest.NewRecorder() s.router.ServeHTTP(w2, req2) assert.NotEqual(t, http.StatusNotFound, w2.Code, "round 2 (enabled) should NOT 404") assert.NotContains(t, w2.Body.String(), "v2 API is currently disabled") }