Files
coder/aibridge/passthrough_test.go
T
Paweł Banaszewski e00e85765b chore: move aibridge library code into coder repo (#24190)
This PR merges code from `coder/aibridge` repository into `coder/coder`.
It was split into 4 PRs for easier review but stacked PRs will need to
be merged into this PR so all checks pass.

* https://github.com/coder/coder/pull/24190 -> raw code copy (this PR,
before merging PRs on top of it, it was just 1 commit:
https://github.com/coder/coder/commit/70d33f33200c7e77df910957595715f81f9bec24)
* https://github.com/coder/coder/pull/24570 -> update imports in
`coder/coder` to use copied code
* https://github.com/coder/coder/pull/24586 -> linter fixes and CI
integration (also added README.md)
* https://github.com/coder/coder/pull/24571 -> added exclude to
scripts/check_emdash.sh check

Original PR message (before PR squash):
Moves coder/aibridge code into coder/coder repository.

Omitted files:

- `go.mod`, `go.sum`, `.gitignore`, `.github/workflows/ci.yml,`
`Makefile`, `LICENSE`, `README.md` (modified README.md is added later)
- `.github`, `example`, `buildinfo,` `scripts` directories

Simple verification script (will list omitted files)

```
tmp=$(mktemp -d)
echo "$tmp"
git clone --depth=1 https://github.com/coder/aibridge "$tmp/aibridge"
git clone --depth=1 --branch pb/aibridge-code-move https://github.com/coder/coder "$tmp/coder"
diff -rq --exclude=.git "$tmp/aibridge" "$tmp/coder/aibridge"
# rm -rf "$tmp"
```
2026-04-22 17:01:01 +02:00

304 lines
8.9 KiB
Go

package aibridge //nolint:testpackage // tests unexported newPassthroughRouter
import (
"crypto/tls"
"maps"
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/aibridge/internal/testutil"
)
var testTracer = otel.Tracer("bridge_test")
func TestPassthroughRoutes(t *testing.T) {
t.Parallel()
upstreamRespBody := "upstream response"
tests := []struct {
name string
baseURLPath string
reqPath string
reqHost string
reqRemoteAddr string
reqHeaders http.Header
expectRequestPath string
expectQuery string
expectHeaders http.Header
expectRespStatus int
expectRespBody string
}{
{
name: "passthrough_route_no_path",
reqPath: "/v1/conversations",
expectRequestPath: "/v1/conversations",
expectRespStatus: http.StatusOK,
expectRespBody: upstreamRespBody,
},
{
name: "base_URL_path_is_preserved_in_passthrough_routes",
baseURLPath: "/api/v2",
reqPath: "/v1/models",
expectRequestPath: "/api/v2/v1/models",
expectRespStatus: http.StatusOK,
expectRespBody: upstreamRespBody,
},
{
name: "passthrough_route_break_parse_base_url",
baseURLPath: "/%zz",
reqPath: "/v1/models/",
expectRespStatus: http.StatusBadGateway,
expectRespBody: "invalid provider base URL",
},
{
name: "passthrough_route_rejects_invalid_base_url_path",
baseURLPath: "/%25",
reqPath: "/v1/models",
expectRespStatus: http.StatusBadGateway,
expectRespBody: "invalid provider base URL",
},
{
name: "proxy_headers_are_set_and_forwarded_chain_is_appended",
reqPath: "/v1/models",
reqHost: "client.example.com",
reqRemoteAddr: "1.1.1.1:1111",
reqHeaders: http.Header{
"X-Forwarded-For": {"2.2.2.2, 3.3.3.3"},
},
expectRequestPath: "/v1/models",
expectRespStatus: http.StatusOK,
expectRespBody: upstreamRespBody,
expectHeaders: http.Header{
"Accept-Encoding": {"gzip"},
"User-Agent": {"aibridge"},
"X-Forwarded-For": {"2.2.2.2, 3.3.3.3, 1.1.1.1"},
"X-Forwarded-Host": {"client.example.com"},
"X-Forwarded-Proto": {"http"},
},
},
{
name: "query_string_is_preserved",
reqPath: "/v1/models?search=gpt&limit=10",
expectRequestPath: "/v1/models",
expectQuery: "search=gpt&limit=10",
expectRespStatus: http.StatusOK,
expectRespBody: upstreamRespBody,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, tc.expectRequestPath, r.URL.Path)
assert.Equal(t, tc.expectQuery, r.URL.RawQuery)
if tc.expectHeaders != nil {
assert.Equal(t, tc.expectHeaders, r.Header)
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(upstreamRespBody))
}))
t.Cleanup(upstream.Close)
prov := &testutil.MockProvider{
URL: upstream.URL + tc.baseURLPath,
}
handler := newPassthroughRouter(prov, logger, nil, testTracer)
req := httptest.NewRequest("", tc.reqPath, nil)
maps.Copy(req.Header, tc.reqHeaders)
req.Host = tc.reqHost
req.RemoteAddr = tc.reqRemoteAddr
resp := httptest.NewRecorder()
handler.ServeHTTP(resp, req)
assert.Equal(t, tc.expectRespStatus, resp.Code)
assert.Contains(t, resp.Body.String(), tc.expectRespBody)
})
}
}
func TestRewritePassthroughRequest(t *testing.T) {
t.Parallel()
tests := []struct {
name string
reqPath string
reqRemoteAddr string
reqHeaders http.Header
reqTLS bool
provider *testutil.MockProvider
expectURL string
expectHeaders http.Header
}{
{
name: "sets_upstream_url_and_forwarded_headers_from_client_peer",
reqPath: "http://client-host/chat?stream=true",
reqRemoteAddr: "1.1.1.1:1111",
provider: &testutil.MockProvider{URL: "https://upstream-host/base"},
expectURL: "https://upstream-host/base/chat?stream=true",
expectHeaders: http.Header{
"X-Forwarded-Host": {"client-host"},
"X-Forwarded-Proto": {"http"},
"X-Forwarded-For": {"1.1.1.1"},
"User-Agent": {"aibridge"},
},
},
{
name: "preserves_client_user_agent",
reqPath: "http://client-host/chat",
reqRemoteAddr: "1.1.1.1:1111",
reqHeaders: http.Header{"User-Agent": {"custom-agent/1.0"}},
provider: &testutil.MockProvider{URL: "https://upstream-host/base"},
expectURL: "https://upstream-host/base/chat",
expectHeaders: http.Header{
"X-Forwarded-Host": {"client-host"},
"X-Forwarded-Proto": {"http"},
"X-Forwarded-For": {"1.1.1.1"},
"User-Agent": {"custom-agent/1.0"},
},
},
{
name: "injects_auth_header",
reqPath: "http://client-host/chat",
reqRemoteAddr: "1.1.1.1:1111",
provider: &testutil.MockProvider{
URL: "https://upstream-host/base",
InjectAuthHeaderFunc: func(h *http.Header) {
h.Set("Authorization", "Bearer test-token")
},
},
expectURL: "https://upstream-host/base/chat",
expectHeaders: http.Header{
"X-Forwarded-Host": {"client-host"},
"X-Forwarded-Proto": {"http"},
"X-Forwarded-For": {"1.1.1.1"},
"User-Agent": {"aibridge"},
"Authorization": {"Bearer test-token"},
},
},
{
name: "appends_remote_addr_to_existing_forwarded_for_chain",
reqPath: "http://client-host/chat",
reqRemoteAddr: "1.1.1.1:1111",
reqHeaders: http.Header{
"X-Forwarded-For": {"2.2.2.2, 3.3.3.3"},
},
provider: &testutil.MockProvider{URL: "https://upstream-host/base"},
expectURL: "https://upstream-host/base/chat",
expectHeaders: http.Header{
"X-Forwarded-Host": {"client-host"},
"X-Forwarded-Proto": {"http"},
"X-Forwarded-For": {"2.2.2.2, 3.3.3.3, 1.1.1.1"},
"User-Agent": {"aibridge"},
},
},
{
name: "tls_request_sets_forwarded_proto_to_https",
reqPath: "http://client-host/chat",
reqRemoteAddr: "1.1.1.1:1111",
reqTLS: true,
provider: &testutil.MockProvider{URL: "https://upstream-host/base"},
expectURL: "https://upstream-host/base/chat",
expectHeaders: http.Header{
"X-Forwarded-Host": {"client-host"},
"X-Forwarded-Proto": {"https"},
"X-Forwarded-For": {"1.1.1.1"},
"User-Agent": {"aibridge"},
},
},
{
// This is an edge case where whole `X-Forwarded-For` header
// is dropped if last hop (remote addr) is not parseable.
// This is how library handles this case and is not directly
// related to our code. Added it to verify that we
// don't accidentally break this behavior.
name: "omits_forwarded_for_when_remote_addr_is_not_parseable",
reqPath: "http://client-host/chat",
reqRemoteAddr: "not-a-socket-address",
reqHeaders: http.Header{
"X-Forwarded-For": {"1.1.1.1"},
},
provider: &testutil.MockProvider{URL: "https://upstream-host/base"},
expectURL: "https://upstream-host/base/chat",
expectHeaders: http.Header{
"X-Forwarded-Host": {"client-host"},
"X-Forwarded-Proto": {"http"},
"User-Agent": {"aibridge"},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r := httptest.NewRequest(http.MethodGet, tc.reqPath, nil)
maps.Copy(r.Header, tc.reqHeaders)
r.RemoteAddr = tc.reqRemoteAddr
if tc.reqTLS {
r.TLS = &tls.ConnectionState{}
}
provBaseURL, err := url.Parse(tc.provider.URL)
assert.NoError(t, err)
pr := &httputil.ProxyRequest{
In: r,
Out: r.Clone(r.Context()),
}
rewritePassthroughRequest(pr, provBaseURL, tc.provider)
assert.Equal(t, tc.expectURL, pr.Out.URL.String())
assert.Equal(t, "", pr.Out.Host)
assert.Equal(t, tc.expectHeaders, pr.Out.Header)
})
}
}
func TestPassthroughRouterReusesProxyInstance(t *testing.T) {
t.Parallel()
var newConnections atomic.Int32
upstream := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
upstream.Config.ConnState = func(_ net.Conn, state http.ConnState) {
if state == http.StateNew {
newConnections.Add(1)
}
}
upstream.Start()
t.Cleanup(upstream.Close)
logger := slogtest.Make(t, nil)
prov := &testutil.MockProvider{URL: upstream.URL}
handler := newPassthroughRouter(prov, logger, nil, testTracer)
for i := range 2 {
req := httptest.NewRequest(http.MethodGet, "http://proxy.example.test/v1/models", nil)
resp := httptest.NewRecorder()
handler.ServeHTTP(resp, req)
assert.Equalf(t, http.StatusOK, resp.Code, "request %d", i+1)
assert.Equal(t, "ok", resp.Body.String())
}
assert.EqualValues(t, 1, newConnections.Load())
}