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: "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, 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) } }