test: testify table-driven coverage for low-coverage branches

Lifts all 14 functions the operator flagged as <75% (measured at ed2119c)
to >=75%, using table-driven tests (require for fatals, assert for checks):

- limits.go: failPolicyGet (50.0% -> 100.0%)
- provider_gemini.go: httpClient (0.0% -> 100.0%)
- redis_store.go: get (71.4% -> 78.6%)
- resp.go: dialTimeoutFor, respDeadlineExceededErr.{Error,Timeout,Temporary},
  ensureConnLocked, getBatch (66.7-30.0% -> 91.3-100.0%)
- routes_media.go: resolveMediaRequest (50.0% -> 100.0%)
- routes_unified.go: unifiedCostMicros (66.7% -> 100.0%)
- translate_anthropic.go: isTruthy, toFloat64 (50.0/33.3% -> 100.0%)
- translate_gemini.go: geminiToolConfigFromOpenAI (66.7% -> 100.0%)

resp_test.go adds a hand-rolled fakeConn (net.Conn with injectable
Write/SetDeadline errors) to reach two respClient error branches a real
TCP connection cannot be coaxed into deterministically.

Total coverage: 90.7% -> 92.4% (no regression; report has the full
per-file table). Zero production-code changes. Existing tests untouched:
every diff is a pure addition of new test functions and their imports.

Gates: go test -count=1 (pass), -race (pass, 5x incl. an isolated
baseline-vs-branch comparison after one flaky run implicated pre-existing
registry_test.go timing tests this task never touched), gofmt/vet (clean),
golangci-lint incl. gosec and fieldalignment (0 issues), make yaegi-check
(OK), make integration (pass, untouched-green).
This commit is contained in:
2026-08-20 21:33:08 +01:00
parent f7d9208bde
commit aee3908bc4
8 changed files with 530 additions and 0 deletions
+28
View File
@@ -6,6 +6,9 @@ import (
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestLimiterRequestWindow is the brief's Step-1 case: a per-minute
@@ -632,3 +635,28 @@ func TestLimitsConfigValidate(t *testing.T) {
})
}
}
// TestLimiter_FailPolicyGet covers failPolicyGet's own fail-open/fail-closed
// branching in isolation (storeGet's error/latched paths already drive it
// indirectly elsewhere in this file): failOpen=true reads through to the
// in-process fallback, failOpen=false refuses outright.
func TestLimiter_FailPolicyGet(t *testing.T) {
cases := []struct {
name string
failOpen bool
wantOK bool
}{
{name: "failOpen true reads the fallback", failOpen: true, wantOK: true},
{name: "failOpen false refuses", failOpen: false, wantOK: false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
l := newLimiter(nil, c.failOpen)
v, ok := l.failPolicyGet("k")
require.Equal(t, c.wantOK, ok)
if c.wantOK {
assert.Equal(t, int64(0), v)
}
})
}
}
+13
View File
@@ -10,6 +10,9 @@ import (
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestNewGeminiAdapter_KeylessIsAConstructorError proves an empty resolved
@@ -566,3 +569,13 @@ func TestGeminiAdapter_ListModels(t *testing.T) {
t.Errorf("got %#v, want %#v", got, want)
}
}
// TestGeminiAdapter_HTTPClient proves httpClient returns the adapter's own
// *http.Client — the one newGeminiAdapter constructed via
// newAdapterHTTPClient — not a fresh or nil client.
func TestGeminiAdapter_HTTPClient(t *testing.T) {
a, err := newGeminiAdapter("p1", "https://generativelanguage.googleapis.com", "AIza-test")
require.NoError(t, err)
require.NotNil(t, a.httpClient())
assert.Same(t, a.client, a.httpClient())
}
+16
View File
@@ -3,6 +3,8 @@ package traefikllmgateway
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestRedisStore_IncrBy_PipelinesIncrbyAndExpire is the brief's Step-1
@@ -166,3 +168,17 @@ func TestLimiter_FailOpenTrue_HungRedisServer_FallsBackQuickly(t *testing.T) {
t.Fatalf("elapsed = %v, want < 3s (one respCallTimeout for the first op, the second latched and skipping the network entirely)", elapsed)
}
}
// TestRedisStore_Get_DownServer_ReturnsError asserts get surfaces a
// transport failure (dead address) as an error, mirroring incrBy's own
// contract for the same failure — the limiter's storeGet relies on this to
// treat a Redis outage as a store error rather than a false zero reading.
func TestRedisStore_Get_DownServer_ReturnsError(t *testing.T) {
ln := newFakeListener(t)
deadAddr := ln.Addr().String()
require.NoError(t, ln.Close())
store := newRedisStore(newRESPClient(deadAddr, "", 0))
_, err := store.get("k")
require.Error(t, err)
}
+252
View File
@@ -3,6 +3,7 @@ package traefikllmgateway
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
@@ -10,6 +11,9 @@ import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestEncodeCommand pins the exact RESP2 wire format every command must
@@ -594,3 +598,251 @@ func TestRESPClient_GetBytes_DownServer_ReturnsError(t *testing.T) {
t.Fatal("want an error dialing a dead address")
}
}
// --- fakeConn: a net.Conn whose Write/SetDeadline fail on command, for
// the handful of respClient error branches a real TCP connection cannot be
// coaxed into deterministically (a write failing on an otherwise-live
// connection, SetDeadline failing on a reused connection) ---
// fakeAddr is a trivial net.Addr for fakeConn's LocalAddr/RemoteAddr.
type fakeAddr struct{}
func (fakeAddr) Network() string { return "fake" }
func (fakeAddr) String() string { return "fake" }
// fakeConn implements net.Conn, returning writeErr from Write and
// setDeadlineErr from SetDeadline when set, so a test can drive respClient
// methods directly against a connection already known bad — bypassing
// ensureConnLocked's real net.DialTimeout, which always succeeds against a
// live listener and so cannot itself be made to fail this way.
type fakeConn struct {
writeErr error
setDeadlineErr error
}
func (fakeConn) Read([]byte) (int, error) { return 0, io.EOF }
func (c fakeConn) Write(b []byte) (int, error) {
if c.writeErr != nil {
return 0, c.writeErr
}
return len(b), nil
}
func (fakeConn) Close() error { return nil }
func (fakeConn) LocalAddr() net.Addr { return fakeAddr{} }
func (fakeConn) RemoteAddr() net.Addr { return fakeAddr{} }
func (c fakeConn) SetDeadline(time.Time) error {
if c.setDeadlineErr != nil {
return c.setDeadlineErr
}
return nil
}
func (fakeConn) SetReadDeadline(time.Time) error { return nil }
func (fakeConn) SetWriteDeadline(time.Time) error { return nil }
// --- dialTimeoutFor / respDeadlineExceededErr ---
// TestDialTimeoutFor covers dialTimeoutFor's clamp: a deadline farther away
// than respDialTimeout gets the fixed respDialTimeout, never the (larger)
// remaining time — only a near deadline gets clamped down to the smaller
// remaining value.
func TestDialTimeoutFor(t *testing.T) {
cases := []struct {
check func(t *testing.T, got time.Duration)
name string
delta time.Duration
}{
{
name: "deadline farther away than respDialTimeout clamps to respDialTimeout",
delta: 10 * time.Second,
check: func(t *testing.T, got time.Duration) {
assert.Equal(t, respDialTimeout, got)
},
},
{
name: "deadline closer than respDialTimeout returns the remaining time",
delta: 50 * time.Millisecond,
check: func(t *testing.T, got time.Duration) {
assert.Greater(t, got, time.Duration(0))
assert.Less(t, got, respDialTimeout)
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
c.check(t, dialTimeoutFor(time.Now().Add(c.delta)))
})
}
}
// TestRespDeadlineExceededErr pins the three methods that let pipeline's
// isTimeout check treat an already-elapsed call deadline exactly like any
// other timed-out I/O error.
func TestRespDeadlineExceededErr(t *testing.T) {
var err respDeadlineExceededErr
assert.Equal(t, "resp: call deadline already elapsed", err.Error())
assert.True(t, err.Timeout())
assert.True(t, err.Temporary())
}
// --- ensureConnLocked / attemptPipelineLocked / handshakeLocked error
// branches only reachable via a pre-set connection, not a real dial ---
// TestRESPClient_EnsureConnLocked_Errors covers ensureConnLocked's two
// error returns that never touch the network: a reused connection whose
// SetDeadline itself fails, and a deadline that has already elapsed before
// a fresh dial would even start.
func TestRESPClient_EnsureConnLocked_Errors(t *testing.T) {
cases := []struct {
name string
client func() *respClient
deadline time.Time
wantErrContains string
}{
{
name: "reused connection: SetDeadline failure surfaces",
client: func() *respClient {
return &respClient{conn: fakeConn{setDeadlineErr: errors.New("stub: set deadline failed")}}
},
deadline: time.Now().Add(time.Second),
wantErrContains: "set deadline",
},
{
name: "no connection yet: an already-elapsed deadline never dials",
client: func() *respClient {
return &respClient{addr: "127.0.0.1:1"}
},
deadline: time.Now().Add(-time.Second),
wantErrContains: "call deadline already elapsed",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := c.client().ensureConnLocked(c.deadline)
require.Error(t, err)
assert.Contains(t, err.Error(), c.wantErrContains)
})
}
}
// TestRESPClient_AttemptPipelineLocked_WriteError covers the command-write
// failure attemptPipelineLocked returns when an already-connected socket
// refuses a write (a broken pipe) — distinct from a dial failure, which
// never reaches this code path.
func TestRESPClient_AttemptPipelineLocked_WriteError(t *testing.T) {
c := &respClient{
conn: fakeConn{writeErr: errors.New("stub: broken pipe")},
r: bufio.NewReader(strings.NewReader("")),
}
_, err := c.attemptPipelineLocked([][]string{{"GET", "k"}}, time.Now().Add(time.Second))
require.Error(t, err)
assert.Contains(t, err.Error(), "write")
}
// TestRESPClient_HandshakeLocked_WriteError covers handshakeLocked's own
// write failure, the same broken-pipe shape as
// TestRESPClient_AttemptPipelineLocked_WriteError but for the AUTH/SELECT
// handshake command specifically.
func TestRESPClient_HandshakeLocked_WriteError(t *testing.T) {
c := &respClient{conn: fakeConn{writeErr: errors.New("stub: broken pipe")}}
err := c.handshakeLocked("AUTH", "pw")
require.Error(t, err)
assert.Contains(t, err.Error(), "AUTH")
}
// TestRESPClient_AuthHandshakeFailure_ErrorReplySurfaces covers
// ensureConnLocked's AUTH-failure branch and handshakeLocked's respErr
// branch together: a RESP error reply to AUTH must fail the call, not be
// silently treated as success. It calls attemptPipelineLocked directly
// (one attempt, one scripted connection) rather than the public do/
// pipeline, which would retry once more against a second connection the
// fakeRESPServer fixture — built for a server-initiated closeConn, not a
// client-initiated close on handshake failure — cannot script cleanly.
func TestRESPClient_AuthHandshakeFailure_ErrorReplySurfaces(t *testing.T) {
ln := newFakeListener(t)
runFakeRESPServer(t, ln, []respStep{
{wantArgs: []string{"AUTH", "wrong-pw"}, reply: []byte("-ERR invalid password\r\n")},
})
c := newRESPClient(ln.Addr().String(), "wrong-pw", 0)
_, err := c.attemptPipelineLocked([][]string{{"GET", "k"}}, time.Now().Add(2*time.Second))
require.Error(t, err)
assert.Contains(t, err.Error(), "AUTH failed")
}
// --- getBatch ---
// TestRESPClient_GetBatch covers every reply shape getBatch's decode loop
// handles: a mix of hits and a missing key (RESP null bulk -> 0) on
// success, and the three ways a single bad reply fails the whole batch —
// a RESP error reply, an unexpected (non-bulk) reply type, and a bulk
// value that is not a base-10 integer.
func TestRESPClient_GetBatch(t *testing.T) {
cases := []struct {
name string
wantErrSubstr string
keys []string
steps []respStep
want []int64
wantErr bool
}{
{
name: "mixed hits and a missing key",
keys: []string{"a", "b", "c"},
steps: []respStep{
{wantArgs: []string{"GET", "a"}, reply: []byte("$3\r\n123\r\n")},
{wantArgs: []string{"GET", "b"}, reply: []byte("$-1\r\n")},
{wantArgs: []string{"GET", "c"}, reply: []byte("$3\r\n456\r\n")},
},
want: []int64{123, 0, 456},
},
{
name: "a RESP error reply fails the whole batch",
keys: []string{"a", "b"},
steps: []respStep{
{wantArgs: []string{"GET", "a"}, reply: []byte("$1\r\n1\r\n")},
{wantArgs: []string{"GET", "b"}, reply: []byte("-ERR busy\r\n")},
},
wantErr: true,
wantErrSubstr: "ERR busy",
},
{
name: "an unexpected reply type fails the whole batch",
keys: []string{"a"},
steps: []respStep{
{wantArgs: []string{"GET", "a"}, reply: []byte(":5\r\n")},
},
wantErr: true,
wantErrSubstr: "unexpected reply type",
},
{
name: "a non-integer value fails the whole batch",
keys: []string{"a"},
steps: []respStep{
{wantArgs: []string{"GET", "a"}, reply: []byte("$3\r\nabc\r\n")},
},
wantErr: true,
wantErrSubstr: "non-integer value",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ln := newFakeListener(t)
steps := append([]respStep{{wantArgs: []string{"SELECT", "0"}, reply: []byte("+OK\r\n")}}, c.steps...)
runFakeRESPServer(t, ln, steps)
client := newRESPClient(ln.Addr().String(), "", 0)
got, err := client.getBatch(c.keys)
if c.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), c.wantErrSubstr)
return
}
require.NoError(t, err)
assert.Equal(t, c.want, got)
})
}
}
+56
View File
@@ -13,6 +13,9 @@ import (
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newMultipartTranscriptionRequest builds a POST /v1/audio/transcriptions
@@ -1035,3 +1038,56 @@ func TestAnthropicMediaEndpoints_Return501_NeverCallUpstream(t *testing.T) {
}
})
}
// ---- resolveMediaRequest: the shared resolve/limit failure branches ----
//
// Every other media-route test above drives resolveMediaRequest's success
// path only; these two cover its two failure returns (routes_media.go) —
// a model no configured provider knows, and a limit violation — using
// images.generations as the representative caller, since all three media
// routes share the same resolveMediaRequest call.
// TestHandleImagesGenerations_UnknownModel_404 covers resolveMediaRequest's
// registry.resolve error branch: a model id no configured provider knows at
// all.
func TestHandleImagesGenerations_UnknownModel_404(t *testing.T) {
gw := newMediaTestGateway(t, newMediaTestConfig("http://127.0.0.1:1", "img-test"))
body := map[string]any{"model": "no-such-model", "prompt": "a cat"}
req := newUnifiedRequest(t, http.MethodPost, imagesGenerationsPath, "sk-alice", body)
rec := httptest.NewRecorder()
gw.ServeHTTP(rec, req)
assert.Equal(t, http.StatusNotFound, rec.Code, "body=%s", rec.Body.String())
}
// TestHandleImagesGenerations_LimitExceeded_429WithRetryAfter covers
// resolveMediaRequest's checkAndCount violation branch: a second request
// past a one-per-minute limit is refused before the adapter is ever called.
func TestHandleImagesGenerations_LimitExceeded_429WithRetryAfter(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"created":1734000000,"data":[{"b64_json":"xyz"}]}`))
}))
defer srv.Close()
cfg := newMediaTestConfig(srv.URL, "img-test")
cfg.Users = &UsersConfig{Inline: []*UserConfig{
{Name: "alice", Group: "default", APIKey: "sk-alice", Limits: &LimitsConfig{RequestsPerMinute: 1}},
}}
gw := newMediaTestGateway(t, cfg)
body := map[string]any{"model": "img-test", "prompt": "a cat"}
req1 := newUnifiedRequest(t, http.MethodPost, imagesGenerationsPath, "sk-alice", body)
rec1 := httptest.NewRecorder()
gw.ServeHTTP(rec1, req1)
require.Equal(t, http.StatusOK, rec1.Code, "first request body=%s", rec1.Body.String())
req2 := newUnifiedRequest(t, http.MethodPost, imagesGenerationsPath, "sk-alice", body)
rec2 := httptest.NewRecorder()
gw.ServeHTTP(rec2, req2)
assert.Equal(t, http.StatusTooManyRequests, rec2.Code, "body=%s", rec2.Body.String())
assert.NotEmpty(t, rec2.Header().Get("Retry-After"))
}
+47
View File
@@ -17,6 +17,8 @@ import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// newUnifiedRequest builds a POST request against the unified routes,
@@ -1609,3 +1611,48 @@ func TestCacheCaptureWriter_ImplicitStatus_CapturesContentType(t *testing.T) {
t.Errorf("contentType = %q, want %q (must be captured on the implicit-200 path too)", cw.contentType, "application/json")
}
}
// TestUnifiedCostMicros covers ruling (d)'s two-id price lookup order:
// canonical ("provider/model") first, falling back to bare — the upstream
// model id alone — only when canonical has no configured price at all, in
// neither overrides nor the built-in table.
func TestUnifiedCostMicros(t *testing.T) {
u := usage{prompt: 1_000_000, completion: 1_000_000}
cases := []struct {
overrides map[string]*ModelPricing
name string
canonical string
bare string
want int64
}{
{
name: "canonical override price wins over bare",
canonical: "openai/gpt-5",
bare: "gpt-5",
overrides: map[string]*ModelPricing{
"openai/gpt-5": {InputPerM: 1, OutputPerM: 2},
},
want: 3_000_000, // (1M prompt * $1/M) + (1M completion * $2/M), in micros
},
{
name: "canonical unpriced falls back to bare's built-in price",
canonical: "unknown-provider/gpt-5",
bare: "gpt-5",
overrides: nil,
want: costMicros("gpt-5", u, nil),
},
{
name: "neither id priced returns zero",
canonical: "unknown-provider/unknown-model",
bare: "unknown-model",
overrides: nil,
want: 0,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, unifiedCostMicros(c.canonical, c.bare, u, c.overrides))
})
}
}
+59
View File
@@ -12,6 +12,8 @@ import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// goldenTestdataDir is where every fixture this file walks lives.
@@ -448,3 +450,60 @@ func TestAnthropicRequestFromOpenAI_MaxCompletionTokensPrecedence(t *testing.T)
t.Errorf("max_tokens = %v, want 200 (max_completion_tokens takes precedence)", got["max_tokens"])
}
}
// TestIsTruthy pins isTruthy's truthy-only semantics (per its own doc
// comment): only a present AND non-empty value counts as "the client
// actually asked for this" — nil, false, 0, "", and an empty map/slice
// must not trip a truthy-only unsupported-field check.
func TestIsTruthy(t *testing.T) {
cases := []struct {
v any
name string
want bool
}{
{name: "nil is falsy", v: nil, want: false},
{name: "bool true is truthy", v: true, want: true},
{name: "bool false is falsy", v: false, want: false},
{name: "nonzero float64 is truthy", v: float64(1.5), want: true},
{name: "zero float64 is falsy", v: float64(0), want: false},
{name: "nonempty string is truthy", v: "x", want: true},
{name: "empty string is falsy", v: "", want: false},
{name: "nonempty map is truthy", v: map[string]any{"a": 1}, want: true},
{name: "empty map is falsy", v: map[string]any{}, want: false},
{name: "nonempty slice is truthy", v: []any{1}, want: true},
{name: "empty slice is falsy", v: []any{}, want: false},
{name: "an unrecognized non-nil type is conservatively truthy", v: 42, want: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, isTruthy(c.v))
})
}
}
// TestToFloat64 covers every numeric shape toFloat64 accepts — the normal
// post-json.Unmarshal float64, plus the Go-native int/int64/float32 shapes
// a test or hand-built request can supply — and the fallback for anything
// else.
func TestToFloat64(t *testing.T) {
cases := []struct {
v any
name string
want float64
wantOK bool
}{
{name: "float64", v: float64(3.5), want: 3.5, wantOK: true},
{name: "float32", v: float32(2.5), want: 2.5, wantOK: true},
{name: "int", v: int(7), want: 7, wantOK: true},
{name: "int64", v: int64(9), want: 9, wantOK: true},
{name: "unsupported type returns ok=false", v: "7", want: 0, wantOK: false},
{name: "nil returns ok=false", v: nil, want: 0, wantOK: false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, ok := toFloat64(c.v)
assert.Equal(t, c.wantOK, ok)
assert.Equal(t, c.want, got)
})
}
}
+59
View File
@@ -9,6 +9,8 @@ import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// geminiGoldenTestdataDir is where every fixture this file walks lives.
@@ -508,3 +510,60 @@ func TestGeminiEmbeddingInputTexts_RejectsNonStringArrayEntry(t *testing.T) {
t.Fatalf("err = %v (%T), want *translateError", err, err)
}
}
// TestGeminiToolConfigFromOpenAI covers every tool_choice shape the request
// mapping table lists: the three recognized string modes, a function-name
// object, and the two "no mapping" cases (an unrecognized string, and an
// object without a valid function.name) that must return nil so the caller
// omits toolConfig entirely rather than sending a malformed one.
func TestGeminiToolConfigFromOpenAI(t *testing.T) {
cases := []struct {
tc any
want map[string]any
name string
}{
{
name: "none",
tc: "none",
want: map[string]any{"functionCallingConfig": map[string]any{"mode": "NONE"}},
},
{
name: "auto",
tc: "auto",
want: map[string]any{"functionCallingConfig": map[string]any{"mode": "AUTO"}},
},
{
name: "required",
tc: "required",
want: map[string]any{"functionCallingConfig": map[string]any{"mode": "ANY"}},
},
{
name: "function object names the allowed function",
tc: map[string]any{"type": "function", "function": map[string]any{"name": "get_weather"}},
want: map[string]any{"functionCallingConfig": map[string]any{
"mode": "ANY",
"allowedFunctionNames": []any{"get_weather"},
}},
},
{
name: "unrecognized string has no mapping",
tc: "bogus",
want: nil,
},
{
name: "object without a function.name has no mapping",
tc: map[string]any{"type": "function"},
want: nil,
},
{
name: "an unrecognized type has no mapping",
tc: 42,
want: nil,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
assert.Equal(t, c.want, geminiToolConfigFromOpenAI(c.tc))
})
}
}