Files
dance-lessons-coach/pkg/server/v2_gate_test.go
Gabriel Radureau de5b599455
Some checks failed
CI/CD Pipeline / Build Docker Cache (push) Successful in 9s
CI/CD Pipeline / Trigger Docker Push (push) Has been cancelled
CI/CD Pipeline / CI Pipeline (push) Has been cancelled
feat(server): api.v2_enabled hot-reload via middleware gate (ADR-0023 Phase 4) (#56)
Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
2026-05-05 10:35:03 +02:00

85 lines
3.3 KiB
Go

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")
}