0df28e9dd8
- Unify duplicate uTLS transports into shared internal/transport package - Extract shared version constant into internal/version - Move LoadDefaultCredentials from config to auth (remove config→auth import) - Deduplicate handler.go: extract telemetry/error helpers (324→268 lines) - Break up main.go::run() into initCredential/initEmbedded - Eliminate logging.Config duplication (use config.LoggingConfig directly) - Extract logWriter to embedded/log.go, SSE fixtures to consts in sniff.go - Use uTLS client for usage polling (consistent TLS fingerprint) - Handle sjson.SetBytes errors in sanitize.go instead of silently swallowing - Document reverse-engineered magic values in billing.go - Unexport Credential.CooldownUntil (internal state) - Replace hardcoded auth bypass paths with map in server.go
233 lines
5.8 KiB
Go
233 lines
5.8 KiB
Go
package logging
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/fujin/anthropic-proxy/internal/config"
|
|
)
|
|
|
|
func TestRedactHeaders(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
headers http.Header
|
|
check func(t *testing.T, result string)
|
|
}{
|
|
{
|
|
name: "redacts Authorization",
|
|
headers: http.Header{
|
|
"Authorization": []string{"Bearer secret-token"},
|
|
},
|
|
check: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if m["Authorization"] != "***" {
|
|
t.Errorf("Authorization = %q, want ***", m["Authorization"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "redacts x-api-key",
|
|
headers: http.Header{
|
|
"X-Api-Key": []string{"sk-ant-secret"},
|
|
},
|
|
check: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if m["X-Api-Key"] != "***" {
|
|
t.Errorf("X-Api-Key = %q, want ***", m["X-Api-Key"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "preserves other headers",
|
|
headers: http.Header{
|
|
"Content-Type": []string{"application/json"},
|
|
"Accept": []string{"text/html", "application/json"},
|
|
},
|
|
check: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if m["Content-Type"] != "application/json" {
|
|
t.Errorf("Content-Type = %q, want application/json", m["Content-Type"])
|
|
}
|
|
if m["Accept"] != "text/html, application/json" {
|
|
t.Errorf("Accept = %q, want 'text/html, application/json'", m["Accept"])
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "case-insensitive redaction",
|
|
headers: http.Header{
|
|
"authorization": []string{"Bearer token"},
|
|
"X-API-KEY": []string{"key123"},
|
|
},
|
|
check: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
// http.Header canonicalizes keys, but RedactHeaders lowercases for comparison
|
|
for _, v := range m {
|
|
if v != "***" {
|
|
t.Errorf("expected all values to be ***, got %q", v)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "empty headers",
|
|
headers: http.Header{},
|
|
check: func(t *testing.T, result string) {
|
|
if result != "{}" {
|
|
t.Errorf("result = %q, want {}", result)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "mixed sensitive and non-sensitive",
|
|
headers: http.Header{
|
|
"Authorization": []string{"Bearer tok"},
|
|
"X-Api-Key": []string{"key"},
|
|
"Content-Type": []string{"application/json"},
|
|
"X-Request-Id": []string{"abc123"},
|
|
},
|
|
check: func(t *testing.T, result string) {
|
|
var m map[string]string
|
|
if err := json.Unmarshal([]byte(result), &m); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if m["Authorization"] != "***" {
|
|
t.Errorf("Authorization = %q, want ***", m["Authorization"])
|
|
}
|
|
if m["X-Api-Key"] != "***" {
|
|
t.Errorf("X-Api-Key = %q, want ***", m["X-Api-Key"])
|
|
}
|
|
if m["Content-Type"] != "application/json" {
|
|
t.Errorf("Content-Type = %q", m["Content-Type"])
|
|
}
|
|
if m["X-Request-Id"] != "abc123" {
|
|
t.Errorf("X-Request-Id = %q", m["X-Request-Id"])
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := RedactHeaders(tt.headers)
|
|
// Result should be valid JSON
|
|
if !json.Valid([]byte(result)) {
|
|
t.Fatalf("result is not valid JSON: %q", result)
|
|
}
|
|
tt.check(t, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedactHeaders_ReturnsJSON(t *testing.T) {
|
|
h := http.Header{"Foo": []string{"bar"}}
|
|
result := RedactHeaders(h)
|
|
if !strings.HasPrefix(result, "{") || !strings.HasSuffix(result, "}") {
|
|
t.Errorf("result not JSON object: %q", result)
|
|
}
|
|
}
|
|
|
|
func TestStatusLevel(t *testing.T) {
|
|
tests := []struct {
|
|
status int
|
|
want zerolog.Level
|
|
}{
|
|
{200, zerolog.InfoLevel},
|
|
{201, zerolog.InfoLevel},
|
|
{204, zerolog.InfoLevel},
|
|
{301, zerolog.InfoLevel},
|
|
{399, zerolog.InfoLevel},
|
|
{400, zerolog.WarnLevel},
|
|
{401, zerolog.WarnLevel},
|
|
{403, zerolog.WarnLevel},
|
|
{404, zerolog.WarnLevel},
|
|
{429, zerolog.WarnLevel},
|
|
{499, zerolog.WarnLevel},
|
|
{500, zerolog.ErrorLevel},
|
|
{502, zerolog.ErrorLevel},
|
|
{503, zerolog.ErrorLevel},
|
|
{599, zerolog.ErrorLevel},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := statusLevel(tt.status)
|
|
if got != tt.want {
|
|
t.Errorf("statusLevel(%d) = %v, want %v", tt.status, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetup_WithFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
logFile := filepath.Join(dir, "test.log")
|
|
|
|
logger := Setup(config.LoggingConfig{
|
|
Level: "debug",
|
|
File: logFile,
|
|
MaxSizeMB: 10,
|
|
MaxBackups: 1,
|
|
MaxAgeDays: 1,
|
|
})
|
|
|
|
// Verify logger works (no panic)
|
|
logger.Info().Msg("test message")
|
|
}
|
|
|
|
func TestSetup_WithoutFile(t *testing.T) {
|
|
// File empty — should use console or stderr mode depending on TTY
|
|
logger := Setup(config.LoggingConfig{
|
|
Level: "warn",
|
|
})
|
|
|
|
// Verify logger works (no panic)
|
|
logger.Warn().Msg("test warning")
|
|
}
|
|
|
|
func TestSetup_DefaultLevel(t *testing.T) {
|
|
// Empty level should default to info
|
|
logger := Setup(config.LoggingConfig{})
|
|
_ = logger // verify no panic
|
|
}
|
|
|
|
func TestSetup_InvalidLevel(t *testing.T) {
|
|
// Invalid level should default to info
|
|
logger := Setup(config.LoggingConfig{Level: "not-a-level"})
|
|
_ = logger // verify no panic
|
|
}
|
|
|
|
func TestFromContext_NoLogger(t *testing.T) {
|
|
// Background context has no zerolog logger — should return global
|
|
ctx := context.Background()
|
|
l := FromContext(ctx)
|
|
if l == nil {
|
|
t.Fatal("FromContext returned nil")
|
|
}
|
|
}
|
|
|
|
func TestFromContext_WithLogger(t *testing.T) {
|
|
logger := zerolog.Nop()
|
|
ctx := logger.WithContext(context.Background())
|
|
l := FromContext(ctx)
|
|
if l == nil {
|
|
t.Fatal("FromContext returned nil")
|
|
}
|
|
}
|