Files
anthropic-proxy/internal/proxy/sniff_test.go
T
Alexander 9150f466e5 test: add comprehensive test harness across all packages (156 tests)
Characterization tests capturing current behavior before refactoring.
Covers auth, config, logging, proxy, ratelimit, server, and telemetry
packages with race-safe concurrent access tests.
2026-04-15 10:40:43 +02:00

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")
}
}