mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-26 12:14:17 +00:00
9d4ad84(read-only app password -> 403) ande6959e6(bounded HTTP clients on the token path) both landed with no test at all. These are the ones a regression would be silent in: a revert of either leaves every existing test green. Each test was mutation-verified against the defect it claims to catch, in a throwaway worktree, and required to fail: * revert ResolveHoldDID to http.DefaultClient -> SlowHoldIsCutOff fails * revert getServiceAuth to http.DefaultClient -> SlowPDSIsCutOff fails * NewSessionValidator back to &http.Client{} -> ClientsAreBounded fails * drop the InsufficientScope classification -> IsClassified fails * drop the handler's errors.Is branch -> Returns403 fails, and the body it returns is the exact retry-inviting 503 UNAVAILABLE the commit exists to remove The slow-path tests wait on an outer deadline rather than on the call itself. With an unbounded client these calls never return, so a test that simply awaited the result would hang the suite instead of failing it, and a hung suite reports nothing. The client caps are asserted twice on purpose: once as a field value, which guards the production 10s/15s numbers, and once functionally, which proves the call site routes through the bounded client rather than merely declaring one. Neither half catches the other's regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
73 lines
2.8 KiB
Go
73 lines
2.8 KiB
Go
package atproto
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ResolveHoldDID used http.DefaultClient, which has no timeout, so an
|
|
// unreachable hold held /auth/token open indefinitely — well past Docker's own
|
|
// token-fetch deadline, with nothing to cut it off.
|
|
//
|
|
// The client is package-level, so this also fixes every other ResolveHoldDID
|
|
// caller (GC, Jetstream backfill, the hold-health worker). That widened blast
|
|
// radius is the reason the cap is asserted here rather than only at the token
|
|
// path.
|
|
func TestResolveHoldDID_UsesBoundedClient(t *testing.T) {
|
|
if holdDIDResolveClient.Timeout != 10*time.Second {
|
|
t.Errorf("holdDIDResolveClient.Timeout = %v, want 10s — an unbounded client here stalls /auth/token, GC and Jetstream alike",
|
|
holdDIDResolveClient.Timeout)
|
|
}
|
|
}
|
|
|
|
// The field assertion above proves the client is configured; this proves
|
|
// ResolveHoldDID actually routes through it. Reverting the call site to
|
|
// http.DefaultClient leaves the assertion above green and fails this one.
|
|
func TestResolveHoldDID_SlowHoldIsCutOff(t *testing.T) {
|
|
// The hold never answers until the test releases it. httptest.Server.Close
|
|
// blocks on in-flight handlers, so the release has to happen before Close
|
|
// or the teardown pays the full stall it is simulating.
|
|
release := make(chan struct{})
|
|
hold := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
<-release
|
|
_, _ = w.Write([]byte("did:web:hold.example.com"))
|
|
}))
|
|
t.Cleanup(func() { close(release); hold.Close() })
|
|
|
|
restore := holdDIDResolveClient
|
|
holdDIDResolveClient = &http.Client{Timeout: 100 * time.Millisecond}
|
|
t.Cleanup(func() { holdDIDResolveClient = restore })
|
|
|
|
type result struct {
|
|
err error
|
|
elapsed time.Duration
|
|
}
|
|
done := make(chan result, 1)
|
|
go func() {
|
|
start := time.Now()
|
|
_, err := ResolveHoldDID(context.Background(), hold.URL)
|
|
done <- result{err, time.Since(start)}
|
|
}()
|
|
|
|
// The deadline is what keeps an unbounded client from hanging the suite
|
|
// instead of failing it: with http.DefaultClient this call never returns on
|
|
// its own, and a test that hangs reports nothing useful.
|
|
select {
|
|
case res := <-done:
|
|
if res.err == nil {
|
|
t.Fatal("expected a timeout error from an unresponsive hold, got none")
|
|
}
|
|
// A whole second is ten times the configured cap, so this distinguishes
|
|
// "the bounded client was used" from "the request ran to completion"
|
|
// without being flaky under load.
|
|
if res.elapsed > time.Second {
|
|
t.Errorf("ResolveHoldDID took %v with a 100ms client cap — the call is not going through holdDIDResolveClient", res.elapsed)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("ResolveHoldDID never returned against a hold that never answers — the call is not going through holdDIDResolveClient")
|
|
}
|
|
}
|