d52c3ee76e
Expand asn address groups to concrete prefixes centrally (one iplocate key, consistent fleet-wide) and refresh them on a per-group TTL (default 24h). A background Refresher scans for due groups, unions each group's ASNs to a deduped prefix set, and writes them to a new resolved/resolved_at column (migration 0002). Fail-safe: a lookup error or empty expansion keeps the last-good set, never emptying it. The compiler folds resolved prefixes into the rendered set members; membership churn bumps the generation but never rewrites rules. The iplocate client is endpoint-configurable and response-tolerant, documented as needing endpoint/key confirmation. Unit tests cover TTL parsing, due-checks, and union/dedup/error propagation with a fake expander.
77 lines
1.9 KiB
Go
77 lines
1.9 KiB
Go
package asnexpand
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.unkin.net/unkin/tomswallapi/internal/model"
|
|
)
|
|
|
|
func TestParseTTL(t *testing.T) {
|
|
cases := map[string]time.Duration{
|
|
"": DefaultTTL,
|
|
"bad": DefaultTTL,
|
|
"0s": DefaultTTL,
|
|
"-1h": DefaultTTL,
|
|
"6h": 6 * time.Hour,
|
|
"30m": 30 * time.Minute,
|
|
"24h": 24 * time.Hour,
|
|
}
|
|
for in, want := range cases {
|
|
if got := ParseTTL(in); got != want {
|
|
t.Errorf("ParseTTL(%q) = %v, want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDue(t *testing.T) {
|
|
now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
|
|
old := now.Add(-25 * time.Hour)
|
|
recent := now.Add(-1 * time.Hour)
|
|
|
|
if !due(model.AddressGroup{Refresh: "24h"}, now) {
|
|
t.Error("never-resolved group should be due")
|
|
}
|
|
if !due(model.AddressGroup{Refresh: "24h", ResolvedAt: &old}, now) {
|
|
t.Error("group past its TTL should be due")
|
|
}
|
|
if due(model.AddressGroup{Refresh: "24h", ResolvedAt: &recent}, now) {
|
|
t.Error("group within its TTL should not be due")
|
|
}
|
|
}
|
|
|
|
type fakeExpander struct {
|
|
byASN map[string][]string
|
|
err error
|
|
}
|
|
|
|
func (f fakeExpander) Prefixes(_ context.Context, asn string) ([]string, error) {
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return f.byASN[asn], nil
|
|
}
|
|
|
|
func TestExpandUnionsAndDedups(t *testing.T) {
|
|
r := &Refresher{Expander: fakeExpander{byASN: map[string][]string{
|
|
"13335": {"1.1.1.0/24", "104.16.0.0/13"},
|
|
"209242": {"104.16.0.0/13", "203.0.113.0/24"}, // overlaps with 13335
|
|
}}}
|
|
got, err := r.expand(context.Background(), model.AddressGroup{Members: []string{"13335", "209242"}})
|
|
if err != nil {
|
|
t.Fatalf("expand: %v", err)
|
|
}
|
|
if len(got) != 3 {
|
|
t.Fatalf("want 3 deduped prefixes, got %d: %v", len(got), got)
|
|
}
|
|
}
|
|
|
|
func TestExpandPropagatesError(t *testing.T) {
|
|
r := &Refresher{Expander: fakeExpander{err: errors.New("boom")}}
|
|
if _, err := r.expand(context.Background(), model.AddressGroup{Members: []string{"13335"}}); err == nil {
|
|
t.Fatal("expected error to propagate so last-good is kept")
|
|
}
|
|
}
|