// Создано: 2026-03-11 // Юнит-тесты для invoke.go — фильтрация hop-by-hop заголовков. // Не требуют k8s, работают с httptest. package handler import ( "io" "net/http" "net/http/httptest" "strings" "testing" ) // TestHopByHopHeaders_FilteredFromResponse проверяет что заголовки управления // TCP-соединением НЕ пробрасываются клиенту из ответа функции. // Transfer-Encoding особенно опасен: его пересылка ломает framing тела ответа. func TestHopByHopHeaders_FilteredFromResponse(t *testing.T) { // Мок-бэкенд — возвращает hop-by-hop и обычный заголовок backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Custom", "keep-me") w.Header().Set("Transfer-Encoding", "chunked") // должен быть отфильтрован w.Header().Set("Connection", "close") // должен быть отфильтрован w.WriteHeader(http.StatusOK) _, _ = io.WriteString(w, `{"ok":true}`) })) defer backend.Close() // Делаем запрос напрямую к бэкенду и применяем нашу логику фильтрации resp, err := http.Get(backend.URL) if err != nil { t.Fatal(err) } defer resp.Body.Close() rec := httptest.NewRecorder() // Воспроизводим логику из InvokeFunction for k, vals := range resp.Header { if hopByHopHeaders[k] { continue } for _, v := range vals { rec.Header().Add(k, v) } } // Обычные заголовки — должны пройти if rec.Header().Get("Content-Type") == "" { t.Error("Content-Type should pass through") } if rec.Header().Get("X-Custom") == "" { t.Error("X-Custom should pass through") } // Hop-by-hop — должны быть отфильтрованы if rec.Header().Get("Transfer-Encoding") != "" { t.Error("Transfer-Encoding must NOT pass through") } if rec.Header().Get("Connection") != "" { t.Error("Connection must NOT pass through") } } // TestHopByHopHeaders_MapContainsAllRFC2616 проверяет что карта содержит // все 8 hop-by-hop заголовков из RFC 2616 §13.5.1. func TestHopByHopHeaders_MapContainsAllRFC2616(t *testing.T) { required := []string{ "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", "Trailers", "Transfer-Encoding", "Upgrade", } for _, h := range required { if !hopByHopHeaders[h] { t.Errorf("hopByHopHeaders missing: %s", h) } } // Content-Type — обычный заголовок, не должен быть в списке if hopByHopHeaders["Content-Type"] { t.Error("Content-Type must NOT be in hopByHopHeaders") } } // TestHopByHopHeaders_CaseCheck проверяет что ключи в карте — с заглавной буквы // (canonical form которую Go http.Header использует внутри). func TestHopByHopHeaders_CaseCheck(t *testing.T) { for k := range hopByHopHeaders { canonical := http.CanonicalHeaderKey(strings.ToLower(k)) if k != canonical { t.Errorf("key %q should be in canonical form %q", k, canonical) } } }