indexer/scraper/transport_test.go
Ricardo (XenGi) Band 63c6f4b761
Some checks failed
CI pipeline for ffd-indexer / checks (go test + golangci) (push) Failing after 26s
CI pipeline for ffd-indexer / build binaries + containers (push) Failing after 3s
CI pipeline for ffd-indexer / format (gofmt + nixfmt) (push) Failing after 3s
CI pipeline for ffd-indexer / integration (live scrape + Typesense) (push) Failing after 20s
🔥
2026-10-01 00:37:44 +02:00

143 lines
3.9 KiB
Go

package scraper
import (
"crypto/tls"
"crypto/x509"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestAdvertisesH3(t *testing.T) {
cases := []struct {
hdr http.Header
want bool
}{
{http.Header{"Alt-Svc": []string{`h3=":443";ma=2592000`}}, true},
{http.Header{"Alt-Svc": []string{`h2=":443";ma=2592000`}}, false},
{http.Header{}, false},
{nil, false},
}
for i, c := range cases {
if got := advertisesH3(c.hdr); got != c.want {
t.Errorf("case %d: advertisesH3(%v) = %v, want %v", i, c.hdr, got, c.want)
}
}
}
func TestH3CapabilityCache(t *testing.T) {
tr := newScraperTransport()
host := "example.test"
// Unknown host: try h3 first (newest-available-first).
if !tr.shouldTryH3(host) {
t.Errorf("unknown host should attempt h3 first")
}
// Positive capability keeps h3 enabled.
tr.recordH3(host, true)
if !tr.shouldTryH3(host) {
t.Errorf("capable host should attempt h3")
}
// Negative capability gates h3 off.
tr.recordH3(host, false)
if tr.shouldTryH3(host) {
t.Errorf("negative capability should disable h3")
}
// A forced host tries h3 even during a negative cool-down.
tr.recordH3(host, false)
tr.forceH3(host)
if !tr.shouldTryH3(host) {
t.Errorf("forced host should attempt h3")
}
}
func TestH3NegativeExpiry(t *testing.T) {
tr := newScraperTransport()
host := "expiry.test"
// A negative capability older than its TTL must re-enable h3.
tr.mu.Lock()
tr.cached[host] = h3Capability{capable: false, expireAt: time.Now().Add(-time.Second)}
tr.mu.Unlock()
if !tr.shouldTryH3(host) {
t.Errorf("expired negative capability should attempt h3 again")
}
}
func TestObserveAltSvcLearnsCapability(t *testing.T) {
tr := newScraperTransport()
host := "advertises.test"
// Alt-Svc advertisement records a positive capability.
tr.observeAltSvc(&http.Response{Header: http.Header{"Alt-Svc": []string{`h3=":443"`}}}, host)
tr.mu.Lock()
c := tr.cached[host]
tr.mu.Unlock()
if !c.capable {
t.Errorf("alt-svc advertisement should record a positive h3 capability")
}
// A response without alt-svc must not clear an existing capability.
tr.observeAltSvc(&http.Response{Header: http.Header{}}, host)
tr.mu.Lock()
c = tr.cached[host]
tr.mu.Unlock()
if !c.capable {
t.Errorf("no alt-svc must not clear an existing capability")
}
}
// TestRoundTripFallsBackToH12 verifies that when HTTP/3 is not available (the
// httptest server speaks only HTTP/1.1 over TLS — no QUIC), the transport
// falls back to the stdlib (h1/h2) transport and still returns a response.
func TestRoundTripFallsBackToH12(t *testing.T) {
srv := newTLSServer(t)
cfg := srv.TLS
cfg.NextProtos = []string{"http/1.1"} // keep it HTTP/1.1 so result is deterministic
tr := newScraperTransport()
// Make the h3 handshake fail fast instead of waiting the 5s const.
quicCfg := tr.h3Transport().QUICConfig
quicCfg.HandshakeIdleTimeout = 200 * time.Millisecond
// Trust the httptest server's self-signed cert in both sub-transports.
pool := x509.NewCertPool()
pool.AddCert(srv.Certificate())
cerr := &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: pool}
tr.h12.TLSClientConfig = cerr
tr.h3.TLSClientConfig = cerr
addr := srv.Listener.Addr().String()
req, err := http.NewRequest("GET", "https://"+addr+"/", nil)
if err != nil {
t.Fatalf("new request: %v", err)
}
resp, err := tr.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, want 200", resp.StatusCode)
}
// It must have worked over h12: HTTP/1.x (we pinned ALPN) and definitely
// not HTTP/3.
if resp.ProtoMajor >= 3 {
t.Errorf("unexpected proto %s (should have fallen back to h1/h2)", resp.Proto)
}
}
func newTLSServer(t *testing.T) *httptest.Server {
t.Helper()
s := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}))
s.StartTLS()
t.Cleanup(s.Close)
return s
}