Files
anthropic-proxy/internal/auth/selector_test.go
T
Alexander 0df28e9dd8 refactor: modularize codebase — deduplicate, extract, clean up
- 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
2026-04-15 11:01:29 +02:00

319 lines
7.1 KiB
Go

package auth
import (
"testing"
"time"
)
func TestNewPool(t *testing.T) {
creds := []*Credential{
{ID: "a", AccessToken: "tok-a"},
{ID: "b", AccessToken: "tok-b"},
}
p := NewPool(creds)
if p == nil {
t.Fatal("NewPool returned nil")
}
if len(p.creds) != 2 {
t.Errorf("pool has %d creds, want 2", len(p.creds))
}
if p.cursor != 0 {
t.Errorf("initial cursor = %d, want 0", p.cursor)
}
}
func TestPool_Pick_EmptyPool(t *testing.T) {
p := NewPool(nil)
_, err := p.Pick()
if err == nil {
t.Fatal("expected error from empty pool, got nil")
}
want := "no credentials available"
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestPool_Pick_SingleCredential(t *testing.T) {
cred := &Credential{ID: "only", AccessToken: "tok-only"}
p := NewPool([]*Credential{cred})
got, err := p.Pick()
if err != nil {
t.Fatalf("Pick() error = %v", err)
}
if got.ID != "only" {
t.Errorf("Pick() returned cred ID %q, want %q", got.ID, "only")
}
// Picking again should return the same credential
got2, err := p.Pick()
if err != nil {
t.Fatalf("second Pick() error = %v", err)
}
if got2.ID != "only" {
t.Errorf("second Pick() returned cred ID %q, want %q", got2.ID, "only")
}
}
func TestPool_Pick_RoundRobin(t *testing.T) {
creds := []*Credential{
{ID: "a"},
{ID: "b"},
{ID: "c"},
}
p := NewPool(creds)
// Should cycle through a, b, c, a, b, c
expected := []string{"a", "b", "c", "a", "b", "c"}
for i, want := range expected {
got, err := p.Pick()
if err != nil {
t.Fatalf("Pick() #%d error = %v", i, err)
}
if got.ID != want {
t.Errorf("Pick() #%d = %q, want %q", i, got.ID, want)
}
}
}
func TestPool_Pick_SkipsCooldown(t *testing.T) {
creds := []*Credential{
{ID: "a"},
{ID: "b", cooldownUntil: time.Now().Add(1 * time.Hour)},
{ID: "c"},
}
p := NewPool(creds)
// First pick: "a" (index 0, not on cooldown)
got, err := p.Pick()
if err != nil {
t.Fatalf("Pick() #1 error = %v", err)
}
if got.ID != "a" {
t.Errorf("Pick() #1 = %q, want %q", got.ID, "a")
}
// Second pick: cursor at 1, but "b" is on cooldown → skip to "c"
got, err = p.Pick()
if err != nil {
t.Fatalf("Pick() #2 error = %v", err)
}
if got.ID != "c" {
t.Errorf("Pick() #2 = %q, want %q", got.ID, "c")
}
// Third pick: cursor advanced past "c" to 0 → "a"
got, err = p.Pick()
if err != nil {
t.Fatalf("Pick() #3 error = %v", err)
}
if got.ID != "a" {
t.Errorf("Pick() #3 = %q, want %q", got.ID, "a")
}
}
func TestPool_Pick_AllOnCooldown(t *testing.T) {
future := time.Now().Add(1 * time.Hour)
creds := []*Credential{
{ID: "a", cooldownUntil: future},
{ID: "b", cooldownUntil: future},
}
p := NewPool(creds)
_, err := p.Pick()
if err == nil {
t.Fatal("expected error when all on cooldown, got nil")
}
want := "all 2 credentials are on cooldown"
if err.Error() != want {
t.Errorf("error = %q, want %q", err.Error(), want)
}
}
func TestPool_MarkFailure(t *testing.T) {
tests := []struct {
name string
statusCode int
expectCooldown bool
expectedDur time.Duration
}{
{
name: "429 sets 30s cooldown",
statusCode: 429,
expectCooldown: true,
expectedDur: 30 * time.Second,
},
{
name: "500 sets 5s cooldown",
statusCode: 500,
expectCooldown: true,
expectedDur: 5 * time.Second,
},
{
name: "502 sets 5s cooldown",
statusCode: 502,
expectCooldown: true,
expectedDur: 5 * time.Second,
},
{
name: "503 sets 5s cooldown",
statusCode: 503,
expectCooldown: true,
expectedDur: 5 * time.Second,
},
{
name: "400 does NOT set cooldown",
statusCode: 400,
expectCooldown: false,
},
{
name: "401 does NOT set cooldown",
statusCode: 401,
expectCooldown: false,
},
{
name: "403 does NOT set cooldown",
statusCode: 403,
expectCooldown: false,
},
{
name: "404 does NOT set cooldown",
statusCode: 404,
expectCooldown: false,
},
{
name: "422 does NOT set cooldown",
statusCode: 422,
expectCooldown: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cred := &Credential{ID: "test"}
p := NewPool([]*Credential{cred})
before := time.Now()
p.MarkFailure(cred, tt.statusCode)
if tt.expectCooldown {
if !cred.IsOnCooldown() {
t.Errorf("expected cooldown after status %d", tt.statusCode)
}
// Verify approximate duration
cred.mu.Lock()
cooldownEnd := cred.cooldownUntil
cred.mu.Unlock()
lower := before.Add(tt.expectedDur)
upper := time.Now().Add(tt.expectedDur)
if cooldownEnd.Before(lower) || cooldownEnd.After(upper) {
t.Errorf("cooldownUntil %v not in expected range [%v, %v]", cooldownEnd, lower, upper)
}
} else {
if cred.IsOnCooldown() {
t.Errorf("did not expect cooldown after status %d", tt.statusCode)
}
}
})
}
}
func TestPool_MarkSuccess(t *testing.T) {
cred := &Credential{
ID: "test",
cooldownUntil: time.Now().Add(1 * time.Hour),
}
p := NewPool([]*Credential{cred})
if !cred.IsOnCooldown() {
t.Fatal("precondition: expected credential to be on cooldown")
}
p.MarkSuccess(cred)
if cred.IsOnCooldown() {
t.Error("expected cooldown to be cleared after MarkSuccess")
}
}
func TestPool_RoundRobinCursorAdvancement(t *testing.T) {
creds := []*Credential{
{ID: "0"},
{ID: "1"},
{ID: "2"},
}
p := NewPool(creds)
// Verify cursor starts at 0
if p.cursor != 0 {
t.Fatalf("initial cursor = %d, want 0", p.cursor)
}
// Pick cred[0], cursor should advance to 1
got, _ := p.Pick()
if got.ID != "0" {
t.Errorf("first pick = %q, want %q", got.ID, "0")
}
if p.cursor != 1 {
t.Errorf("cursor after first pick = %d, want 1", p.cursor)
}
// Pick cred[1], cursor should advance to 2
got, _ = p.Pick()
if got.ID != "1" {
t.Errorf("second pick = %q, want %q", got.ID, "1")
}
if p.cursor != 2 {
t.Errorf("cursor after second pick = %d, want 2", p.cursor)
}
// Pick cred[2], cursor should wrap to 0
got, _ = p.Pick()
if got.ID != "2" {
t.Errorf("third pick = %q, want %q", got.ID, "2")
}
if p.cursor != 0 {
t.Errorf("cursor after third pick = %d, want 0 (wrap)", p.cursor)
}
}
func TestPool_RoundRobinWithCooldownSkip(t *testing.T) {
creds := []*Credential{
{ID: "0"},
{ID: "1", cooldownUntil: time.Now().Add(1 * time.Hour)},
{ID: "2"},
}
p := NewPool(creds)
// First pick: cred[0]
got, _ := p.Pick()
if got.ID != "0" {
t.Errorf("first pick = %q, want %q", got.ID, "0")
}
// Cursor should be at 1
if p.cursor != 1 {
t.Errorf("cursor after first pick = %d, want 1", p.cursor)
}
// Second pick: cursor at 1, but cred[1] on cooldown → skip to cred[2]
got, _ = p.Pick()
if got.ID != "2" {
t.Errorf("second pick = %q, want %q", got.ID, "2")
}
// Cursor should advance past cred[2] to 0
if p.cursor != 0 {
t.Errorf("cursor after second pick (skip) = %d, want 0", p.cursor)
}
// Third pick: cursor at 0, cred[0] available
got, _ = p.Pick()
if got.ID != "0" {
t.Errorf("third pick = %q, want %q", got.ID, "0")
}
if p.cursor != 1 {
t.Errorf("cursor after third pick = %d, want 1", p.cursor)
}
}