Files
claude 1c13d2265b Score the boundary against wrong credentials, not just right ones (V-673)
Every shape of wrong credential gets a case: no header, wrong token, a prefix
of the token, the token with no scheme, and Basic. Plus the two the allowlist
exists for, /slots and its save action, and the caps.

The readiness test now posts to /v1/chat/completions. The allowlist sits in
front of the readiness check and answers 405 to a method mavgpud never
serves, so the old GET measured the allowlist rather than the 503.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESv8hqNPseYt1CnotZpqDz
2026-08-11 10:13:10 +04:00

181 lines
5.3 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
)
// ok is what the boundary is protecting: anything that reaches it has spent
// the card.
func ok(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }
func TestRequireTokenRefusesEveryWrongCredential(t *testing.T) {
h := requireToken("s3cret", 1<<20, http.HandlerFunc(ok))
cases := []struct {
name string
auth string
want int
}{
{"no header", "", http.StatusUnauthorized},
{"wrong token", "Bearer wrong", http.StatusUnauthorized},
{"prefix of the token", "Bearer s3cre", http.StatusUnauthorized},
{"token with no scheme", "s3cret", http.StatusUnauthorized},
{"basic auth", "Basic czNjcmV0", http.StatusUnauthorized},
{"right token", "Bearer s3cret", http.StatusTeapot},
{"scheme is case-insensitive", "bearer s3cret", http.StatusTeapot},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/health", nil)
if tc.auth != "" {
r.Header.Set("Authorization", tc.auth)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
if w.Code != tc.want {
t.Errorf("status %d, want %d", w.Code, tc.want)
}
})
}
}
// The 401 must not say which part was wrong. A probe that can tell a malformed
// header from a wrong token learns the header shape for free.
func TestUnauthorizedSaysNothingUseful(t *testing.T) {
h := requireToken("s3cret", 1<<20, http.HandlerFunc(ok))
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/health", nil))
if got := strings.TrimSpace(w.Body.String()); got != "unauthorized" {
t.Errorf("body %q, want %q", got, "unauthorized")
}
if got := w.Header().Get("WWW-Authenticate"); got != "Bearer" {
t.Errorf("WWW-Authenticate %q, want Bearer", got)
}
}
// The body cap applies to an authenticated request. An unauthenticated one
// never gets far enough to allocate anything.
func TestRequireTokenCapsTheBody(t *testing.T) {
var read error
h := requireToken("t", 8, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf := make([]byte, 64)
for read == nil {
if _, read = r.Body.Read(buf); read != nil {
break
}
}
}))
r := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(strings.Repeat("x", 4096)))
r.Header.Set("Authorization", "Bearer t")
h.ServeHTTP(httptest.NewRecorder(), r)
if read == nil || !strings.Contains(read.Error(), "too large") {
t.Errorf("read error %v, want the body cap", read)
}
}
func TestAllowlistRefusesWhatMavenDoesNotCall(t *testing.T) {
h := allowlist(http.HandlerFunc(ok))
cases := []struct {
method, path string
want int
}{
{http.MethodPost, "/v1/chat/completions", http.StatusTeapot},
{http.MethodGet, "/v1/models", http.StatusTeapot},
// /slots returns the prompts of whoever else is using the card, and
// its actions write files the request names.
{http.MethodGet, "/slots", http.StatusNotFound},
{http.MethodPost, "/slots/0?action=save", http.StatusNotFound},
{http.MethodGet, "/", http.StatusNotFound},
{http.MethodGet, "/v1/chat/completions", http.StatusMethodNotAllowed},
}
for _, tc := range cases {
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(tc.method, tc.path, nil))
if w.Code != tc.want {
t.Errorf("status %d, want %d", w.Code, tc.want)
}
})
}
}
func TestLimitInflightCapsConcurrency(t *testing.T) {
const cap = 2
var mu sync.Mutex
now, peak := 0, 0
release := make(chan struct{})
h := limitInflight(cap, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
now++
if now > peak {
peak = now
}
mu.Unlock()
<-release
mu.Lock()
now--
mu.Unlock()
}))
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil))
}()
}
// Let the first wave arrive, then drain. The assertion is the peak, and a
// peak that never reached the cap still cannot exceed it.
close(release)
wg.Wait()
if peak > cap {
t.Errorf("%d requests in flight at once, cap is %d", peak, cap)
}
}
func TestLoopbackListen(t *testing.T) {
cases := map[string]bool{
":8080": false, // the shipped default, and the whole LAN
"0.0.0.0:8080": false,
"[::]:8080": false,
"192.168.1.105:8080": false,
"127.0.0.1:8080": true,
"[::1]:8080": true,
"localhost:8080": true,
}
for addr, want := range cases {
if got := loopbackListen(addr); got != want {
t.Errorf("loopbackListen(%q) = %v, want %v", addr, got, want)
}
}
}
func TestReadToken(t *testing.T) {
dir := t.TempDir()
good := filepath.Join(dir, "tok")
if err := os.WriteFile(good, []byte(" abc123\n"), 0o600); err != nil {
t.Fatal(err)
}
got, err := readToken(good)
if err != nil || got != "abc123" {
t.Errorf("readToken = %q, %v; want abc123", got, err)
}
blank := filepath.Join(dir, "blank")
if err := os.WriteFile(blank, []byte("\n\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := readToken(blank); err == nil {
t.Error("an empty token file is not a token")
}
if _, err := readToken(filepath.Join(dir, "absent")); err == nil {
t.Error("a missing token file is not a token")
}
}