Answer-time tests check the answer, not wall-clock milliseconds (closes #316)
check / check (push) Waiting to run
check / check (push) Waiting to run
The waf test that asks a question while every outside source hangs now runs it in a synctest bubble and checks that the answer comes after exactly the GeoJS wait, within the time limit. GeoJS is the silent stand-in; the lists are fetched outside the bubble. The routewatch test for a hanging routewatch checks that Route answers no_answer_yet instead of timing it. Model: opus-5-5
This commit is contained in:
1 parent
eba697af48
commit
0e62e7efc5
2 files changed
+53
-36
No files matched your search
@@ -291,12 +291,11 @@ func TestHangingRoutewatchNeverDelaysAndFailsTheQuery(t *testing.T) {
|
|||||||
<-r.Context().Done()
|
<-r.Context().Done()
|
||||||
})
|
})
|
||||||
|
|
||||||
start := time.Now()
|
// Route answers no_answer_yet while the query is under way, and
|
||||||
|
// request_failed once it has failed, after queryTimeout.
|
||||||
f.route()
|
if _, missing := f.route(); missing != routewatch.NoAnswerYet {
|
||||||
|
t.Errorf("Route gave %q while routewatch hangs, want %q",
|
||||||
if elapsed := time.Since(start); elapsed > time.Second {
|
missing, routewatch.NoAnswerYet)
|
||||||
t.Errorf("Route took %s while routewatch hangs", elapsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.waitFor(routewatch.RequestFailed)
|
f.waitFor(routewatch.RequestFailed)
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||||
|
|
||||||
"sneak.berlin/go/smallwebwaf/internal/bans"
|
"sneak.berlin/go/smallwebwaf/internal/bans"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||||
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
"sneak.berlin/go/smallwebwaf/internal/lookup/lookuptest"
|
||||||
@@ -30,6 +31,8 @@ const (
|
|||||||
answerTimeLimit = 2 * time.Second
|
answerTimeLimit = 2 * time.Second
|
||||||
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
// defaultLookupWait is the default of SWW_LOOKUP_TIMEOUT.
|
||||||
defaultLookupWait = 800 * time.Millisecond
|
defaultLookupWait = 800 * time.Millisecond
|
||||||
|
// defaultReputationWait is the default of SWW_REPUTATION_TIMEOUT.
|
||||||
|
defaultReputationWait = 2 * time.Second
|
||||||
// The header limits SPEC.md gives.
|
// The header limits SPEC.md gives.
|
||||||
cookieLimit = 50
|
cookieLimit = 50
|
||||||
headerFieldLimit = 100
|
headerFieldLimit = 100
|
||||||
@@ -628,7 +631,8 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
|||||||
) {
|
) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
held, askedGeoJS, _ := startHeldGeoJS(t)
|
// The lists' stand-in: one for GeoJS that answers nothing.
|
||||||
|
held, _, _ := startHeldGeoJS(t)
|
||||||
|
|
||||||
// A resolver that takes every query and answers none.
|
// A resolver that takes every query and answers none.
|
||||||
resolver, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", localhost+":0")
|
resolver, err := (&net.ListenConfig{}).ListenPacket(t.Context(), "udp", localhost+":0")
|
||||||
@@ -644,7 +648,7 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
|||||||
params.AbuseIPDBURL = silentAbuseIPDBURL
|
params.AbuseIPDBURL = silentAbuseIPDBURL
|
||||||
|
|
||||||
return waf.New(params)
|
return waf.New(params)
|
||||||
}, held, time.Now, map[string]string{
|
}, silentGeoJSURL, time.Now, map[string]string{
|
||||||
tagDeny: "country=KP", wafMode: block,
|
tagDeny: "country=KP", wafMode: block,
|
||||||
dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
|
dnsblZones: dnsblZone, dnsblResolver: resolver.LocalAddr().String(),
|
||||||
abuseIPDBKey: accountKey,
|
abuseIPDBKey: accountKey,
|
||||||
@@ -652,34 +656,48 @@ func TestWAFAnswersWithinTheTimeLimitWhileEveryOutsideSourceHangs(
|
|||||||
crowdSecURL: held, crowdSecKey: bouncerKey,
|
crowdSecURL: held, crowdSecKey: bouncerKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
// AbuseIPDB is asked only about a client with an offence.
|
// The lists are fetched outside the synctest bubble below: a wait on the
|
||||||
group := netip.MustParsePrefix(client + "/32")
|
// network inside it would keep its clock from moving on.
|
||||||
server.Limiter.AddToHistory(group, time.Now(), ratelimit.Request{Attack: true})
|
|
||||||
|
|
||||||
go server.Lists.Run(t.Context())
|
go server.Lists.Run(t.Context())
|
||||||
|
|
||||||
status, took := askDirectly(t, server, func(req *http.Request) {
|
// In the bubble, as TestRequestWaitsAsLongAsTheLookupTimeoutSays says, the
|
||||||
req.Header.Set("User-Agent", "test")
|
// answer takes exactly as long as waf waits, however busy the host is.
|
||||||
req.Header.Set("Accept", "*/*")
|
// GeoJS and AbuseIPDB never answer there, without the network. The query to
|
||||||
req.Host = siteA
|
// the DNS blocklist starts only after the wait for GeoJS.
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
// AbuseIPDB is asked only about a client with an offence.
|
||||||
|
group := netip.MustParsePrefix(client + "/32")
|
||||||
|
server.Limiter.AddToHistory(group, time.Now(), ratelimit.Request{Attack: true})
|
||||||
|
|
||||||
|
status, took := askDirectly(t, server, func(req *http.Request) {
|
||||||
|
req.Header.Set("User-Agent", "test")
|
||||||
|
req.Header.Set("Accept", "*/*")
|
||||||
|
req.Host = siteA
|
||||||
|
})
|
||||||
|
|
||||||
|
// The wait for GeoJS decides when the answer comes.
|
||||||
|
if status != http.StatusOK || took != defaultLookupWait || took >= answerTimeLimit {
|
||||||
|
t.Errorf("answered %d after %s, want %d after %s, within %s; output:\n%s",
|
||||||
|
status, took, http.StatusOK, defaultLookupWait, answerTimeLimit, out.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each outside source was asked, and none has answered. GeoJS is asked
|
||||||
|
// only about the clients waiting for it, here client alone.
|
||||||
|
if requests := testutil.ToFloat64(server.Metrics.GeoJSRequests); requests != 1 {
|
||||||
|
t.Errorf("%v requests to GeoJS, want 1", requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
if queries := server.DNSBL.Queries(dnsblZone); queries != 1 {
|
||||||
|
t.Errorf("%d queries to the DNS blocklist, want 1", queries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if checked := server.AbuseIPDB.Checked(); checked != 1 {
|
||||||
|
t.Errorf("%d checks with AbuseIPDB, want 1", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The check with AbuseIPDB is abandoned before the bubble ends. The
|
||||||
|
// bubble also waits for the query to the DNS blocklist to fail, which
|
||||||
|
// it does on the real clock, since the query waits on the network.
|
||||||
|
time.Sleep(defaultReputationWait)
|
||||||
})
|
})
|
||||||
|
|
||||||
if status != http.StatusOK || took >= answerTimeLimit {
|
|
||||||
t.Errorf("answered %d after %s, want %d within %s; output:\n%s",
|
|
||||||
status, took, http.StatusOK, answerTimeLimit, out.text())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Each outside source was asked, and none has answered. The stand-in for
|
|
||||||
// GeoJS is the lists' too, which name no address.
|
|
||||||
if !slices.Contains(askedGeoJS(), client) {
|
|
||||||
t.Errorf("GeoJS was asked about %q, not %s", askedGeoJS(), client)
|
|
||||||
}
|
|
||||||
|
|
||||||
if queries := server.DNSBL.Queries(dnsblZone); queries != 1 {
|
|
||||||
t.Errorf("%d queries to the DNS blocklist, want 1", queries)
|
|
||||||
}
|
|
||||||
|
|
||||||
if checked := server.AbuseIPDB.Checked(); checked != 1 {
|
|
||||||
t.Errorf("%d checks with AbuseIPDB, want 1", checked)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user