check / check (push) Canceled after 0s
The AS Name Search card on the home page said the search was coming
soon. It now asks the new GET /api/v1/search/as?q=<text>, which
searches the AS list built into asinfo, and lists the matches as links
to their AS pages. An empty q answers 400. asinfo.Search now ignores
case and returns its matches in AS number order. asinfo lowers each
handle and description once, when the list loads.
A match can be an AS that the database has no record of. Its AS page
and /api/v1/as/{asn} then take its handle and description from asinfo
and still list its prefixes. They answer 404 only for an AS with no
record, no asinfo entry and no prefix. Empty prefix lists in that JSON
answer are [] rather than null.
Model: opus-5-5
267 lines
5.2 KiB
Go
267 lines
5.2 KiB
Go
package asinfo_test
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/routewatch/pkg/asinfo"
|
|
)
|
|
|
|
func TestGet(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
asn int
|
|
wantOK bool
|
|
wantDesc string
|
|
}{
|
|
{
|
|
name: "AS1 Level 3",
|
|
asn: 1,
|
|
wantOK: true,
|
|
wantDesc: "Level 3 Parent, LLC",
|
|
},
|
|
{
|
|
name: "AS15169 Google",
|
|
asn: 15169,
|
|
wantOK: true,
|
|
wantDesc: "Google LLC",
|
|
},
|
|
{
|
|
name: "Non-existent AS",
|
|
asn: 999999999,
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "AS0 IANA Reserved",
|
|
asn: 0,
|
|
wantOK: true,
|
|
wantDesc: "Internet Assigned Numbers Authority",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
info, ok := asinfo.Get(tt.asn)
|
|
if ok != tt.wantOK {
|
|
t.Errorf("Get(%d) ok = %v, want %v", tt.asn, ok, tt.wantOK)
|
|
|
|
return
|
|
}
|
|
|
|
if ok && info.Description != tt.wantDesc {
|
|
t.Errorf("Get(%d) description = %q, want %q",
|
|
tt.asn, info.Description, tt.wantDesc)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetDescription(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
asn int
|
|
want string
|
|
}{
|
|
{1, "Level 3 Parent, LLC"},
|
|
{999999999, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := asinfo.GetDescription(tt.asn)
|
|
if got != tt.want {
|
|
t.Errorf("GetDescription(%d) = %q, want %q", tt.asn, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetHandle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
asn int
|
|
want string
|
|
}{
|
|
{1, "LVLT"},
|
|
{0, "IANA-RSVD"},
|
|
{999999999, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run("", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := asinfo.GetHandle(tt.asn)
|
|
if got != tt.want {
|
|
t.Errorf("GetHandle(%d) = %q, want %q", tt.asn, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTotal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
total := asinfo.Total()
|
|
if total < 100000 {
|
|
t.Errorf("Total() = %d, expected > 100000", total)
|
|
}
|
|
|
|
// Verify it's consistent
|
|
total2 := asinfo.Total()
|
|
if total2 != total {
|
|
t.Errorf("Total() returned different values: %d vs %d", total, total2)
|
|
}
|
|
}
|
|
|
|
func TestSearch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
query string
|
|
wantMin int
|
|
}{
|
|
{
|
|
name: "Search for Google",
|
|
query: "Google",
|
|
wantMin: 1,
|
|
},
|
|
{
|
|
name: "Search for University",
|
|
query: "University",
|
|
wantMin: 10,
|
|
},
|
|
{
|
|
name: "Non-existent org",
|
|
query: "XYZABC123456",
|
|
wantMin: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
results := asinfo.Search(tt.query)
|
|
if len(results) < tt.wantMin {
|
|
t.Errorf("Search(%q) returned %d results, want at least %d",
|
|
tt.query, len(results), tt.wantMin)
|
|
}
|
|
|
|
// Verify all results contain the query, ignoring case
|
|
query := strings.ToLower(tt.query)
|
|
for _, r := range results {
|
|
if !strings.Contains(strings.ToLower(r.Handle), query) &&
|
|
!strings.Contains(strings.ToLower(r.Description), query) {
|
|
t.Errorf("Search result ASN %d doesn't contain query %q",
|
|
r.ASN, tt.query)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSearchEmptyQueryMatchesNothing expects an empty query to match no AS,
|
|
// although every handle contains the empty string.
|
|
func TestSearchEmptyQueryMatchesNothing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
results := asinfo.Search("")
|
|
if len(results) != 0 {
|
|
t.Errorf(`Search("") returned %d results, want none`, len(results))
|
|
}
|
|
}
|
|
|
|
// TestSearchIgnoresCase searches for Cloudflare's handle in lower case and its
|
|
// description in upper case, and expects both searches to find AS13335.
|
|
func TestSearchIgnoresCase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const cloudflareASN = 13335 // handle CLOUDFLARENET, "Cloudflare, Inc."
|
|
|
|
isCloudflare := func(info asinfo.Info) bool {
|
|
return info.ASN == cloudflareASN
|
|
}
|
|
|
|
for _, query := range []string{"cloudflarenet", "CLOUDFLARE, INC."} {
|
|
if !slices.ContainsFunc(asinfo.Search(query), isCloudflare) {
|
|
t.Errorf("Search(%q) did not find AS%d", query, cloudflareASN)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSearchOrdersByASN expects the matches of a search with many of them to
|
|
// come in AS number order.
|
|
func TestSearchOrdersByASN(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const query = "University"
|
|
|
|
results := asinfo.Search(query)
|
|
|
|
for i := 1; i < len(results); i++ {
|
|
if results[i-1].ASN >= results[i].ASN {
|
|
t.Fatalf("Search(%q): AS%d comes after AS%d",
|
|
query, results[i].ASN, results[i-1].ASN)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDataIntegrity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Verify no duplicate ASNs
|
|
all := asinfo.All()
|
|
seen := make(map[int]bool)
|
|
|
|
for _, info := range all {
|
|
if seen[info.ASN] {
|
|
t.Errorf("Duplicate ASN found: %d", info.ASN)
|
|
}
|
|
|
|
seen[info.ASN] = true
|
|
}
|
|
|
|
// Verify all entries have required fields
|
|
for _, info := range all {
|
|
if info.Handle == "" && info.ASN != 0 {
|
|
t.Errorf("ASN %d has empty handle", info.ASN)
|
|
}
|
|
|
|
if info.Description == "" {
|
|
t.Errorf("ASN %d has empty description", info.ASN)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkGet(b *testing.B) {
|
|
// Common ASNs to lookup
|
|
asns := []int{1, 15169, 13335, 32934, 8075, 16509}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := range b.N {
|
|
asinfo.Get(asns[i%len(asns)])
|
|
}
|
|
}
|
|
|
|
func BenchmarkSearch(b *testing.B) {
|
|
queries := []string{"Google", "Amazon", "Microsoft", "University"}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := range b.N {
|
|
asinfo.Search(queries[i%len(queries)])
|
|
}
|
|
}
|