package ratelimit import ( "errors" "net" "strings" "testing" "time" ) // cidrs parses CIDR strings into networks for a test fixture. func cidrs(t *testing.T, ss ...string) []*net.IPNet { t.Helper() out := make([]*net.IPNet, 0, len(ss)) for _, s := range ss { _, n, err := net.ParseCIDR(s) if err != nil { t.Fatalf("bad cidr %q: %v", s, err) } out = append(out, n) } return out } func TestBlocklistBlocked(t *testing.T) { bl := NewBlocklist(true, cidrs(t, "10.20.30.0/24")) // allowlist bl.SetCIDRs(cidrs(t, "1.2.3.0/24", "203.0.113.5/32", "198.51.100.0/28", "10.20.30.0/24"), time.Now()) cases := map[string]bool{ "1.2.3.0": true, // range start "1.2.3.255": true, // range end "1.2.4.0": false, // just past the /24 "203.0.113.5": true, // /32 exact "203.0.113.6": false, "198.51.100.15": true, // within /28 "198.51.100.16": false, // past the /28 "9.9.9.9": false, // not listed "10.20.30.99": false, // listed BUT allowlisted — never blocked "::1": false, // IPv6 — never matched here "not-an-ip": false, } for ip, want := range cases { if got := bl.Blocked(ip); got != want { t.Errorf("Blocked(%q) = %v, want %v", ip, got, want) } } } func TestBlocklistDisabledOrEmpty(t *testing.T) { off := NewBlocklist(false, nil) off.SetCIDRs(cidrs(t, "1.2.3.0/24"), time.Now()) if off.Blocked("1.2.3.4") { t.Error("a disabled blocklist must not block") } empty := NewBlocklist(true, nil) if empty.Blocked("1.2.3.4") { t.Error("an empty (never-loaded) blocklist must not block") } } func TestParseDROP(t *testing.T) { in := "; Spamhaus DROP\n" + "1.2.3.0/24 ; SBL1\n" + " 198.51.100.0/28 ; SBL2\n" + "\n" + "203.0.113.5 ; a bare IP -> /32\n" + "2001:db8::/32 ; IPv6 skipped\n" + "garbage line\n" nets, err := ParseDROP(strings.NewReader(in)) if err != nil { t.Fatal(err) } if len(nets) != 3 { t.Fatalf("got %d networks, want 3: %v", len(nets), nets) } bl := NewBlocklist(true, nil) bl.SetCIDRs(nets, time.Now()) for ip, want := range map[string]bool{"1.2.3.9": true, "198.51.100.1": true, "203.0.113.5": true, "203.0.113.6": false, "2001:db8::1": false} { if got := bl.Blocked(ip); got != want { t.Errorf("Blocked(%s) = %v, want %v", ip, got, want) } } } func TestApplyRefresh(t *testing.T) { bl := NewBlocklist(true, nil) now := time.Now() if o := ApplyRefresh(bl, cidrs(t, "1.2.3.0/24"), nil, now, time.Hour); o != RefreshUpdated { t.Fatalf("success: want RefreshUpdated, got %v", o) } if !bl.Blocked("1.2.3.4") { t.Fatal("a successful refresh must apply the feed") } if o := ApplyRefresh(bl, nil, errors.New("net"), now.Add(30*time.Minute), time.Hour); o != RefreshKept { t.Fatalf("fresh failure: want RefreshKept, got %v", o) } if !bl.Blocked("1.2.3.4") { t.Error("a kept feed must still block") } if o := ApplyRefresh(bl, nil, errors.New("net"), now.Add(2*time.Hour), time.Hour); o != RefreshDropped { t.Fatalf("stale failure: want RefreshDropped, got %v", o) } if bl.Blocked("1.2.3.4") { t.Error("a stale feed must be dropped (fail-open)") } }