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
143 lines
3.9 KiB
Go
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
|
|
}
|