package iprange import ( "math/rand" "net/netip" "strings" "testing" ) func TestParse(t *testing.T) { for _, tc := range []struct { in, want, err string ok bool }{ {in: "10.0.10.101-10.0.10.199", want: "10.0.10.101-10.0.10.199", ok: true}, {in: "10.0.10.101 - 10.0.10.199", want: "10.0.10.101-10.0.10.199", ok: true}, {in: "2001:DB8::100-2001:db8::1ff", want: "2001:db8::100-2001:db8::1ff", ok: true}, {in: "10.0.0.5-10.0.0.5", want: "10.0.0.5-10.0.0.5", ok: true}, {in: "branch-office", ok: false}, {in: "10-net", ok: false}, {in: "10.0.0.0/24", ok: false}, {in: "10.0.10.101-199", ok: true, err: "not a full IP address"}, {in: "10.0.10.101-", ok: true, err: "no end address"}, {in: "2001:db8::100-1ff", ok: true, err: "not a full IP address"}, {in: "10.0.0.9-10.0.0.1", ok: true, err: "start is after the end"}, {in: "10.0.0.1-2001:db8::1", ok: true, err: "mixes IPv4 and IPv6"}, {in: "fe80::1%eth0-fe80::9%eth0", ok: true, err: "zone"}, {in: "10.0.300.1-10.0.300.9", ok: true, err: "not a full IP address"}, {in: "10.0.0.1-10.0.0.9-10.0.0.20", ok: true, err: "not a full IP address"}, } { r, ok, err := Parse(tc.in) if ok != tc.ok || (err == nil) != (tc.err == "") || err != nil && !strings.Contains(err.Error(), tc.err) { t.Errorf("%s: ok=%v err=%v, want ok=%v err~%q", tc.in, ok, err, tc.ok, tc.err) continue } if err == nil && ok && r.String() != tc.want { t.Errorf("%s = %s, want %s", tc.in, r, tc.want) } } } func TestPrefixes(t *testing.T) { for in, want := range map[string]string{ "10.0.10.101-10.0.10.199": "10.0.10.101/32 10.0.10.102/31 10.0.10.104/29 10.0.10.112/28 10.0.10.128/26 10.0.10.192/29", "10.0.0.0-10.0.0.255": "10.0.0.0/24", "10.0.0.5-10.0.0.5": "10.0.0.5/32", "0.0.0.0-255.255.255.255": "0.0.0.0/0", "255.255.255.254-255.255.255.255": "255.255.255.254/31", "2001:db8::100-2001:db8::1ff": "2001:db8::100/120", "::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff": "::/0", } { r, _, err := Parse(in) if err != nil { t.Fatal(err) } var got []string for _, p := range r.Prefixes() { got = append(got, p.String()) } if strings.Join(got, " ") != want { t.Errorf("%s = %s, want %s", in, strings.Join(got, " "), want) } } // The worst cases stay small. if n := len(must(t, "0.0.0.1-255.255.255.254").Prefixes()); n != 62 { t.Errorf("worst IPv4 case = %d prefixes, want 62", n) } if n := len(must(t, "::1-ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe").Prefixes()); n != 254 { t.Errorf("worst IPv6 case = %d prefixes, want 254", n) } } // Every address of a random range is covered once, and nothing outside it. func TestPrefixesExact(t *testing.T) { rnd := rand.New(rand.NewSource(1)) for i := 0; i < 300; i++ { lo, hi := rnd.Intn(4096), rnd.Intn(4096) if hi < lo { lo, hi = hi, lo } base := []byte{10, 20, 0, 0} addr := func(n int) netip.Addr { return netip.AddrFrom4([4]byte{base[0], base[1], byte(n >> 8), byte(n)}) } r := Range{Start: addr(lo), End: addr(hi)} ps := r.Prefixes() for n := max(lo-3, 0); n <= min(hi+3, 4095); n++ { c := 0 for _, p := range ps { if p.Contains(addr(n)) { c++ } } if in := n >= lo && n <= hi; in && c != 1 || !in && c != 0 { t.Fatalf("%s: %s covered %d times (%v)", r, addr(n), c, ps) } } if got := r.Size().Int64(); got != int64(hi-lo+1) { t.Fatalf("%s: size %d", r, got) } } } func TestSizeAndMapped(t *testing.T) { if s := must(t, "10.0.10.101-10.0.10.199").SizeText(); s != "99 addresses" { t.Errorf("size = %s", s) } if s := must(t, "2001:db8::-2001:db8:0:ffff:ffff:ffff:ffff:ffff").SizeText(); s != "about 2^80 addresses" { t.Errorf("size = %s", s) } if !must(t, "::ffff:10.0.0.1-::ffff:10.0.0.9").TouchesMapped4() || must(t, "2001:db8::1-2001:db8::9").TouchesMapped4() || must(t, "10.0.0.1-10.0.0.9").TouchesMapped4() { t.Error("TouchesMapped4") } } func must(t *testing.T, s string) Range { t.Helper() r, ok, err := Parse(s) if !ok || err != nil { t.Fatalf("%s: %v %v", s, ok, err) } return r }