Run the GeoJS lookup tests on a clock the test controls #74
@@ -0,0 +1,9 @@
|
||||
package lookup
|
||||
|
||||
import "net/http"
|
||||
|
||||
// SetTransport has g's requests to GeoJS go through transport instead of
|
||||
// the network.
|
||||
func (g *GeoJS) SetTransport(transport http.RoundTripper) {
|
||||
g.httpClient.Transport = transport
|
||||
}
|
||||
+254
-225
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||
@@ -26,80 +27,87 @@ const (
|
||||
leftOut = "203.0.113.7"
|
||||
// timeout is how long a new client waits for its answer.
|
||||
timeout = time.Second
|
||||
// waitLimit bounds how long a test waits for what should happen.
|
||||
waitLimit = 10 * time.Second
|
||||
// pollInterval is how often a test looks again.
|
||||
pollInterval = 10 * time.Millisecond
|
||||
// week is how long an answer is kept.
|
||||
week = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// The tests that have GeoJS asked run in a synctest bubble, where the time
|
||||
// package runs on a clock of the test's own: a wait lasts exactly as long
|
||||
// as it should, however slowly the test process runs, and synctest.Wait
|
||||
// returns once g has done all it can before time passes. The stand-in for
|
||||
// GeoJS answers without the network, since a request waiting on the
|
||||
// network would keep that clock from moving on.
|
||||
|
||||
func TestKeptAnswerIsUsedFor7DaysThenAskedAgain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
placed := netip.MustParsePrefix("203.0.113.9/32")
|
||||
notPlaced := netip.MustParsePrefix(unplaced + "/32")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
placed := netip.MustParsePrefix("203.0.113.9/32")
|
||||
notPlaced := netip.MustParsePrefix(unplaced + "/32")
|
||||
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantCountry(t, g, notPlaced, "")
|
||||
wantRequests(t, geojs, 2)
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantCountry(t, g, notPlaced, "")
|
||||
wantRequests(t, geojs, 2)
|
||||
|
||||
// An answer without a country is kept too.
|
||||
clock.advance(week - time.Second)
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantCountry(t, g, notPlaced, "")
|
||||
wantRequests(t, geojs, 2)
|
||||
// An answer without a country is kept too.
|
||||
clock.advance(week - time.Second)
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantCountry(t, g, notPlaced, "")
|
||||
wantRequests(t, geojs, 2)
|
||||
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantRequests(t, geojs, 3)
|
||||
wantAsked(t, geojs, 2, "203.0.113.9")
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantRequests(t, geojs, 3)
|
||||
wantAsked(t, geojs, 2, "203.0.113.9")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
client := netip.MustParsePrefix("203.0.113.9/32")
|
||||
|
||||
// The client comes while GeoJS is asked about an earlier client, which
|
||||
// it answers most of a second later. It is then asked about the client
|
||||
// and does not answer: that request is abandoned a second after it
|
||||
// began, well after the client's wait is over.
|
||||
geojs.set(answeringSlowly)
|
||||
// The client comes while GeoJS is asked about an earlier client, which
|
||||
// it answers most of a second later. It is then asked about the client
|
||||
// and does not answer: that request is abandoned a second after it
|
||||
// began, well after the client's wait is over.
|
||||
geojs.set(answeringSlowly)
|
||||
|
||||
var earlier sync.WaitGroup
|
||||
var earlier sync.WaitGroup
|
||||
|
||||
earlier.Go(func() { g.Country(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) })
|
||||
defer earlier.Wait()
|
||||
earlier.Go(func() { g.Country(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) })
|
||||
defer earlier.Wait()
|
||||
|
||||
waitForRequests(t, geojs, 1)
|
||||
geojs.set(hanging)
|
||||
waitForRequests(t, geojs, 1)
|
||||
geojs.set(hanging)
|
||||
|
||||
began := time.Now()
|
||||
began := time.Now()
|
||||
|
||||
wantCountry(t, g, client, "")
|
||||
wantCountry(t, g, client, "")
|
||||
|
||||
took := time.Since(began)
|
||||
if took < timeout || took > timeout+timeout/2 {
|
||||
t.Errorf("waited %s for the answer, want %s", took, timeout)
|
||||
}
|
||||
took := time.Since(began)
|
||||
if took != timeout {
|
||||
t.Errorf("waited %s for the answer, want %s", took, timeout)
|
||||
}
|
||||
|
||||
// Its next request does not wait.
|
||||
began = time.Now()
|
||||
// Its next request does not wait.
|
||||
began = time.Now()
|
||||
|
||||
wantCountry(t, g, client, "")
|
||||
wantCountry(t, g, client, "")
|
||||
|
||||
took = time.Since(began)
|
||||
if took > timeout/2 {
|
||||
t.Errorf("waited %s again, want no wait", took)
|
||||
}
|
||||
took = time.Since(began)
|
||||
if took != 0 {
|
||||
t.Errorf("waited %s again, want no wait", took)
|
||||
}
|
||||
|
||||
// Once GeoJS answers, the client is asked about again in the
|
||||
// background, and has its country.
|
||||
geojs.set(answering)
|
||||
waitForCountry(t, g, clock, client, germany)
|
||||
// Once GeoJS answers, the client is asked about again in the
|
||||
// background, and has its country.
|
||||
geojs.set(answering)
|
||||
waitForCountry(t, g, clock, client, germany)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
|
||||
@@ -118,33 +126,35 @@ func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
other := netip.MustParsePrefix("203.0.113.1/32")
|
||||
client := netip.MustParsePrefix(leftOut + "/32")
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
other := netip.MustParsePrefix("203.0.113.1/32")
|
||||
client := netip.MustParsePrefix(leftOut + "/32")
|
||||
|
||||
// GeoJS fails, and is left alone for a second while the client
|
||||
// comes too, so that the next request asks about both.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, other, "")
|
||||
wantCountry(t, g, client, "")
|
||||
// GeoJS fails, and is left alone for a second while the client
|
||||
// comes too, so that the next request asks about both.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, other, "")
|
||||
wantCountry(t, g, client, "")
|
||||
|
||||
geojs.set(tc.answers)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, other, "")
|
||||
waitForRequests(t, geojs, 2)
|
||||
geojs.set(tc.answers)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, other, "")
|
||||
waitForRequests(t, geojs, 2)
|
||||
|
||||
// The answer counts as a failure, and the client is asked about
|
||||
// again, with the other client only if the answer left it out too.
|
||||
geojs.set(answering)
|
||||
waitForCountry(t, g, clock, client, germany)
|
||||
wantCountry(t, g, other, germany)
|
||||
wantRequests(t, geojs, 3)
|
||||
// The answer counts as a failure, and the client is asked about
|
||||
// again, with the other client only if the answer left it out too.
|
||||
geojs.set(answering)
|
||||
waitForCountry(t, g, clock, client, germany)
|
||||
wantCountry(t, g, other, germany)
|
||||
wantRequests(t, geojs, 3)
|
||||
|
||||
if tc.named {
|
||||
wantAsked(t, geojs, 2, leftOut)
|
||||
} else {
|
||||
wantAsked(t, geojs, 2, leftOut, "203.0.113.1")
|
||||
}
|
||||
if tc.named {
|
||||
wantAsked(t, geojs, 2, leftOut)
|
||||
} else {
|
||||
wantAsked(t, geojs, 2, leftOut, "203.0.113.1")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -152,173 +162,190 @@ func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
|
||||
func TestRedirectCountsAsFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, _, g := start(t)
|
||||
geojs.set(redirecting)
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, _, g := start()
|
||||
geojs.set(redirecting)
|
||||
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "")
|
||||
wantRequests(t, geojs, 1)
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "")
|
||||
wantRequests(t, geojs, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCountryIsKeptInCapitals(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, _, g := start(t)
|
||||
geojs.set(answeringInLowerCase)
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, _, g := start()
|
||||
geojs.set(answeringInLowerCase)
|
||||
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), germany)
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), germany)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFailureIsLoggedWithoutTheAddressesAskedAbout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var log strings.Builder
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var log strings.Builder
|
||||
|
||||
// Nothing listens on port 1, so asking GeoJS fails.
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: "http://127.0.0.1:1",
|
||||
Now: time.Now,
|
||||
ProcessLog: slog.New(slog.NewTextHandler(&log, nil)),
|
||||
// GeoJS does not answer, so the request to it is abandoned, and fails.
|
||||
geojs := &standIn{answers: hanging}
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: lookup.URL,
|
||||
Now: time.Now,
|
||||
ProcessLog: slog.New(slog.NewTextHandler(&log, nil)),
|
||||
})
|
||||
g.SetTransport(geojs)
|
||||
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "")
|
||||
synctest.Wait()
|
||||
|
||||
logged := log.String()
|
||||
if !strings.Contains(logged, "asking GeoJS failed") ||
|
||||
strings.Contains(logged, "203.0.113.9") {
|
||||
t.Errorf("logged %q, want the failure without the address asked about", logged)
|
||||
}
|
||||
})
|
||||
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "")
|
||||
|
||||
logged := log.String()
|
||||
if !strings.Contains(logged, "asking GeoJS failed") ||
|
||||
strings.Contains(logged, "203.0.113.9") {
|
||||
t.Errorf("logged %q, want the failure without the address asked about", logged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitingClientsAreAskedAboutInOneRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
|
||||
// GeoJS fails, and is then left alone for a second, while three more
|
||||
// clients come. An IPv6 client is a /64, and GeoJS is asked about its
|
||||
// first address.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.1/32"), "")
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.2/32"), "")
|
||||
wantCountry(t, g, netip.MustParsePrefix("2001:db8:1:2::/64"), "")
|
||||
wantRequests(t, geojs, 1)
|
||||
// GeoJS fails, and is then left alone for a second, while three more
|
||||
// clients come. An IPv6 client is a /64, and GeoJS is asked about its
|
||||
// first address.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.1/32"), "")
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.2/32"), "")
|
||||
wantCountry(t, g, netip.MustParsePrefix("2001:db8:1:2::/64"), "")
|
||||
wantRequests(t, geojs, 1)
|
||||
|
||||
geojs.set(answering)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.3/32"), germany)
|
||||
wantRequests(t, geojs, 2)
|
||||
wantAsked(t, geojs, 1, "203.0.113.1", "203.0.113.2", "2001:db8:1:2::", "203.0.113.3")
|
||||
geojs.set(answering)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, netip.MustParsePrefix("203.0.113.3/32"), germany)
|
||||
wantRequests(t, geojs, 2)
|
||||
wantAsked(t, geojs, 1, "203.0.113.1", "203.0.113.2", "2001:db8:1:2::", "203.0.113.3")
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeptAnswersUnaffectedWhileGeoJSFailsAndAskedAgainWithBackoff(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
clients := newClients()
|
||||
kept := clients()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
clients := newClients()
|
||||
kept := clients()
|
||||
|
||||
wantCountry(t, g, kept, germany)
|
||||
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, kept, germany)
|
||||
wantRequests(t, geojs, 1)
|
||||
|
||||
// Each failure leaves GeoJS alone twice as long as the one before, up
|
||||
// to five minutes. New clients meanwhile count as not found, and the
|
||||
// client with a kept answer still gets its country, without GeoJS being
|
||||
// asked.
|
||||
requests := 1
|
||||
|
||||
for _, delay := range []time.Duration{
|
||||
time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second,
|
||||
16 * time.Second, 32 * time.Second, 64 * time.Second, 128 * time.Second,
|
||||
256 * time.Second, 5 * time.Minute, 5 * time.Minute,
|
||||
} {
|
||||
wantCountry(t, g, clients(), "")
|
||||
|
||||
requests++
|
||||
wantRequests(t, geojs, requests)
|
||||
|
||||
clock.advance(delay - time.Millisecond)
|
||||
wantCountry(t, g, clients(), "")
|
||||
wantCountry(t, g, kept, germany)
|
||||
wantRequests(t, geojs, requests)
|
||||
|
||||
clock.advance(time.Millisecond)
|
||||
}
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, kept, germany)
|
||||
wantRequests(t, geojs, 1)
|
||||
|
||||
// Once GeoJS answers again, it is asked about every client waiting.
|
||||
geojs.set(answering)
|
||||
wantCountry(t, g, clients(), germany)
|
||||
wantRequests(t, geojs, requests+1)
|
||||
// Each failure leaves GeoJS alone twice as long as the one before, up
|
||||
// to five minutes. New clients meanwhile count as not found, and the
|
||||
// client with a kept answer still gets its country, without GeoJS being
|
||||
// asked.
|
||||
requests := 1
|
||||
|
||||
asked := waitForRequests(t, geojs, requests+1)
|
||||
if len(asked[requests]) != 23 {
|
||||
t.Errorf("GeoJS was asked about %d clients, want 23", len(asked[requests]))
|
||||
}
|
||||
for _, delay := range []time.Duration{
|
||||
time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second,
|
||||
16 * time.Second, 32 * time.Second, 64 * time.Second, 128 * time.Second,
|
||||
256 * time.Second, 5 * time.Minute, 5 * time.Minute,
|
||||
} {
|
||||
wantCountry(t, g, clients(), "")
|
||||
|
||||
requests++
|
||||
wantRequests(t, geojs, requests)
|
||||
|
||||
clock.advance(delay - time.Millisecond)
|
||||
wantCountry(t, g, clients(), "")
|
||||
wantCountry(t, g, kept, germany)
|
||||
wantRequests(t, geojs, requests)
|
||||
|
||||
clock.advance(time.Millisecond)
|
||||
}
|
||||
|
||||
// Once GeoJS answers again, it is asked about every client waiting.
|
||||
geojs.set(answering)
|
||||
wantCountry(t, g, clients(), germany)
|
||||
wantRequests(t, geojs, requests+1)
|
||||
|
||||
asked := waitForRequests(t, geojs, requests+1)
|
||||
if len(asked[requests]) != 23 {
|
||||
t.Errorf("GeoJS was asked about %d clients, want 23", len(asked[requests]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAtMost200AddressesInOneRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
clients := newClients()
|
||||
first := clients()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
clients := newClients()
|
||||
first := clients()
|
||||
|
||||
// 201 clients wait while GeoJS is left alone after a failure.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, first, "")
|
||||
// 201 clients wait while GeoJS is left alone after a failure.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, first, "")
|
||||
|
||||
for range 200 {
|
||||
wantCountry(t, g, clients(), "")
|
||||
}
|
||||
for range 200 {
|
||||
wantCountry(t, g, clients(), "")
|
||||
}
|
||||
|
||||
// The first one's next request has GeoJS asked again.
|
||||
geojs.set(answering)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, first, "")
|
||||
// The first one's next request has GeoJS asked again.
|
||||
geojs.set(answering)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, first, "")
|
||||
|
||||
asked := waitForRequests(t, geojs, 3)
|
||||
if len(asked[1]) != 200 || len(asked[2]) != 1 {
|
||||
t.Errorf("GeoJS was asked about %d and then %d clients, want 200 and 1",
|
||||
len(asked[1]), len(asked[2]))
|
||||
}
|
||||
asked := waitForRequests(t, geojs, 3)
|
||||
if len(asked[1]) != 200 || len(asked[2]) != 1 {
|
||||
t.Errorf("GeoJS was asked about %d and then %d clients, want 200 and 1",
|
||||
len(asked[1]), len(asked[2]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAtMost10000ClientsWait(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
clients := newClients()
|
||||
first := clients()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
geojs, clock, g := start()
|
||||
clients := newClients()
|
||||
first := clients()
|
||||
|
||||
// 10,000 clients wait while GeoJS is left alone after a failure, and
|
||||
// one more cannot join them.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, first, "")
|
||||
// 10,000 clients wait while GeoJS is left alone after a failure, and
|
||||
// one more cannot join them.
|
||||
geojs.set(failing)
|
||||
wantCountry(t, g, first, "")
|
||||
|
||||
for range 9999 {
|
||||
wantCountry(t, g, clients(), "")
|
||||
}
|
||||
|
||||
extra := clients()
|
||||
wantCountry(t, g, extra, "")
|
||||
|
||||
// The first one's next request has GeoJS asked about the 10,000, 200
|
||||
// at a time, and not about the one more.
|
||||
geojs.set(answering)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, first, "")
|
||||
|
||||
asked := waitForRequests(t, geojs, 51)
|
||||
for i, request := range asked {
|
||||
if slices.Contains(request, extra.Addr().String()) {
|
||||
t.Errorf("request %d asked about %s", i, extra.Addr())
|
||||
for range 9999 {
|
||||
wantCountry(t, g, clients(), "")
|
||||
}
|
||||
}
|
||||
|
||||
// With room among those waiting, it is asked about.
|
||||
wantCountry(t, g, extra, germany)
|
||||
extra := clients()
|
||||
wantCountry(t, g, extra, "")
|
||||
|
||||
// The first one's next request has GeoJS asked about the 10,000, 200
|
||||
// at a time, and not about the one more.
|
||||
geojs.set(answering)
|
||||
clock.advance(time.Second)
|
||||
wantCountry(t, g, first, "")
|
||||
|
||||
asked := waitForRequests(t, geojs, 51)
|
||||
for i, request := range asked {
|
||||
if slices.Contains(request, extra.Addr().String()) {
|
||||
t.Errorf("request %d asked about %s", i, extra.Addr())
|
||||
}
|
||||
}
|
||||
|
||||
// With room among those waiting, it is asked about.
|
||||
wantCountry(t, g, extra, germany)
|
||||
})
|
||||
}
|
||||
|
||||
// How the stand-in for GeoJS answers.
|
||||
@@ -337,13 +364,25 @@ const (
|
||||
// standIn is a stand-in for GeoJS. It notes the addresses each request
|
||||
// asks about.
|
||||
type standIn struct {
|
||||
server *httptest.Server
|
||||
|
||||
mu sync.Mutex
|
||||
answers int
|
||||
requests [][]string
|
||||
}
|
||||
|
||||
// RoundTrip has the stand-in answer req, in place of the network. A request
|
||||
// abandoned before the stand-in answers fails, as over the network.
|
||||
func (s *standIn) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
answer := httptest.NewRecorder()
|
||||
s.ServeHTTP(answer, req)
|
||||
|
||||
err := req.Context().Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return answer.Result(), nil
|
||||
}
|
||||
|
||||
// ServeHTTP answers a request about the addresses in its ip parameter.
|
||||
func (s *standIn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
addrs := strings.Split(r.URL.Query().Get("ip"), ",")
|
||||
@@ -422,7 +461,8 @@ func (s *standIn) asked() [][]string {
|
||||
return slices.Clone(s.requests)
|
||||
}
|
||||
|
||||
// testClock is a clock the test sets.
|
||||
// testClock is a clock the test sets. GeoJS tells the time by it, while
|
||||
// waits run on the bubble's clock.
|
||||
type testClock struct {
|
||||
mu sync.Mutex
|
||||
now time.Time
|
||||
@@ -444,21 +484,17 @@ func (c *testClock) advance(d time.Duration) {
|
||||
c.now = c.now.Add(d)
|
||||
}
|
||||
|
||||
// start starts a stand-in for GeoJS that answers, and returns it, a
|
||||
// clock, and a GeoJS asking it by that clock.
|
||||
func start(t *testing.T) (*standIn, *testClock, *lookup.GeoJS) {
|
||||
t.Helper()
|
||||
|
||||
// start returns a stand-in for GeoJS that answers, a clock, and a GeoJS
|
||||
// asking the stand-in by that clock.
|
||||
func start() (*standIn, *testClock, *lookup.GeoJS) {
|
||||
geojs := &standIn{}
|
||||
geojs.server = httptest.NewServer(geojs)
|
||||
t.Cleanup(geojs.server.Close)
|
||||
|
||||
clock := &testClock{now: time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)}
|
||||
g := lookup.New(lookup.Params{
|
||||
URL: geojs.server.URL,
|
||||
URL: lookup.URL,
|
||||
Now: clock.Now,
|
||||
ProcessLog: slog.New(slog.DiscardHandler),
|
||||
})
|
||||
g.SetTransport(geojs)
|
||||
|
||||
return geojs, clock, g
|
||||
}
|
||||
@@ -513,41 +549,34 @@ func wantAsked(t *testing.T, geojs *standIn, i int, want ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// waitForRequests waits for GeoJS to have had count requests, and returns
|
||||
// the addresses each asked about.
|
||||
// waitForRequests waits until g has done all it can before time passes,
|
||||
// checks that GeoJS has had count requests, and returns the addresses each
|
||||
// asked about.
|
||||
func waitForRequests(t *testing.T, geojs *standIn, count int) [][]string {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(waitLimit)
|
||||
for time.Now().Before(deadline) {
|
||||
asked := geojs.asked()
|
||||
if len(asked) >= count {
|
||||
return asked
|
||||
}
|
||||
synctest.Wait()
|
||||
|
||||
time.Sleep(pollInterval)
|
||||
asked := geojs.asked()
|
||||
if len(asked) != count {
|
||||
t.Fatalf("GeoJS had %d requests, want %d", len(asked), count)
|
||||
}
|
||||
|
||||
t.Fatalf("fewer than %d requests to GeoJS after %s", count, waitLimit)
|
||||
|
||||
return nil
|
||||
return asked
|
||||
}
|
||||
|
||||
// waitForCountry waits for g to give client the country want, moving the
|
||||
// clock on a minute at a time, so that GeoJS is asked again after a
|
||||
// failure.
|
||||
// waitForCountry lets a request to GeoJS under way be abandoned, and moves
|
||||
// the clock on a minute, so that GeoJS may be asked again after a failure.
|
||||
// It then checks that client's next request does not wait but has it asked
|
||||
// about again in the background, after which g gives it the country want.
|
||||
func waitForCountry(
|
||||
t *testing.T, g *lookup.GeoJS, clock *testClock, client netip.Prefix, want string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(waitLimit)
|
||||
for g.Country(t.Context(), client) != want {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("%s is not in %q after %s", client, want, waitLimit)
|
||||
}
|
||||
|
||||
clock.advance(time.Minute)
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
time.Sleep(timeout)
|
||||
clock.advance(time.Minute)
|
||||
wantCountry(t, g, client, "")
|
||||
synctest.Wait()
|
||||
wantCountry(t, g, client, want)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/lookup"
|
||||
@@ -12,30 +13,32 @@ import (
|
||||
func TestSnapshotHoldsEachAnswerAndWhenItWasLastUsed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, clock, g := start(t)
|
||||
placed := netip.MustParsePrefix("203.0.113.9/32")
|
||||
notPlaced := netip.MustParsePrefix(unplaced + "/32")
|
||||
asked := clock.Now()
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
_, clock, g := start()
|
||||
placed := netip.MustParsePrefix("203.0.113.9/32")
|
||||
notPlaced := netip.MustParsePrefix(unplaced + "/32")
|
||||
asked := clock.Now()
|
||||
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantCountry(t, g, notPlaced, "")
|
||||
wantCountry(t, g, placed, germany)
|
||||
wantCountry(t, g, notPlaced, "")
|
||||
|
||||
clock.advance(time.Hour)
|
||||
wantCountry(t, g, placed, germany)
|
||||
clock.advance(time.Hour)
|
||||
wantCountry(t, g, placed, germany)
|
||||
|
||||
want := []lookup.Answer{
|
||||
{Client: notPlaced, Country: "", Answered: asked, Used: asked},
|
||||
{Client: placed, Country: germany, Answered: asked, Used: asked.Add(time.Hour)},
|
||||
}
|
||||
if got := g.Snapshot(); !slices.Equal(got, want) {
|
||||
t.Errorf("snapshot\n%+v\nwant\n%+v", got, want)
|
||||
}
|
||||
want := []lookup.Answer{
|
||||
{Client: notPlaced, Country: "", Answered: asked, Used: asked},
|
||||
{Client: placed, Country: germany, Answered: asked, Used: asked.Add(time.Hour)},
|
||||
}
|
||||
if got := g.Snapshot(); !slices.Equal(got, want) {
|
||||
t.Errorf("snapshot\n%+v\nwant\n%+v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadedAnswersAreKeptFor7DaysFromWhenGeoJSGaveThem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
geojs, clock, g := start(t)
|
||||
geojs, clock, g := start()
|
||||
now := clock.Now()
|
||||
kept := lookup.Answer{
|
||||
Client: netip.MustParsePrefix("203.0.113.9/32"),
|
||||
@@ -65,7 +68,7 @@ func TestLoadDropsTheAnswerUsedLongestAgoFirst(t *testing.T) {
|
||||
|
||||
const maxAnswers = 100000
|
||||
|
||||
_, clock, g := start(t)
|
||||
_, clock, g := start()
|
||||
now := clock.Now()
|
||||
|
||||
// lookups.json lists the answers by client. Here each was last used a
|
||||
|
||||
Reference in New Issue
Block a user