Give every IP lookup result the same fields (closes #200)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Every result of /ip/, /ip/{addr} and /api/v1/ip/{ip} now has every
field. A field without a value is null instead of left out, so an
address with no live route has all its route and AS fields set to null.
ptr is an empty list when there are no PTR names.
Two new fields, num_origins and origin_asns, give how many origin ASes
peers carry the netblock with, and which. The routing table's prefix
summary now lists them. asn still names the origin that most peers
carry.
first_seen is null, no longer the zero time, when the database has not
stored the netblock yet.
Model: opus-5-5
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"html"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -1219,7 +1220,8 @@ func TestIPLookupKeepsRouteWhenPTRLookupDoesNotAnswer(t *testing.T) {
|
||||
t.Fatalf("decoding the answer %q: %v", rec.Body, err)
|
||||
}
|
||||
|
||||
if len(body.Data.Results) != 1 || body.Data.Results[0].Netblock != testPrefixV4 {
|
||||
if len(body.Data.Results) != 1 || body.Data.Results[0].Netblock == nil ||
|
||||
*body.Data.Results[0].Netblock != testPrefixV4 {
|
||||
t.Errorf("answer %s, want one result in %s", rec.Body, testPrefixV4)
|
||||
}
|
||||
}
|
||||
@@ -1311,12 +1313,36 @@ func TestIPLookupReportsBlocklists(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// jsonNull is how JSON writes a field without a value.
|
||||
const jsonNull = "null"
|
||||
|
||||
// resultFields decodes an IP lookup answer and returns each of its results as
|
||||
// its JSON fields.
|
||||
func resultFields(t *testing.T, answer []byte) []map[string]json.RawMessage {
|
||||
t.Helper()
|
||||
|
||||
var body struct {
|
||||
Data struct {
|
||||
Results []map[string]json.RawMessage `json:"results"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(answer, &body)
|
||||
if err != nil {
|
||||
t.Fatalf("decoding the answer %s: %v", answer, err)
|
||||
}
|
||||
|
||||
return body.Data.Results
|
||||
}
|
||||
|
||||
// TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute looks up a listed
|
||||
// address that has no live route, alone and beside a routed address. Each
|
||||
// answer is 200 and has a result for the address, with its blocklists and no
|
||||
// route fields. Before the fix the lookup of that address alone answered
|
||||
// 404, and beside a routed address it was only named in errors, so its
|
||||
// blocklists were never reported.
|
||||
// answer is 200 and has a result for the address, with its blocklists and
|
||||
// every route field set to null. Before the fix the lookup of that address
|
||||
// alone answered 404, and beside a routed address it was only named in
|
||||
// errors, so its blocklists were never reported. After that fix the result
|
||||
// still left out the route fields
|
||||
// (https://git.eeqj.de/sneak/routewatch/issues/200).
|
||||
func TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1324,6 +1350,15 @@ func TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute(t *testing.T) {
|
||||
s.resolver = &fakeResolver{}
|
||||
s.SetBlocklists(fakeBlocklists{})
|
||||
|
||||
// routeFields are the fields of a result that come from the address's
|
||||
// route and that route's origin AS.
|
||||
routeFields := []string{
|
||||
"netblock", "mask_length", "ip_version", "num_peers", "asn",
|
||||
"num_origins", "origin_asns", "as_name", "handle", "description",
|
||||
"org_name", "org_id", "address", "country_code", "abuse_email", "rir",
|
||||
"first_seen", "last_seen",
|
||||
}
|
||||
|
||||
for _, target := range []string{
|
||||
listedUnroutedAddr,
|
||||
addrInTestPrefixV4 + "," + listedUnroutedAddr,
|
||||
@@ -1354,12 +1389,21 @@ func TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
result := body.Data.Results[i]
|
||||
if result.IPInfo != nil {
|
||||
t.Errorf("%s: result for %s has route fields %+v",
|
||||
target, listedUnroutedAddr, result.IPInfo)
|
||||
fields := resultFields(t, rec.Body.Bytes())[i]
|
||||
for _, name := range routeFields {
|
||||
value, present := fields[name]
|
||||
|
||||
switch {
|
||||
case !present:
|
||||
t.Errorf("%s: the result for %s has no %s",
|
||||
target, listedUnroutedAddr, name)
|
||||
case string(value) != jsonNull:
|
||||
t.Errorf("%s: %s of %s is %s, want %s",
|
||||
target, name, listedUnroutedAddr, value, jsonNull)
|
||||
}
|
||||
}
|
||||
|
||||
result := body.Data.Results[i]
|
||||
if len(result.Blocklists) != 1 ||
|
||||
result.Blocklists[0].Name != testListing().Name {
|
||||
t.Errorf("%s: blocklists %+v, want [%+v]",
|
||||
@@ -1368,6 +1412,45 @@ func TestIPLookupReportsBlocklistsOfAnAddressWithNoRoute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// twoAddressHostName is a host name that resolves to addrInTestPrefixV4 and
|
||||
// listedUnroutedAddr.
|
||||
const twoAddressHostName = "two-addresses.test"
|
||||
|
||||
// TestIPLookupResultsHaveTheSameFields looks up a host name, addresses and
|
||||
// the caller's own address, through each IP lookup route. Some of the
|
||||
// addresses have a route and some do not, and none has PTR names. Every
|
||||
// result has the same fields. Before the fix a result left out every field
|
||||
// without a value (https://git.eeqj.de/sneak/routewatch/issues/200).
|
||||
func TestIPLookupResultsHaveTheSameFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{hosts: map[string][]string{
|
||||
twoAddressHostName: {addrInTestPrefixV4, listedUnroutedAddr},
|
||||
}}
|
||||
s.SetBlocklists(fakeBlocklists{})
|
||||
|
||||
// want is the fields of the first result.
|
||||
var want []string
|
||||
|
||||
for _, path := range []string{
|
||||
"/api/v1/ip/" + twoAddressHostName,
|
||||
"/ip/" + addrInTestPrefixV6 + "," + listedUnroutedAddr,
|
||||
ownAddressPath,
|
||||
} {
|
||||
for _, result := range resultFields(t, get(t, s, path).Body.Bytes()) {
|
||||
got := slices.Sorted(maps.Keys(result))
|
||||
if want == nil {
|
||||
want = got
|
||||
}
|
||||
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("%s: a result has the fields %v, want %v", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIPLookupAnswersMayBeCached checks the Cache-Control header of the IP
|
||||
// lookup answers. Caches may keep them for the blocklists' shortest interval,
|
||||
// and only the caller's own browser may keep the answer about the caller's
|
||||
|
||||
Reference in New Issue
Block a user