Files
benvin d52c3ee76e
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Add central ASN address-group expander
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.
2026-07-19 21:44:30 +10:00

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