check / check (push) Canceled after 0s
The built-in AS list had no letter outside ASCII, and make asupdate rejected today's as.csv, which added a country code column. The generator now checks only the first three header columns and reads past the rest; the parsing moved into parseCSV. The list is regenerated. The new source spells some names differently and no longer lists AS 0 or the documentation AS numbers, so the tests that relied on those entries changed. A WHOIS answer that is not valid UTF-8 is now decoded as Latin-1, which RIPE and APNIC send, before it is parsed and stored. LACNIC answers in UTF-8. Model: opus-5-5
318 lines
6.6 KiB
Go
318 lines
6.6 KiB
Go
package asinfo_test
|
|
|
|
import (
|
|
"cmp"
|
|
"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: "AS1251 with accented letters",
|
|
asn: 1251,
|
|
wantOK: true,
|
|
wantDesc: "FUNDAÇÃO DE AMPARO À PESQUISA DO ESTADO SÃO PAULO",
|
|
},
|
|
}
|
|
|
|
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-1"},
|
|
{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: "Search for São Paulo",
|
|
query: "São Paulo",
|
|
wantMin: 1,
|
|
},
|
|
{
|
|
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, asinfo.Total())
|
|
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, total := asinfo.Search("", asinfo.Total())
|
|
if len(results) != 0 || total != 0 {
|
|
t.Errorf(`Search("") returned %d results and a total of %d, want none`,
|
|
len(results), total)
|
|
}
|
|
}
|
|
|
|
// 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."} {
|
|
results, _ := asinfo.Search(query, asinfo.Total())
|
|
if !slices.ContainsFunc(results, 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, asinfo.Total())
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSearchStopsAtLimit searches for a word that matches more entries than
|
|
// the limit. It expects exactly limit entries, the first matches in AS number
|
|
// order, and the number of all matches, counted here from All.
|
|
func TestSearchStopsAtLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
query = "university"
|
|
limit = 10
|
|
)
|
|
|
|
var matches []asinfo.Info
|
|
|
|
for _, info := range asinfo.All() {
|
|
if strings.Contains(strings.ToLower(info.Handle), query) ||
|
|
strings.Contains(strings.ToLower(info.Description), query) {
|
|
matches = append(matches, info)
|
|
}
|
|
}
|
|
|
|
if len(matches) <= limit {
|
|
t.Fatalf("%q matches %d entries; the test needs more than %d",
|
|
query, len(matches), limit)
|
|
}
|
|
|
|
slices.SortFunc(matches, func(a, b asinfo.Info) int {
|
|
return cmp.Compare(a.ASN, b.ASN)
|
|
})
|
|
|
|
results, total := asinfo.Search(query, limit)
|
|
|
|
if total != len(matches) {
|
|
t.Errorf("Search(%q, %d): total %d, want %d",
|
|
query, limit, total, len(matches))
|
|
}
|
|
|
|
if !slices.Equal(results, matches[:limit]) {
|
|
t.Errorf("Search(%q, %d) = %v, want %v",
|
|
query, limit, results, matches[:limit])
|
|
}
|
|
}
|
|
|
|
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) {
|
|
const limit = 50
|
|
|
|
queries := []string{"Google", "Amazon", "Microsoft", "University"}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := range b.N {
|
|
asinfo.Search(queries[i%len(queries)], limit)
|
|
}
|
|
}
|