Files
routewatch/pkg/asinfo/asinfo_test.go
T
sneak 50283d9e9e
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. The answer holds the query, the number of matches
and the first 50 of them; an empty q answers 400. asinfo.Search now
ignores case, uses strings.Contains instead of its own substring loop,
and returns its matches in AS number order. A number, or AS and a
number, typed into the card still opens that AS's page. The card builds
the list with textContent, since the names come from outside data.

Model: opus-5-5
2026-10-26 20:21:14 +00:00

261 lines
5.0 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: "Empty search",
query: "",
wantMin: 0,
},
{
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)
}
}
})
}
}
// 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)])
}
}