Make the home page's AS name search work (closes #151)
check / check (push) Canceled after 0s
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> and lists the matches
as links to their AS pages. asinfo.Search ignores case, returns the first
matches up to a limit in AS number order, and counts all matches. An
empty q answers 400.
When the database has no record of an AS, its page and /api/v1/as/{asn}
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. AS 0 means no known origin: it always answers 404, and the search
never lists it.
Model: opus-5-5
This commit is contained in:
+103
-15
@@ -1,6 +1,8 @@
|
||||
package asinfo_test
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -140,11 +142,6 @@ func TestSearch(t *testing.T) {
|
||||
query: "University",
|
||||
wantMin: 10,
|
||||
},
|
||||
{
|
||||
name: "Empty search",
|
||||
query: "",
|
||||
wantMin: 0,
|
||||
},
|
||||
{
|
||||
name: "Non-existent org",
|
||||
query: "XYZABC123456",
|
||||
@@ -156,26 +153,115 @@ func TestSearch(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
results := asinfo.Search(tt.query)
|
||||
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
|
||||
if tt.query != "" {
|
||||
for _, r := range results {
|
||||
if !strings.Contains(r.Handle, tt.query) &&
|
||||
!strings.Contains(r.Description, tt.query) {
|
||||
t.Errorf("Search result ASN %d doesn't contain query %q",
|
||||
r.ASN, tt.query)
|
||||
}
|
||||
// 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()
|
||||
|
||||
@@ -215,11 +301,13 @@ func BenchmarkGet(b *testing.B) {
|
||||
}
|
||||
|
||||
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)])
|
||||
asinfo.Search(queries[i%len(queries)], limit)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user