package reverseproxy import ( "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" ) // testProxyStatusName identifies the proxy in Proxy-Status assertions. RFC 9209 // wants the deployment named rather than the software, so the tests configure // one the way a deployment would. const testProxyStatusName = "proxy-3.example.com" // assertRefused checks the response of a refused incremental forwarding // against what RFC 10036 section 4.1 recommends. func assertRefused(t *testing.T, err error, hdr http.Header) { t.Helper() var handlerErr caddyhttp.HandlerError if !errors.As(err, &handlerErr) { t.Fatalf("expected a caddyhttp.HandlerError, got %T: %v", err, err) } if handlerErr.StatusCode != http.StatusNotImplemented { t.Errorf("status = %d, want %d", handlerErr.StatusCode, http.StatusNotImplemented) } want := testProxyStatusName + ";error=" + proxyErrorIncrementalRefused if got := hdr.Get("Proxy-Status"); got != want { t.Errorf("Proxy-Status = %q, want %q", got, want) } } // incrementalHandler proxies to backendAddr under a name it can report in a // Proxy-Status field. func incrementalHandler(backendAddr string) *Handler { h := minimalHandler(0, &Upstream{ Host: new(Host), Dial: backendAddr, }) h.ProxyStatusName = testProxyStatusName return h } // incrementalRequest builds a request marked incremental; an unknown length is // what a chunked request looks like once the server has parsed it. func incrementalRequest(body string, knownLength bool) *http.Request { var r io.Reader = strings.NewReader(body) if !knownLength { r = io.NopCloser(r) // opaque to httptest, so ContentLength stays -1 } req := httptest.NewRequest(http.MethodPost, "http://example.com/", r) req.Header.Set("Incremental", "?1") return prepareTestRequest(req) } // lengthRequiringTransport stands in for fastcgi, which cannot forward a body // whose length it does not know. type lengthRequiringTransport struct{ testTransport } func (lengthRequiringTransport) RequiresContentLength() bool { return true } type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } type lengthRequiringRoundTripFunc roundTripFunc func (f lengthRequiringRoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return roundTripFunc(f)(r) } func (lengthRequiringRoundTripFunc) RequiresContentLength() bool { return true } type stagedBody struct { release <-chan struct{} sent bool } func (b *stagedBody) Read(p []byte) (int, error) { if !b.sent { b.sent = true p[0] = 'x' return 1, nil } <-b.release return 0, io.EOF } func (*stagedBody) Close() error { return nil } // Without buffering or a transport framing conflict, an unknown-length // incremental request is streamed unchanged. func TestIncrementalProxiedWhenRequestLengthUnknown(t *testing.T) { var gotBody string var gotLength int64 var gotIncremental string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) gotBody = string(b) gotLength = r.ContentLength gotIncremental = r.Header.Get("Incremental") })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, incrementalRequest("hello", false), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if gotBody != "hello" { t.Errorf("upstream body = %q, want %q", gotBody, "hello") } if gotLength != -1 { t.Errorf("upstream ContentLength = %d, want -1", gotLength) } if gotIncremental != "?1" { t.Errorf("upstream Incremental = %q, want ?1", gotIncremental) } } // A transport that needs a CONTENT_LENGTH is the exception: only buffering the // body in full can supply one, which is what incremental forwarding rules out. func TestIncrementalRefusedWhenTransportRequiresContentLength(t *testing.T) { for _, requestBuffers := range []int64{0, 4096} { t.Run(fmt.Sprintf("buffers_%d", requestBuffers), func(t *testing.T) { var reached bool backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.RequestBuffers = requestBuffers h.Transport = lengthRequiringTransport{testTransport{&http.Transport{}}} rec := httptest.NewRecorder() err := h.ServeHTTP(rec, incrementalRequest("hello", false), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) assertRefused(t, err, rec.Header()) if reached { t.Error("request was forwarded upstream despite being refused") } }) } } // An unlimited buffer holds the entire body no matter its length, which RFC // 10036 section 3 forbids for a message marked incremental. func TestIncrementalRefusedWhenRequestBufferUnlimited(t *testing.T) { var reached bool backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.RequestBuffers = -1 rec := httptest.NewRecorder() err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) assertRefused(t, err, rec.Header()) if reached { t.Error("request was forwarded upstream despite being refused") } } // An explicit buffer is operator policy. An incremental request is refused // instead of allowing an external header to disable that policy. func TestIncrementalRefusedWhenRequestBufferConfigured(t *testing.T) { h := incrementalHandler("unused.invalid") h.RequestBuffers = 4096 rec := httptest.NewRecorder() err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) assertRefused(t, err, rec.Header()) } // A transport-supplied buffer that exists only to discover Content-Length is // unnecessary when the request already has one and must not delay forwarding. func TestIncrementalKnownLengthRequestBypassesTransportDefault(t *testing.T) { release := make(chan struct{}) reached := make(chan struct{}) h := incrementalHandler("unused.invalid") h.RequestBuffers = 4096 h.requestBuffersFromTransport = true h.Transport = lengthRequiringRoundTripFunc(func(req *http.Request) (*http.Response, error) { close(reached) return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: http.NoBody, ContentLength: 0, Request: req, }, nil }) req := httptest.NewRequest(http.MethodPost, "http://example.com/", &stagedBody{release: release}) req.ContentLength = 2 req.Header.Set("Content-Length", "2") req.Header.Set("Incremental", "?1") req = prepareTestRequest(req) done := make(chan error, 1) go func() { done <- h.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) }() select { case <-reached: case <-time.After(2 * time.Second): t.Error("incremental request remained in the transport's default buffer") } close(release) if err := <-done; err != nil { t.Fatalf("ServeHTTP() error = %v", err) } } // Without buffering configured there is nothing to refuse, so the request is // proxied as usual. func TestIncrementalProxiedWhenRequestNotBuffered(t *testing.T) { var gotBody string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) gotBody = string(b) })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, incrementalRequest("hello", false), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if gotBody != "hello" { t.Errorf("upstream body = %q, want %q", gotBody, "hello") } } // A bodyless request has no content to forward incrementally, so buffering it // is a no-op and there is nothing to refuse. func TestIncrementalProxiedWhenRequestHasNoBody(t *testing.T) { var reached bool backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.RequestBuffers = -1 req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) req.Header.Set("Incremental", "?1") req = prepareTestRequest(req) rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if !reached { t.Error("bodyless request was not forwarded upstream") } } // A configured method rewrite can remove the request content before proxying. // Refusal is decided after that rewrite rather than from the original request. func TestIncrementalRewriteRemovesRequestContentBeforeRefusal(t *testing.T) { var gotLength int64 = -1 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotLength = r.ContentLength })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.RequestBuffers = -1 h.Rewrite = &rewrite.Rewrite{Method: http.MethodGet} rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if gotLength != 0 { t.Errorf("upstream ContentLength = %d, want 0", gotLength) } } func TestIncrementalAddedByHeaderUpAfterBufferingIsRefused(t *testing.T) { h := incrementalHandler("unused.invalid") h.RequestBuffers = 4096 h.Headers = &headers.Handler{Request: &headers.HeaderOps{ Set: http.Header{"Incremental": []string{"?1"}}, }} req := incrementalRequest("hello", true) req.Header.Del("Incremental") rec := httptest.NewRecorder() err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) assertRefused(t, err, rec.Header()) } // serveIncremental runs an ordinary request against a backend whose response // is marked incremental. Request and response signals are independent. func serveIncremental(t *testing.T, responseBuffers int64, backendFn http.HandlerFunc) (*httptest.ResponseRecorder, error) { t.Helper() backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Incremental", "?1") backendFn(w, r) })) t.Cleanup(backend.Close) h := incrementalHandler(backend.Listener.Addr().String()) h.ResponseBuffers = responseBuffers req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil)) rec := httptest.NewRecorder() err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) return rec, err } // An explicit response buffer is operator policy. An incremental response is // refused rather than allowing the upstream to disable that policy. func TestIncrementalRefusedWhenResponseBufferConfigured(t *testing.T) { rec, err := serveIncremental(t, 4096, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) w.(http.Flusher).Flush() // forces chunked, so the length stays unknown _, _ = w.Write([]byte("data: hi\n\n")) }) assertRefused(t, err, rec.Header()) } // An unlimited buffer holds the entire response, whatever its length. func TestIncrementalRefusedWhenResponseBufferUnlimited(t *testing.T) { rec, err := serveIncremental(t, -1, func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("data: hi\n\n")) }) assertRefused(t, err, rec.Header()) } // A bodyless response has no content to forward incrementally, so there is // nothing to refuse even with buffering configured. func TestIncrementalProxiedWhenResponseHasNoBody(t *testing.T) { rec, err := serveIncremental(t, -1, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) if err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if rec.Code != http.StatusOK { t.Errorf("status = %d, want 200", rec.Code) } } // The upstream response is passed through untouched when no response // buffering is configured. func TestIncrementalProxiedWhenResponseNotBuffered(t *testing.T) { rec, err := serveIncremental(t, 0, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) w.(http.Flusher).Flush() _, _ = w.Write([]byte("data: hi\n\n")) }) if err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if rec.Code != http.StatusOK { t.Errorf("status = %d, want 200", rec.Code) } if got := rec.Body.String(); got != "data: hi\n\n" { t.Errorf("body = %q, want %q", got, "data: hi\n\n") } if got := rec.Header().Get("Incremental"); got != "?1" { t.Errorf("downstream Incremental = %q, want ?1", got) } } // Replacing an upstream response does not forward or buffer its body. The // replacement remains valid even when that unused response was incremental. func TestIncrementalResponseCanBeReplaced(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Incremental", "?1") _, _ = w.Write([]byte("upstream")) })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.ResponseBuffers = -1 h.HandleResponse = []caddyhttp.ResponseHandler{{}} req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil)) rec := httptest.NewRecorder() err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { _, writeErr := w.Write([]byte("replacement")) return writeErr })) if err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if got := rec.Body.String(); got != "replacement" { t.Errorf("body = %q, want %q", got, "replacement") } } func TestIncrementalAddedByHeaderDownAfterBufferingIsRefused(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("hello")) })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.ResponseBuffers = 4096 h.Headers = &headers.Handler{Response: &headers.RespHeaderOps{HeaderOps: &headers.HeaderOps{ Set: http.Header{"Incremental": []string{"?1"}}, }}} req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "http://example.com/", nil)) rec := httptest.NewRecorder() err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) assertRefused(t, err, rec.Header()) } // A software name identifies no deployment, so with nothing configured to // identify this proxy there is no Proxy-Status field to generate: RFC 9209 // leaves generating it optional. func TestProxyStatusOmittedWhenUnnamed(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.ProxyStatusName = "" h.RequestBuffers = -1 rec := httptest.NewRecorder() err := h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) var handlerErr caddyhttp.HandlerError if !errors.As(err, &handlerErr) || handlerErr.StatusCode != http.StatusNotImplemented { t.Fatalf("expected a 501 HandlerError, got %v", err) } if got, ok := rec.Header()["Proxy-Status"]; ok { t.Errorf("Proxy-Status = %q, want no field", got) } } // RFC 9209 section 2 asks intermediaries to preserve the members already in // the field, so the whole chain that handled the response stays visible. func TestProxyStatusPreservesUpstreamMembers(t *testing.T) { rec, err := serveIncremental(t, -1, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Proxy-Status", "ExampleCDN") _, _ = w.Write([]byte("data: hi\n\n")) }) var handlerErr caddyhttp.HandlerError if !errors.As(err, &handlerErr) || handlerErr.StatusCode != http.StatusNotImplemented { t.Fatalf("expected a 501 HandlerError, got %v", err) } want := "ExampleCDN, " + testProxyStatusName + ";error=" + proxyErrorIncrementalRefused if got := rec.Header().Get("Proxy-Status"); got != want { t.Errorf("Proxy-Status = %q, want %q", got, want) } } func TestProxyStatusDropsInvalidMembers(t *testing.T) { h := &Handler{ProxyStatusName: testProxyStatusName} upstream := http.Header{ "Proxy-Status": []string{"?1, (inner), upstream.example"}, } got, ok := h.proxyStatus(upstream, proxyErrorIncrementalRefused) if !ok { t.Fatal("proxyStatus() did not produce a value") } want := testProxyStatusName + ";error=" + proxyErrorIncrementalRefused if got != want { t.Errorf("Proxy-Status = %q, want %q", got, want) } } // A name a token cannot hold is quoted as a string instead, which RFC 9209 // allows just as well. func TestProxyStatusNameNeedingQuotes(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.ProxyStatusName = "Example CDN" h.RequestBuffers = -1 rec := httptest.NewRecorder() _ = h.ServeHTTP(rec, incrementalRequest("hello", true), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) want := `"Example CDN";error=` + proxyErrorIncrementalRefused if got := rec.Header().Get("Proxy-Status"); got != want { t.Errorf("Proxy-Status = %q, want %q", got, want) } } // HTTP/3 reports an unknown length for bodyless GET and HEAD requests. The // transport still needs to receive these requests rather than a 501 refusal. func TestIncrementalProxiedWhenBodylessRequestHasUnknownLength(t *testing.T) { var reached bool backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reached = true })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.RequestBuffers = 4096 h.Transport = lengthRequiringTransport{testTransport{&http.Transport{}}} req := httptest.NewRequest(http.MethodGet, "http://example.com/", io.NopCloser(strings.NewReader(""))) req.Header.Set("Incremental", "?1") req = prepareTestRequest(req) if req.ContentLength != -1 { t.Fatalf("ContentLength = %d, want -1", req.ContentLength) } rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if !reached { t.Error("bodyless request was not forwarded upstream") } } func TestIncrementalAddedByHeaderUpWhenBodylessRequestHasUnknownLength(t *testing.T) { h := incrementalHandler("unused.invalid") h.Headers = &headers.Handler{Request: &headers.HeaderOps{ Set: http.Header{"Incremental": []string{"?1"}}, }} h.Transport = lengthRequiringRoundTripFunc(func(req *http.Request) (*http.Response, error) { if req.ContentLength != 0 { t.Errorf("ContentLength = %d, want 0", req.ContentLength) } if req.Body != nil { t.Errorf("Body = %T, want nil", req.Body) } return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: http.NoBody, ContentLength: 0, Request: req, }, nil }) req := httptest.NewRequest(http.MethodGet, "http://example.com/", io.NopCloser(strings.NewReader(""))) req = prepareTestRequest(req) if req.ContentLength != -1 { t.Fatalf("ContentLength = %d, want -1", req.ContentLength) } rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } } func TestIncrementalBodylessUnknownLengthIsNormalised(t *testing.T) { h := Handler{Transport: lengthRequiringTransport{testTransport{&http.Transport{}}}} req := httptest.NewRequest(http.MethodGet, "http://example.com/", io.NopCloser(strings.NewReader(""))) req.Header.Set("Incremental", "?1") repl := caddy.NewReplacer() req = caddyhttp.PrepareRequest(req, repl, nil, &caddyhttp.Server{}) got, err := h.prepareRequest(req, repl) if err != nil { t.Fatalf("prepareRequest() error = %v", err) } if got.ContentLength != 0 { t.Errorf("ContentLength = %d, want 0", got.ContentLength) } if got.Body != nil { t.Errorf("Body = %T, want nil", got.Body) } } // A response to HEAD advertises the length the content would have had, while // carrying no content at all, so there is nothing for a buffer to hold back. func TestIncrementalProxiedWhenHeadResponseAdvertisesLength(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Incremental", "?1") w.Header().Set("Content-Length", "10000") w.WriteHeader(http.StatusOK) })) defer backend.Close() h := incrementalHandler(backend.Listener.Addr().String()) h.ResponseBuffers = -1 req := prepareTestRequest(httptest.NewRequest(http.MethodHead, "http://example.com/", nil)) rec := httptest.NewRecorder() if err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })); err != nil { t.Fatalf("ServeHTTP() error = %v", err) } if rec.Code != http.StatusOK { t.Errorf("status = %d, want 200", rec.Code) } } func TestResponseHasContent(t *testing.T) { for _, tc := range []struct { name string method string status int length int64 body io.ReadCloser want bool }{ {name: "unknown response", method: http.MethodGet, status: http.StatusOK, length: -1, body: io.NopCloser(strings.NewReader("x")), want: true}, {name: "HEAD", method: http.MethodHead, status: http.StatusOK, length: 10, body: io.NopCloser(strings.NewReader(""))}, {name: "informational", method: http.MethodGet, status: http.StatusEarlyHints, length: -1, body: io.NopCloser(strings.NewReader(""))}, {name: "no content", method: http.MethodGet, status: http.StatusNoContent, length: -1, body: io.NopCloser(strings.NewReader(""))}, {name: "reset content", method: http.MethodGet, status: http.StatusResetContent, length: -1, body: io.NopCloser(strings.NewReader(""))}, {name: "not modified", method: http.MethodGet, status: http.StatusNotModified, length: -1, body: io.NopCloser(strings.NewReader(""))}, {name: "successful CONNECT", method: http.MethodConnect, status: http.StatusOK, length: -1, body: io.NopCloser(strings.NewReader(""))}, {name: "failed CONNECT", method: http.MethodConnect, status: http.StatusBadGateway, length: -1, body: io.NopCloser(strings.NewReader("x")), want: true}, {name: "zero length", method: http.MethodGet, status: http.StatusOK, length: 0, body: io.NopCloser(strings.NewReader(""))}, {name: "NoBody", method: http.MethodGet, status: http.StatusOK, length: -1, body: http.NoBody}, } { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequest(tc.method, "http://example.com/", nil) res := &http.Response{StatusCode: tc.status, ContentLength: tc.length, Body: tc.body} if got := responseHasContent(req, res); got != tc.want { t.Errorf("responseHasContent() = %v, want %v", got, tc.want) } }) } }