Files
at-container-registry/pkg/atproto/resolver_timeout_test.go
T
Evan JarrettandClaude Opus 5 ce01e47ba6 auth: cover the two batch-09 commits that shipped without tests
9d4ad84 (read-only app password -> 403) and e6959e6 (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
2026-08-25 16:34:25 -05:00

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")
}
}