Run the GeoJS lookup tests on a clock the test controls (closes #73)
check / check (push) Successful in 3m32s

The tests that have GeoJS asked now run in a synctest bubble, so a wait
lasts exactly as long as it should however slowly the test process runs:
a new client's wait is checked to be exactly one second, and its next
request exactly no wait. A request waiting on the network would stop the
bubble's clock, so the stand-in for GeoJS now answers in place of the
network, through a transport that a test-only file lets the tests set.
The test of the failure log uses an abandoned request instead of a
closed port.

Model: opus-5-5
This commit was merged in pull request #74.
This commit is contained in:
2026-10-06 09:12:50 +02:00
parent df2c5042d2
commit 68f687cb0c
3 changed files with 283 additions and 242 deletions
+9
View File
@@ -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
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"testing/synctest"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
@@ -26,80 +27,87 @@ const (
leftOut = "203.0.113.7" leftOut = "203.0.113.7"
// timeout is how long a new client waits for its answer. // timeout is how long a new client waits for its answer.
timeout = time.Second 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 is how long an answer is kept.
week = 7 * 24 * time.Hour 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) { func TestKeptAnswerIsUsedFor7DaysThenAskedAgain(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
placed := netip.MustParsePrefix("203.0.113.9/32") geojs, clock, g := start()
notPlaced := netip.MustParsePrefix(unplaced + "/32") placed := netip.MustParsePrefix("203.0.113.9/32")
notPlaced := netip.MustParsePrefix(unplaced + "/32")
wantCountry(t, g, placed, germany) wantCountry(t, g, placed, germany)
wantCountry(t, g, notPlaced, "") wantCountry(t, g, notPlaced, "")
wantRequests(t, geojs, 2) wantRequests(t, geojs, 2)
// An answer without a country is kept too. // An answer without a country is kept too.
clock.advance(week - time.Second) clock.advance(week - time.Second)
wantCountry(t, g, placed, germany) wantCountry(t, g, placed, germany)
wantCountry(t, g, notPlaced, "") wantCountry(t, g, notPlaced, "")
wantRequests(t, geojs, 2) wantRequests(t, geojs, 2)
clock.advance(time.Second) clock.advance(time.Second)
wantCountry(t, g, placed, germany) wantCountry(t, g, placed, germany)
wantRequests(t, geojs, 3) wantRequests(t, geojs, 3)
wantAsked(t, geojs, 2, "203.0.113.9") wantAsked(t, geojs, 2, "203.0.113.9")
})
} }
func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) { func TestNewClientWaitsAtMostOneSecondThenCountsAsNotFound(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
client := netip.MustParsePrefix("203.0.113.9/32") geojs, clock, g := start()
client := netip.MustParsePrefix("203.0.113.9/32")
// The client comes while GeoJS is asked about an earlier client, which // 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 // 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 // and does not answer: that request is abandoned a second after it
// began, well after the client's wait is over. // began, well after the client's wait is over.
geojs.set(answeringSlowly) 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")) }) earlier.Go(func() { g.Country(t.Context(), netip.MustParsePrefix("203.0.113.1/32")) })
defer earlier.Wait() defer earlier.Wait()
waitForRequests(t, geojs, 1) waitForRequests(t, geojs, 1)
geojs.set(hanging) geojs.set(hanging)
began := time.Now() began := time.Now()
wantCountry(t, g, client, "") wantCountry(t, g, client, "")
took := time.Since(began) took := time.Since(began)
if took < timeout || took > timeout+timeout/2 { if took != timeout {
t.Errorf("waited %s for the answer, want %s", took, timeout) t.Errorf("waited %s for the answer, want %s", took, timeout)
} }
// Its next request does not wait. // Its next request does not wait.
began = time.Now() began = time.Now()
wantCountry(t, g, client, "") wantCountry(t, g, client, "")
took = time.Since(began) took = time.Since(began)
if took > timeout/2 { if took != 0 {
t.Errorf("waited %s again, want no wait", took) t.Errorf("waited %s again, want no wait", took)
} }
// Once GeoJS answers, the client is asked about again in the // Once GeoJS answers, the client is asked about again in the
// background, and has its country. // background, and has its country.
geojs.set(answering) geojs.set(answering)
waitForCountry(t, g, clock, client, germany) waitForCountry(t, g, clock, client, germany)
})
} }
func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) { func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
@@ -118,33 +126,35 @@ func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
other := netip.MustParsePrefix("203.0.113.1/32") geojs, clock, g := start()
client := netip.MustParsePrefix(leftOut + "/32") 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 // GeoJS fails, and is left alone for a second while the client
// comes too, so that the next request asks about both. // comes too, so that the next request asks about both.
geojs.set(failing) geojs.set(failing)
wantCountry(t, g, other, "") wantCountry(t, g, other, "")
wantCountry(t, g, client, "") wantCountry(t, g, client, "")
geojs.set(tc.answers) geojs.set(tc.answers)
clock.advance(time.Second) clock.advance(time.Second)
wantCountry(t, g, other, "") wantCountry(t, g, other, "")
waitForRequests(t, geojs, 2) waitForRequests(t, geojs, 2)
// The answer counts as a failure, and the client is asked about // 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. // again, with the other client only if the answer left it out too.
geojs.set(answering) geojs.set(answering)
waitForCountry(t, g, clock, client, germany) waitForCountry(t, g, clock, client, germany)
wantCountry(t, g, other, germany) wantCountry(t, g, other, germany)
wantRequests(t, geojs, 3) wantRequests(t, geojs, 3)
if tc.named { if tc.named {
wantAsked(t, geojs, 2, leftOut) wantAsked(t, geojs, 2, leftOut)
} else { } else {
wantAsked(t, geojs, 2, leftOut, "203.0.113.1") wantAsked(t, geojs, 2, leftOut, "203.0.113.1")
} }
})
}) })
} }
} }
@@ -152,173 +162,190 @@ func TestAddressLeftOutOfAnAnswerIsAskedAboutAgain(t *testing.T) {
func TestRedirectCountsAsFailure(t *testing.T) { func TestRedirectCountsAsFailure(t *testing.T) {
t.Parallel() t.Parallel()
geojs, _, g := start(t) synctest.Test(t, func(t *testing.T) {
geojs.set(redirecting) geojs, _, g := start()
geojs.set(redirecting)
wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "") wantCountry(t, g, netip.MustParsePrefix("203.0.113.9/32"), "")
wantRequests(t, geojs, 1) wantRequests(t, geojs, 1)
})
} }
func TestCountryIsKeptInCapitals(t *testing.T) { func TestCountryIsKeptInCapitals(t *testing.T) {
t.Parallel() t.Parallel()
geojs, _, g := start(t) synctest.Test(t, func(t *testing.T) {
geojs.set(answeringInLowerCase) 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) { func TestFailureIsLoggedWithoutTheAddressesAskedAbout(t *testing.T) {
t.Parallel() 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. // GeoJS does not answer, so the request to it is abandoned, and fails.
g := lookup.New(lookup.Params{ geojs := &standIn{answers: hanging}
URL: "http://127.0.0.1:1", g := lookup.New(lookup.Params{
Now: time.Now, URL: lookup.URL,
ProcessLog: slog.New(slog.NewTextHandler(&log, nil)), 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) { func TestWaitingClientsAreAskedAboutInOneRequest(t *testing.T) {
t.Parallel() 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 // 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 // clients come. An IPv6 client is a /64, and GeoJS is asked about its
// first address. // first address.
geojs.set(failing) geojs.set(failing)
wantCountry(t, g, netip.MustParsePrefix("203.0.113.1/32"), "") 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("203.0.113.2/32"), "")
wantCountry(t, g, netip.MustParsePrefix("2001:db8:1:2::/64"), "") wantCountry(t, g, netip.MustParsePrefix("2001:db8:1:2::/64"), "")
wantRequests(t, geojs, 1) wantRequests(t, geojs, 1)
geojs.set(answering) geojs.set(answering)
clock.advance(time.Second) clock.advance(time.Second)
wantCountry(t, g, netip.MustParsePrefix("203.0.113.3/32"), germany) wantCountry(t, g, netip.MustParsePrefix("203.0.113.3/32"), germany)
wantRequests(t, geojs, 2) wantRequests(t, geojs, 2)
wantAsked(t, geojs, 1, "203.0.113.1", "203.0.113.2", "2001:db8:1:2::", "203.0.113.3") 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) { func TestKeptAnswersUnaffectedWhileGeoJSFailsAndAskedAgainWithBackoff(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
clients := newClients() geojs, clock, g := start()
kept := clients() 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) 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. // Each failure leaves GeoJS alone twice as long as the one before, up
geojs.set(answering) // to five minutes. New clients meanwhile count as not found, and the
wantCountry(t, g, clients(), germany) // client with a kept answer still gets its country, without GeoJS being
wantRequests(t, geojs, requests+1) // asked.
requests := 1
asked := waitForRequests(t, geojs, requests+1) for _, delay := range []time.Duration{
if len(asked[requests]) != 23 { time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second,
t.Errorf("GeoJS was asked about %d clients, want 23", len(asked[requests])) 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) { func TestAtMost200AddressesInOneRequest(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
clients := newClients() geojs, clock, g := start()
first := clients() clients := newClients()
first := clients()
// 201 clients wait while GeoJS is left alone after a failure. // 201 clients wait while GeoJS is left alone after a failure.
geojs.set(failing) geojs.set(failing)
wantCountry(t, g, first, "") wantCountry(t, g, first, "")
for range 200 { for range 200 {
wantCountry(t, g, clients(), "") wantCountry(t, g, clients(), "")
} }
// The first one's next request has GeoJS asked again. // The first one's next request has GeoJS asked again.
geojs.set(answering) geojs.set(answering)
clock.advance(time.Second) clock.advance(time.Second)
wantCountry(t, g, first, "") wantCountry(t, g, first, "")
asked := waitForRequests(t, geojs, 3) asked := waitForRequests(t, geojs, 3)
if len(asked[1]) != 200 || len(asked[2]) != 1 { if len(asked[1]) != 200 || len(asked[2]) != 1 {
t.Errorf("GeoJS was asked about %d and then %d clients, want 200 and 1", t.Errorf("GeoJS was asked about %d and then %d clients, want 200 and 1",
len(asked[1]), len(asked[2])) len(asked[1]), len(asked[2]))
} }
})
} }
func TestAtMost10000ClientsWait(t *testing.T) { func TestAtMost10000ClientsWait(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
clients := newClients() geojs, clock, g := start()
first := clients() clients := newClients()
first := clients()
// 10,000 clients wait while GeoJS is left alone after a failure, and // 10,000 clients wait while GeoJS is left alone after a failure, and
// one more cannot join them. // one more cannot join them.
geojs.set(failing) geojs.set(failing)
wantCountry(t, g, first, "") wantCountry(t, g, first, "")
for range 9999 { for range 9999 {
wantCountry(t, g, clients(), "") 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())
} }
}
// With room among those waiting, it is asked about. extra := clients()
wantCountry(t, g, extra, germany) 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. // 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 // standIn is a stand-in for GeoJS. It notes the addresses each request
// asks about. // asks about.
type standIn struct { type standIn struct {
server *httptest.Server
mu sync.Mutex mu sync.Mutex
answers int answers int
requests [][]string 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. // ServeHTTP answers a request about the addresses in its ip parameter.
func (s *standIn) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *standIn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
addrs := strings.Split(r.URL.Query().Get("ip"), ",") addrs := strings.Split(r.URL.Query().Get("ip"), ",")
@@ -422,7 +461,8 @@ func (s *standIn) asked() [][]string {
return slices.Clone(s.requests) 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 { type testClock struct {
mu sync.Mutex mu sync.Mutex
now time.Time now time.Time
@@ -444,21 +484,17 @@ func (c *testClock) advance(d time.Duration) {
c.now = c.now.Add(d) c.now = c.now.Add(d)
} }
// start starts a stand-in for GeoJS that answers, and returns it, a // start returns a stand-in for GeoJS that answers, a clock, and a GeoJS
// clock, and a GeoJS asking it by that clock. // asking the stand-in by that clock.
func start(t *testing.T) (*standIn, *testClock, *lookup.GeoJS) { func start() (*standIn, *testClock, *lookup.GeoJS) {
t.Helper()
geojs := &standIn{} 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)} clock := &testClock{now: time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC)}
g := lookup.New(lookup.Params{ g := lookup.New(lookup.Params{
URL: geojs.server.URL, URL: lookup.URL,
Now: clock.Now, Now: clock.Now,
ProcessLog: slog.New(slog.DiscardHandler), ProcessLog: slog.New(slog.DiscardHandler),
}) })
g.SetTransport(geojs)
return geojs, clock, g 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 // waitForRequests waits until g has done all it can before time passes,
// the addresses each asked about. // checks that GeoJS has had count requests, and returns the addresses each
// asked about.
func waitForRequests(t *testing.T, geojs *standIn, count int) [][]string { func waitForRequests(t *testing.T, geojs *standIn, count int) [][]string {
t.Helper() t.Helper()
deadline := time.Now().Add(waitLimit) synctest.Wait()
for time.Now().Before(deadline) {
asked := geojs.asked()
if len(asked) >= count {
return asked
}
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 asked
return nil
} }
// waitForCountry waits for g to give client the country want, moving the // waitForCountry lets a request to GeoJS under way be abandoned, and moves
// clock on a minute at a time, so that GeoJS is asked again after a // the clock on a minute, so that GeoJS may be asked again after a failure.
// 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( func waitForCountry(
t *testing.T, g *lookup.GeoJS, clock *testClock, client netip.Prefix, want string, t *testing.T, g *lookup.GeoJS, clock *testClock, client netip.Prefix, want string,
) { ) {
t.Helper() t.Helper()
deadline := time.Now().Add(waitLimit) time.Sleep(timeout)
for g.Country(t.Context(), client) != want { clock.advance(time.Minute)
if time.Now().After(deadline) { wantCountry(t, g, client, "")
t.Fatalf("%s is not in %q after %s", client, want, waitLimit) synctest.Wait()
} wantCountry(t, g, client, want)
clock.advance(time.Minute)
time.Sleep(pollInterval)
}
} }
+20 -17
View File
@@ -4,6 +4,7 @@ import (
"net/netip" "net/netip"
"slices" "slices"
"testing" "testing"
"testing/synctest"
"time" "time"
"sneak.berlin/go/smallwebwaf/internal/lookup" "sneak.berlin/go/smallwebwaf/internal/lookup"
@@ -12,30 +13,32 @@ import (
func TestSnapshotHoldsEachAnswerAndWhenItWasLastUsed(t *testing.T) { func TestSnapshotHoldsEachAnswerAndWhenItWasLastUsed(t *testing.T) {
t.Parallel() t.Parallel()
_, clock, g := start(t) synctest.Test(t, func(t *testing.T) {
placed := netip.MustParsePrefix("203.0.113.9/32") _, clock, g := start()
notPlaced := netip.MustParsePrefix(unplaced + "/32") placed := netip.MustParsePrefix("203.0.113.9/32")
asked := clock.Now() notPlaced := netip.MustParsePrefix(unplaced + "/32")
asked := clock.Now()
wantCountry(t, g, placed, germany) wantCountry(t, g, placed, germany)
wantCountry(t, g, notPlaced, "") wantCountry(t, g, notPlaced, "")
clock.advance(time.Hour) clock.advance(time.Hour)
wantCountry(t, g, placed, germany) wantCountry(t, g, placed, germany)
want := []lookup.Answer{ want := []lookup.Answer{
{Client: notPlaced, Country: "", Answered: asked, Used: asked}, {Client: notPlaced, Country: "", Answered: asked, Used: asked},
{Client: placed, Country: germany, Answered: asked, Used: asked.Add(time.Hour)}, {Client: placed, Country: germany, Answered: asked, Used: asked.Add(time.Hour)},
} }
if got := g.Snapshot(); !slices.Equal(got, want) { if got := g.Snapshot(); !slices.Equal(got, want) {
t.Errorf("snapshot\n%+v\nwant\n%+v", got, want) t.Errorf("snapshot\n%+v\nwant\n%+v", got, want)
} }
})
} }
func TestLoadedAnswersAreKeptFor7DaysFromWhenGeoJSGaveThem(t *testing.T) { func TestLoadedAnswersAreKeptFor7DaysFromWhenGeoJSGaveThem(t *testing.T) {
t.Parallel() t.Parallel()
geojs, clock, g := start(t) geojs, clock, g := start()
now := clock.Now() now := clock.Now()
kept := lookup.Answer{ kept := lookup.Answer{
Client: netip.MustParsePrefix("203.0.113.9/32"), Client: netip.MustParsePrefix("203.0.113.9/32"),
@@ -65,7 +68,7 @@ func TestLoadDropsTheAnswerUsedLongestAgoFirst(t *testing.T) {
const maxAnswers = 100000 const maxAnswers = 100000
_, clock, g := start(t) _, clock, g := start()
now := clock.Now() now := clock.Now()
// lookups.json lists the answers by client. Here each was last used a // lookups.json lists the answers by client. Here each was last used a