Files
egress/upstream_test.go
zeekayandHanzo Dev 5d5db19600 the store answers this process, and the provider leg goes where we point it
Three things on the two legs that carry a credential.

The KMS leg is addressed. sdk/go v1.1.4 carries the channel binding the
hybrid handshake derives beside the session key, and the store honours a
request only on the channel it names. Agreeing keys with both sides is
what an on-path party does; it now yields nothing, because a request
written for one channel cannot be re-signed for another. What is still
open is the other direction — egress has no name for the real store, so
a substitute answer is not yet distinguishable. Recorded in §9.

scrub takes out what an error shares with the credential, rather than
looking for the credential itself. Providers rarely quote a rejected key
whole: OpenAI answers with the first segment and the last four joined by
asterisks, others truncate after a prefix, and a key carried in a basic
auth header comes back base64. Each of those is a piece worth having, and
the error carrying it is the one value on this path that reaches both a
caller and a log. Matching by shared stretches covers every shape at once,
with a floor of four characters so an ordinary message survives intact —
a test pins both halves, because a scrub that eats the message has traded
a leak for a service nobody can debug.

The provider leg carries a transport of its own with Proxy nil. Go's
default reads HTTP_PROXY, HTTPS_PROXY and ALL_PROXY, so leaving Transport
nil lets whoever writes the environment choose the far end of the
connection that carries a key — the same choice the certificate check
exists to take away from them. Everything else about the default
transport is kept.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 04:54:13 -07:00

163 lines
6.3 KiB
Go

package egress
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"github.com/hanzoai/ai/model"
"github.com/hanzoai/ai/proxy"
)
// TestAPanickingDialectIsOneRefusedCall pins the goroutine. A dialect runs on
// one of its own and takes no context, so a vendor client that falls over
// takes this process with it unless the fall is caught where it happens. Every
// replica serves every tenant, so that is the whole service, not one call.
func TestAPanickingDialectIsOneRefusedCall(t *testing.T) {
result, err := run(context.Background(), func() (*model.ModelResult, error) {
panic("the vendor client fell over")
})
if err == nil {
t.Fatalf("a panicking dialect was reported as an answer: %+v", result)
}
if result != nil {
t.Errorf("result = %+v, want nil", result)
}
}
// TestTheUpstreamCallIsMadeWithThisProcessOwnClient drives a real dialect at a
// server this test controls and counts what arrives. hanzoai/ai holds the
// outbound client in a package variable that starts empty, so a process that
// does not fill it dereferences nothing on its first call.
func TestTheUpstreamCallIsMadeWithThisProcessOwnClient(t *testing.T) {
var arrived atomic.Int64
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
arrived.Add(1)
w.WriteHeader(http.StatusUnauthorized)
}))
defer upstream.Close()
s, key := serving(t, newStore(map[string]string{
orgRef(alice, "openai", "default"): "sk-ours",
}), 100)
s.cfg.URLs = map[string]string{"openai": upstream.URL}
_, body := ask(t, s, http.MethodPost, "/v1/call", token(t, key, nil),
Call{Provider: "OpenAI", Model: "gpt-3.5-turbo", Question: "hi", Lang: "en"})
if arrived.Load() == 0 {
t.Fatalf("the call never reached an upstream: %s", body)
}
}
// TestAnUpstreamThatCannotProveWhoItIsIsRefused pins certificate verification
// on the leg that carries the credential. The server here presents a
// certificate no root vouches for, which is what an interception looks like
// from inside this process.
func TestAnUpstreamThatCannotProveWhoItIsIsRefused(t *testing.T) {
var arrived atomic.Int64
upstream := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
arrived.Add(1)
w.WriteHeader(http.StatusOK)
}))
defer upstream.Close()
s, key := serving(t, newStore(map[string]string{
orgRef(alice, "openai", "default"): "sk-ours",
}), 100)
s.cfg.URLs = map[string]string{"openai": upstream.URL}
_, body := ask(t, s, http.MethodPost, "/v1/call", token(t, key, nil),
Call{Provider: "OpenAI", Model: "gpt-3.5-turbo", Question: "hi", Lang: "en"})
if arrived.Load() != 0 {
t.Fatalf("the credential was sent to an upstream that proved nothing: %s", body)
}
if !strings.Contains(body, "certificate") {
t.Fatalf("the refusal was not about the certificate: %s", body)
}
}
// TestAPanickingDialectCannotEchoTheCredential covers what the caught fall
// says. A panic carries whatever the vendor client was holding, which on this
// path is a request with the key in its header, and the error that replaces it
// travels to the caller and into a log.
func TestAPanickingDialectCannotEchoTheCredential(t *testing.T) {
const key = "sk-ours"
_, err := run(context.Background(), func() (*model.ModelResult, error) {
panic("POST https://api.openai.com: Authorization: Bearer " + key)
})
if err == nil {
t.Fatal("a panicking dialect was reported as an answer")
}
if got := scrub(err, key); strings.Contains(got.Error(), key) {
t.Fatalf("the credential survived the fall: %s", got)
}
}
// TestTheCredentialDoesNotReachTheCallerThroughAnUpstreamError drives the
// whole path: a provider rejects the key and quotes it back the way OpenAI
// does, and the refusal travels through the dialect, through spend, into the
// error frame the caller reads and the line the log keeps. Scrub is a unit
// test elsewhere; this is the one that fails if a return path ever stops
// going through it.
func TestTheCredentialDoesNotReachTheCallerThroughAnUpstreamError(t *testing.T) {
const key = "sk-proj-4tHhQ2vLm8XnPqRs7WdYbGjK1cZeUfAz9q7x"
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":{"message":"Incorrect API key provided: ` +
`sk-proj-****************************9q7x. You can find your API key at ` +
`https://platform.openai.com/account/api-keys.","type":"invalid_request_error",` +
`"code":"invalid_api_key"}}`))
}))
defer upstream.Close()
s, tok := serving(t, newStore(map[string]string{
orgRef(alice, "openai", "default"): key,
}), 100)
s.cfg.URLs = map[string]string{"openai": upstream.URL}
_, body := ask(t, s, http.MethodPost, "/v1/call", token(t, tok, nil),
Call{Provider: "OpenAI", Model: "gpt-3.5-turbo", Question: "hi", Lang: "en"})
for _, piece := range []string{key, "sk-proj-", "9q7x"} {
if strings.Contains(body, piece) {
t.Errorf("a piece of the credential reached the caller: %q is in %s", piece, body)
}
}
}
// TestTheRouteToAProviderIsNotSetFromTheEnvironment pins where a credential
// goes. Go's default transport reads HTTP_PROXY, HTTPS_PROXY and ALL_PROXY,
// so a client that leaves Transport nil lets whoever writes the process
// environment choose the far end of the connection carrying a key — the same
// choice the certificate check exists to take away from them. Egress dials
// the upstream named in its config and nothing else.
//
// This reads the transport rather than staging a proxy and watching, because
// Go resolves the proxy environment once per process and caches the result:
// an env-driven version of this test would pass or fail on which test ran
// first, which is worse than no test.
func TestTheRouteToAProviderIsNotSetFromTheEnvironment(t *testing.T) {
serving(t, newStore(nil), 100)
client := proxy.ProxyHttpClient
if client == nil {
t.Fatal("no outbound client was installed")
}
if client.Transport == http.DefaultTransport {
t.Fatal("the outbound leg rides the process-wide transport, which reads the environment")
}
transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("the outbound client has no transport of its own: %T", client.Transport)
}
if transport.Proxy != nil {
t.Error("the environment can choose where a credential-bearing call goes")
}
}