Files
routewatch/pkg/asinfo/asinfo_test.go
T
sneak 72034907a7
check / check (push) Canceled after 0s
Make the home page's AS name search work (closes #151)
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
2026-10-26 20:58:05 +00:00

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)])
}
}