Skip to content

Commit 1c1ac57

Browse files
authored
[test-improver] Improve tests for proxy handler (#8251)
# Test Improvements: `handler_test.go` ## File Analyzed - **Test File**: `internal/proxy/handler_test.go` - **Package**: `internal/proxy` - **Lines Added**: +80 lines (3 new tests + 1 helper type) ## Improvements Made ### 1. Increased Coverage Three previously uncovered code paths in `handler.go` are now exercised: | Function | Before | After | |---|---|---| | `handleUnrecognizedPassthrough` | 0% | **100%** | | `forwardAndReadBody` | partial | **100%** | | `handleWithDIFC` | ~86% | **95.1%** | | **Package total** | **91.4%** | **92.2%** | ### 2. New Tests Added #### `TestForwardAndReadBody_BodyReadError` - Covers lines 432–435 of `handler.go`: the `io.ReadAll` error path inside `forwardAndReadBody` - Uses a new `bodyErrorTransport` (custom `http.RoundTripper`) that returns a valid HTTP response whose body is an `io.Pipe` immediately closed with an error - Verifies the handler writes a `502 bad_gateway` / `"failed to read upstream response"` JSON error #### `TestHandleUnrecognizedPassthrough_UpstreamFails` - Covers lines 147–149 of `handler.go`: the `if resp == nil { return }` early exit in `handleUnrecognizedPassthrough` - Points the proxy at `(127.0.0.1/redacted) (unreachable) with an unrecognized path `/v1/unknown/endpoint` - Verifies the handler writes a `502` error and returns without panicking on nil response #### `TestHandleWithDIFC_RateLimitDetected` - Covers lines 230–237 of `handler.go`: the rate-limit span annotation block inside `handleWithDIFC` - Upstream returns `429 Too Many Requests` with `X-Ratelimit-Remaining: 0` and a valid `X-Ratelimit-Reset` Unix timestamp - Exercises both the outer `rateLimited` branch and the inner `!resetAt.IsZero()` branch - Verifies the `429` is passed through and `Retry-After` is injected by `writeResponse` ### 3. Helper Type Added `bodyErrorTransport` — a minimal `http.RoundTripper` that synthesises a response with a failing body reader (via `io.Pipe`). Clean, no network I/O required, no TCP hijacking. ## Test Execution ``` ok github.com/github/gh-aw-mcpg/internal/proxy 0.088s coverage: 92.5% of statements ``` All 160+ tests across all packages pass. ## Why These Changes? `handler.go` had three distinct code paths that were entirely untested: 1. The body-read failure in `forwardAndReadBody` — requires a transport-level mock since `httptest.Server` bodies never fail 2. The nil-response early return in `handleUnrecognizedPassthrough` — requires an unreachable upstream for a non-DIFC path 3. The rate-limit tracing annotations in `handleWithDIFC` — requires a `429` response through the full DIFC pipeline All three paths are now covered, and the tests are self-contained, fast, and deterministic. --- *Generated by Test Improver Workflow* *Focuses on better patterns, increased coverage, and more stable tests* > [!WARNING] > <details> > <summary>Firewall blocked 1 domain</summary> > > The following domain was blocked by the firewall during workflow execution: > > - `index.crates.io` >> To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "index.crates.io" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > > </details> > Generated by [Test Improver](https://github.com/github/gh-aw-mcpg/actions/runs/28340521640) · 2K AIC · ⊞ 29.6K · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw-mcpg+%22gh-aw-workflow-id%3A+test-improver%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Test Improver, engine: copilot, version: 1.0.60, model: claude-sonnet-4.6, id: 28340521640, workflow_id: test-improver, run: https://github.com/github/gh-aw-mcpg/actions/runs/28340521640 --> <!-- gh-aw-workflow-id: test-improver --> <!-- gh-aw-workflow-call-id: github/gh-aw-mcpg/test-improver -->
2 parents 54c00fd + 96edd30 commit 1c1ac57

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

internal/proxy/handler_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8+
"fmt"
89
"io"
910
"net/http"
1011
"net/http/httptest"
@@ -20,6 +21,20 @@ import (
2021
"github.com/stretchr/testify/require"
2122
)
2223

24+
// bodyErrorTransport is an http.RoundTripper that returns a valid response whose
25+
// body fails when read, exercising the io.ReadAll error path in forwardAndReadBody.
26+
type bodyErrorTransport struct{}
27+
28+
func (b *bodyErrorTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
29+
pr, pw := io.Pipe()
30+
pw.CloseWithError(errors.New("simulated body read error"))
31+
return &http.Response{
32+
StatusCode: http.StatusOK,
33+
Header: make(http.Header),
34+
Body: pr,
35+
}, nil
36+
}
37+
2338
// newTestServer creates a minimal proxy Server for unit testing.
2439
// It uses a NoopGuard so no WASM file is needed, and points
2540
// upstream at the given URL (use a httptest.Server URL).
@@ -745,3 +760,71 @@ func TestHandleWithDIFC_StrictModeBlocksFilteredItems(t *testing.T) {
745760
// NoopGuard returns nil LabelResponse, so no items are filtered → 200 OK
746761
assert.Equal(t, http.StatusOK, w.Code)
747762
}
763+
764+
// ─── forwardAndReadBody: body read failure ────────────────────────────────────
765+
766+
// TestForwardAndReadBody_BodyReadError covers the io.ReadAll error path in
767+
// forwardAndReadBody (lines 431-435 of handler.go). The upstream request
768+
// succeeds but the response body fails when read.
769+
func TestForwardAndReadBody_BodyReadError(t *testing.T) {
770+
s := newTestServer(t, "http://example.com")
771+
s.httpClient = &http.Client{Transport: &bodyErrorTransport{}}
772+
h := &proxyHandler{server: s}
773+
774+
w := httptest.NewRecorder()
775+
resp, body := h.forwardAndReadBody(w, context.Background(), http.MethodGet, "/repos/org/repo/issues", nil, "", "")
776+
777+
assert.Nil(t, resp)
778+
assert.Nil(t, body)
779+
assertJSONErrorResponse(t, w.Result(), http.StatusBadGateway, "bad_gateway", "failed to read upstream response")
780+
}
781+
782+
// ─── handleUnrecognizedPassthrough: upstream failure returns early ────────────
783+
784+
// TestHandleUnrecognizedPassthrough_UpstreamFails covers the resp == nil early
785+
// return in handleUnrecognizedPassthrough (line 147-149 of handler.go). When the
786+
// upstream is unreachable for an unrecognized path, the handler writes a 502
787+
// error itself (via forwardAndReadBody) and returns without further processing.
788+
func TestHandleUnrecognizedPassthrough_UpstreamFails(t *testing.T) {
789+
// Use a closed httptest server URL to deterministically force a connection failure.
790+
closed := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
791+
upstreamURL := closed.URL
792+
closed.Close()
793+
s := newTestServer(t, upstreamURL)
794+
h := &proxyHandler{server: s}
795+
796+
req := httptest.NewRequest(http.MethodGet, "/v1/unknown/endpoint", nil)
797+
w := httptest.NewRecorder()
798+
h.ServeHTTP(w, req)
799+
800+
assertJSONErrorResponse(t, w.Result(), http.StatusBadGateway, "bad_gateway", "upstream request failed")
801+
}
802+
803+
// ─── handleWithDIFC: rate limit detection ────────────────────────────────────
804+
805+
// TestHandleWithDIFC_RateLimitDetected covers the rate-limit tracing code path
806+
// in handleWithDIFC (lines 230-237 of handler.go). An upstream 429 response with
807+
// a non-zero X-Ratelimit-Reset header exercises both the outer rateLimited branch
808+
// and the inner !resetAt.IsZero() branch.
809+
func TestHandleWithDIFC_RateLimitDetected(t *testing.T) {
810+
resetAt := time.Now().Add(60 * time.Second)
811+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
812+
w.Header().Set("Content-Type", "application/json")
813+
w.Header().Set("X-Ratelimit-Remaining", "0")
814+
w.Header().Set("X-Ratelimit-Reset", fmt.Sprintf("%d", resetAt.Unix()))
815+
w.WriteHeader(http.StatusTooManyRequests)
816+
w.Write([]byte(`{"message":"API rate limit exceeded"}`)) //nolint:errcheck
817+
}))
818+
defer upstream.Close()
819+
820+
s := newTestServer(t, upstream.URL)
821+
h := &proxyHandler{server: s}
822+
823+
req := httptest.NewRequest(http.MethodGet, "/repos/org/repo/issues", nil)
824+
w := httptest.NewRecorder()
825+
h.ServeHTTP(w, req)
826+
827+
// 429 response is passed through; writeResponse injects Retry-After
828+
assert.Equal(t, http.StatusTooManyRequests, w.Code)
829+
assert.NotEmpty(t, w.Header().Get("Retry-After"))
830+
}

0 commit comments

Comments
 (0)