9150f466e5
Characterization tests capturing current behavior before refactoring. Covers auth, config, logging, proxy, ratelimit, server, and telemetry packages with race-safe concurrent access tests.
279 lines
6.8 KiB
Go
279 lines
6.8 KiB
Go
package proxy
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newRequest(t *testing.T, headers map[string][]string) *http.Request {
|
|
t.Helper()
|
|
r := httptest.NewRequest("POST", "/v1/messages", nil)
|
|
r.Header = http.Header{}
|
|
for k, vals := range headers {
|
|
for _, v := range vals {
|
|
r.Header.Add(k, v)
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestExtractProfile_BasicHeaders(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"Content-Type": {"application/json"},
|
|
"X-Custom-Header": {"custom-value"},
|
|
"User-Agent": {"Claude/1.2.3 linux"},
|
|
})
|
|
body := []byte(`{"model":"claude-sonnet-4-6"}`)
|
|
|
|
p := extractProfile(r, body)
|
|
|
|
// Check version parsed
|
|
if p.Version != "1.2.3" {
|
|
t.Errorf("version = %q, want %q", p.Version, "1.2.3")
|
|
}
|
|
|
|
// Check body preserved
|
|
if string(p.Body) != string(body) {
|
|
t.Errorf("body not preserved")
|
|
}
|
|
|
|
// Check headers captured
|
|
found := map[string]bool{}
|
|
for _, h := range p.Headers {
|
|
found[strings.ToLower(h[0])] = true
|
|
}
|
|
if !found["content-type"] {
|
|
t.Error("Content-Type header should be captured")
|
|
}
|
|
if !found["x-custom-header"] {
|
|
t.Error("X-Custom-Header should be captured")
|
|
}
|
|
}
|
|
|
|
func TestExtractProfile_SkipHeaders(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"Host": {"example.com"},
|
|
"Content-Length": {"42"},
|
|
"Authorization": {"Bearer token123"},
|
|
"X-Api-Key": {"key123"},
|
|
"Connection": {"keep-alive"},
|
|
"Content-Type": {"application/json"},
|
|
"X-Custom": {"keep-me"},
|
|
})
|
|
|
|
p := extractProfile(r, []byte(`{}`))
|
|
|
|
for _, h := range p.Headers {
|
|
lower := strings.ToLower(h[0])
|
|
if skipHeaders[lower] {
|
|
t.Errorf("header %q should have been skipped", h[0])
|
|
}
|
|
}
|
|
|
|
// Verify non-skipped headers are present
|
|
found := map[string]bool{}
|
|
for _, h := range p.Headers {
|
|
found[strings.ToLower(h[0])] = true
|
|
}
|
|
if !found["content-type"] {
|
|
t.Error("Content-Type should be kept")
|
|
}
|
|
if !found["x-custom"] {
|
|
t.Error("X-Custom should be kept")
|
|
}
|
|
}
|
|
|
|
func TestExtractProfile_HeaderDeduplication(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"Content-Type": {"application/json"},
|
|
})
|
|
// Add duplicate with different casing - Go's http.Header normalizes to canonical form
|
|
// so we need to add the same canonical header with multiple values to test dedup
|
|
r.Header.Add("Content-Type", "text/plain")
|
|
|
|
p := extractProfile(r, []byte(`{}`))
|
|
|
|
// After deduplication by lowercase key, only one entry per key
|
|
seen := map[string]int{}
|
|
for _, h := range p.Headers {
|
|
seen[strings.ToLower(h[0])]++
|
|
}
|
|
for key, count := range seen {
|
|
if count > 1 {
|
|
t.Errorf("header %q appears %d times after dedup, want 1", key, count)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractProfile_AnthropicBetaContextStripping(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"Anthropic-Beta": {"prompt-caching-2024-07-31,context-1m-2024-09-01,some-other-beta"},
|
|
})
|
|
|
|
p := extractProfile(r, []byte(`{}`))
|
|
|
|
var betaValue string
|
|
for _, h := range p.Headers {
|
|
if strings.ToLower(h[0]) == "anthropic-beta" {
|
|
betaValue = h[1]
|
|
break
|
|
}
|
|
}
|
|
|
|
if strings.Contains(betaValue, "context-1m") {
|
|
t.Errorf("context-1m should be stripped from anthropic-beta, got %q", betaValue)
|
|
}
|
|
if !strings.Contains(betaValue, "prompt-caching-2024-07-31") {
|
|
t.Errorf("prompt-caching should be preserved, got %q", betaValue)
|
|
}
|
|
if !strings.Contains(betaValue, "some-other-beta") {
|
|
t.Errorf("some-other-beta should be preserved, got %q", betaValue)
|
|
}
|
|
}
|
|
|
|
func TestExtractProfile_AnthropicBetaAllContextRemoved(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"Anthropic-Beta": {"context-1m-2024-09-01"},
|
|
})
|
|
|
|
p := extractProfile(r, []byte(`{}`))
|
|
|
|
for _, h := range p.Headers {
|
|
if strings.ToLower(h[0]) == "anthropic-beta" {
|
|
// All betas were context-1m, so after filtering the value should be empty
|
|
if h[1] != "" {
|
|
t.Errorf("all context-1m betas stripped should leave empty, got %q", h[1])
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// It's also acceptable if the header is still present but empty
|
|
}
|
|
|
|
func TestExtractProfile_VersionParsing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
userAgent string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "standard Claude UA",
|
|
userAgent: "Claude/1.2.3 linux x86_64",
|
|
expected: "1.2.3",
|
|
},
|
|
{
|
|
name: "version with no space after",
|
|
userAgent: "Claude/4.5.6",
|
|
expected: "4.5.6",
|
|
},
|
|
{
|
|
name: "no slash in UA",
|
|
userAgent: "Mozilla 5.0",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "empty UA",
|
|
userAgent: "",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "slash at start",
|
|
userAgent: "/1.0.0 rest",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "multiple slashes",
|
|
userAgent: "App/1.0.0 (sub/2.0)",
|
|
expected: "1.0.0",
|
|
},
|
|
{
|
|
name: "version only after slash no space",
|
|
userAgent: "Tool/9.8.7",
|
|
expected: "9.8.7",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"User-Agent": {tt.userAgent},
|
|
})
|
|
p := extractProfile(r, []byte(`{}`))
|
|
if p.Version != tt.expected {
|
|
t.Errorf("version = %q, want %q", p.Version, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractProfile_EmptyHeaders(t *testing.T) {
|
|
r := httptest.NewRequest("POST", "/v1/messages", nil)
|
|
r.Header = http.Header{}
|
|
|
|
p := extractProfile(r, []byte(`{"test":true}`))
|
|
|
|
if len(p.Headers) != 0 {
|
|
t.Errorf("expected no headers, got %d", len(p.Headers))
|
|
}
|
|
if p.Version != "" {
|
|
t.Errorf("expected empty version with no UA, got %q", p.Version)
|
|
}
|
|
if string(p.Body) != `{"test":true}` {
|
|
t.Errorf("body not preserved")
|
|
}
|
|
}
|
|
|
|
func TestExtractProfile_BodyPreserved(t *testing.T) {
|
|
r := newRequest(t, map[string][]string{
|
|
"User-Agent": {"Claude/1.0.0 test"},
|
|
})
|
|
body := []byte(`{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hello"}],"stream":true}`)
|
|
|
|
p := extractProfile(r, body)
|
|
|
|
if string(p.Body) != string(body) {
|
|
t.Errorf("body not preserved.\ngot: %s\nwant: %s", p.Body, body)
|
|
}
|
|
}
|
|
|
|
func TestSkipHeaders_Entries(t *testing.T) {
|
|
expected := map[string]bool{
|
|
"host": true,
|
|
"content-length": true,
|
|
"authorization": true,
|
|
"x-api-key": true,
|
|
"connection": true,
|
|
}
|
|
if len(skipHeaders) != len(expected) {
|
|
t.Errorf("skipHeaders has %d entries, want %d", len(skipHeaders), len(expected))
|
|
}
|
|
for k, v := range expected {
|
|
if skipHeaders[k] != v {
|
|
t.Errorf("skipHeaders[%q] = %v, want %v", k, skipHeaders[k], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSniffedProfile_Fields(t *testing.T) {
|
|
// Verify the struct can hold all expected data
|
|
p := &SniffedProfile{
|
|
Headers: [][2]string{{"Content-Type", "application/json"}},
|
|
Body: []byte(`{}`),
|
|
Version: "1.0.0",
|
|
}
|
|
if len(p.Headers) != 1 {
|
|
t.Error("Headers should have 1 entry")
|
|
}
|
|
if p.Headers[0][0] != "Content-Type" || p.Headers[0][1] != "application/json" {
|
|
t.Error("Header not stored correctly")
|
|
}
|
|
if string(p.Body) != `{}` {
|
|
t.Error("Body not stored correctly")
|
|
}
|
|
if p.Version != "1.0.0" {
|
|
t.Error("Version not stored correctly")
|
|
}
|
|
}
|