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>, 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.
A match can be an AS that the database does not hold yet. Its AS page
and /api/v1/as/{asn} now answer with its handle and description from
asinfo and no prefixes, instead of 404. Empty prefix lists in that JSON
answer are [] rather than null.
Model: opus-5-5
This commit is contained in:
@@ -1748,6 +1748,10 @@ func (d *Database) GetASDetails(asn int) (*ASN, []LiveRoute, error) {
|
||||
// GetASDetailsContext returns the AS and the prefixes it originates, the IPv4
|
||||
// ones first, each once with the time of its newest route. It makes two index
|
||||
// searches per prefix, however many peers carry the prefix.
|
||||
//
|
||||
// An AS that only the AS info lookup knows comes with its handle and
|
||||
// description, and without prefixes or seen times. An AS that neither the
|
||||
// database nor the lookup knows is ErrNoRoute.
|
||||
func (d *Database) GetASDetailsContext(
|
||||
ctx context.Context, asn int,
|
||||
) (*ASN, []LiveRoute, error) {
|
||||
@@ -1765,7 +1769,14 @@ func (d *Database) GetASDetailsContext(
|
||||
&asnInfo.LastSeen)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
|
||||
info, ok := asinfo.Get(asn)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("%w: AS%d", ErrNoRoute, asn)
|
||||
}
|
||||
|
||||
return &ASN{
|
||||
ASN: asn, Handle: info.Handle, Description: info.Description,
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("failed to query AS: %w", err)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/config"
|
||||
"git.eeqj.de/sneak/routewatch/internal/logger"
|
||||
"git.eeqj.de/sneak/routewatch/pkg/asinfo"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
@@ -1699,6 +1700,46 @@ func TestGetASDetailsListsEachPrefixOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetASDetailsOfASNotInDatabase asks an empty database for an AS that the
|
||||
// AS info lookup knows, and gets its handle and description with no prefixes
|
||||
// and no seen times. An AS that the lookup does not know is ErrNoRoute.
|
||||
func TestGetASDetailsOfASNotInDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create database: %v", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
const knownASN, unknownASN = 15169, 4200000000
|
||||
|
||||
info, ok := asinfo.Get(knownASN)
|
||||
if !ok {
|
||||
t.Fatalf("AS%d is not in the AS info data", knownASN)
|
||||
}
|
||||
|
||||
got, prefixes, err := db.GetASDetails(knownASN)
|
||||
if err != nil {
|
||||
t.Fatalf("GetASDetails(%d): %v", knownASN, err)
|
||||
}
|
||||
|
||||
want := ASN{ASN: knownASN, Handle: info.Handle, Description: info.Description}
|
||||
if *got != want || len(prefixes) != 0 {
|
||||
t.Errorf("GetASDetails(%d) = %+v with prefixes %+v, want %+v and none",
|
||||
knownASN, *got, prefixes, want)
|
||||
}
|
||||
|
||||
if _, ok := asinfo.Get(unknownASN); ok {
|
||||
t.Fatalf("AS%d is in the AS info data", unknownASN)
|
||||
}
|
||||
|
||||
_, _, err = db.GetASDetails(unknownASN)
|
||||
if !errors.Is(err, ErrNoRoute) {
|
||||
t.Errorf("GetASDetails(%d): error %v, want %v", unknownASN, err, ErrNoRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestASPrefixQueriesUseCoveringIndexes checks the plan of each query that
|
||||
// lists the prefixes an AS originates: a search of a covering index and no
|
||||
// temporary B-tree, so the list reads one index entry per prefix, not one
|
||||
|
||||
@@ -28,9 +28,10 @@ type ASN struct {
|
||||
RIRRegDate *time.Time `json:"rir_registration_date,omitempty"`
|
||||
RIRLastMod *time.Time `json:"rir_last_modified,omitempty"`
|
||||
WHOISRaw string `json:"whois_raw,omitempty"`
|
||||
// Timestamps
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
// Timestamps. They are zero, and left out of JSON, for an AS that the
|
||||
// database does not hold.
|
||||
FirstSeen time.Time `json:"first_seen,omitzero"`
|
||||
LastSeen time.Time `json:"last_seen,omitzero"`
|
||||
WHOISUpdatedAt *time.Time `json:"whois_updated_at,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user