218 lines
7 KiB
Go
218 lines
7 KiB
Go
package main
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// loadSeed prefers inventory.yaml; falls back to radios.yaml during the
|
|
// migration window. Tests use whichever the repo currently ships.
|
|
func loadSeed(t *testing.T) *seed {
|
|
t.Helper()
|
|
s, err := loadSeedInventory("inventory.yaml")
|
|
if err != nil {
|
|
t.Fatalf("loadSeedInventory: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestLoadSeedInventory(t *testing.T) {
|
|
s := loadSeed(t)
|
|
wantSites := []string{"verona", "altoga", "climax", "culleoka", "newhope", "lowry", "982", "494", "core"}
|
|
if len(s.Order) != len(wantSites) {
|
|
t.Fatalf("site order = %v, want %v", s.Order, wantSites)
|
|
}
|
|
for i, name := range wantSites {
|
|
if s.Order[i] != name {
|
|
t.Fatalf("site[%d] = %q, want %q", i, s.Order[i], name)
|
|
}
|
|
}
|
|
|
|
verona := s.Sites["verona"]
|
|
if verona.Router != "10.254.254.101" {
|
|
t.Errorf("verona router = %q", verona.Router)
|
|
}
|
|
if verona.MgmtSubnet.String() != "10.10.0.0/20" {
|
|
t.Errorf("verona mgmt = %q", verona.MgmtSubnet)
|
|
}
|
|
|
|
altoga := s.Sites["altoga"]
|
|
if altoga.ParentRouter != "10.254.254.101" {
|
|
t.Errorf("altoga parent_router = %q", altoga.ParentRouter)
|
|
}
|
|
if altoga.Router != "" {
|
|
t.Errorf("altoga router = %q, want empty", altoga.Router)
|
|
}
|
|
|
|
// Backhaul link labels indexed by /29.
|
|
climax := s.Sites["climax"]
|
|
pfx := netip.MustParsePrefix("10.250.1.24/29").Masked()
|
|
if got := climax.LinkLabels[pfx]; got != "verona<->climax AF11" {
|
|
t.Errorf("climax link for %s = %q", pfx, got)
|
|
}
|
|
}
|
|
|
|
func TestTopSlash24(t *testing.T) {
|
|
cases := map[string]string{
|
|
"10.10.0.0/20": "10.10.15.0/24",
|
|
"10.10.16.0/20": "10.10.31.0/24",
|
|
"10.10.80.0/20": "10.10.95.0/24",
|
|
"10.10.96.0/20": "10.10.111.0/24",
|
|
"10.10.128.0/20": "10.10.143.0/24",
|
|
"10.10.144.0/20": "10.10.159.0/24",
|
|
"10.10.160.0/20": "10.10.175.0/24",
|
|
}
|
|
for in, want := range cases {
|
|
got := topSlash24(netip.MustParsePrefix(in))
|
|
if got.String() != want {
|
|
t.Errorf("topSlash24(%s) = %s, want %s", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClosestRouterIP(t *testing.T) {
|
|
owners := map[netip.Addr]*routerDiscovery{
|
|
netip.MustParseAddr("10.250.1.25"): nil,
|
|
netip.MustParseAddr("10.250.1.30"): nil,
|
|
}
|
|
got := closestRouterIP(netip.MustParseAddr("10.250.1.26"), owners)
|
|
if got.String() != "10.250.1.25" {
|
|
t.Errorf("closest to .26 = %s, want .25", got)
|
|
}
|
|
got = closestRouterIP(netip.MustParseAddr("10.250.1.29"), owners)
|
|
if got.String() != "10.250.1.30" {
|
|
t.Errorf("closest to .29 = %s, want .30", got)
|
|
}
|
|
}
|
|
|
|
func TestHasCap(t *testing.T) {
|
|
cases := []struct {
|
|
caps, want string
|
|
expect bool
|
|
}{
|
|
{"bridge", "bridge", true},
|
|
{"bridge,router", "bridge", true},
|
|
{"bridge,wlan-ap,router,station-only", "wlan-ap", true},
|
|
{"bridge,router", "wlan-ap", false},
|
|
{"", "bridge", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := hasCap(c.caps, c.want); got != c.expect {
|
|
t.Errorf("hasCap(%q, %q) = %v, want %v", c.caps, c.want, got, c.expect)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCustomerPoolName(t *testing.T) {
|
|
want := []string{"verona-cpe", "altoga-cgnat", "cgnat", "cgnat-full", "DHCP_pool3", "customer-stuff"}
|
|
dont := []string{"verona-tower-pool", "verona-v6-pd-pool", "altoga-old"}
|
|
for _, n := range want {
|
|
if !customerPoolName(n) {
|
|
t.Errorf("customerPoolName(%q) = false, want true", n)
|
|
}
|
|
}
|
|
for _, n := range dont {
|
|
if customerPoolName(n) {
|
|
t.Errorf("customerPoolName(%q) = true, want false", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPoolRangeContains(t *testing.T) {
|
|
p := poolRange{
|
|
Lo: netip.MustParseAddr("10.10.0.1"),
|
|
Hi: netip.MustParseAddr("10.10.14.254"),
|
|
}
|
|
if !p.contains(netip.MustParseAddr("10.10.5.5")) {
|
|
t.Errorf("expected 10.10.5.5 in range")
|
|
}
|
|
if p.contains(netip.MustParseAddr("10.10.15.1")) {
|
|
t.Errorf("10.10.15.1 should be outside")
|
|
}
|
|
}
|
|
|
|
func TestCollectSwitches(t *testing.T) {
|
|
d := &routerDiscovery{
|
|
Router: Router{Name: "culleoka", Host: "10.254.254.104"},
|
|
OwnIP: netip.MustParseAddr("10.254.254.104"),
|
|
Addresses: []addrEntry{
|
|
// directly-connected subnets we accept switches on
|
|
{Prefix: netip.MustParsePrefix("10.10.96.0/20")},
|
|
{Prefix: netip.MustParsePrefix("100.64.24.0/22")},
|
|
},
|
|
Neighbors: []neighborEntry{
|
|
// Real Netonix switch on a directly-connected /22 — include.
|
|
{Addr: netip.MustParseAddr("100.64.27.250"), MAC: "EC:13:B2:A4:05:CA",
|
|
Identity: "Culleoka Switch", Platform: "Netonix WS-26-400-AC", Caps: "bridge"},
|
|
// AP radio — wlan-ap capability, exclude.
|
|
{Addr: netip.MustParseAddr("10.10.111.11"), MAC: "78:45:58:A2:EC:F9",
|
|
Identity: "Culleoka SE", Platform: "EPMP3000", Caps: "bridge,wlan-ap,router,station-only"},
|
|
// Customer-side LAN — outside our address space, exclude.
|
|
{Addr: netip.MustParseAddr("192.168.1.5"), MAC: "18:FD:74:8F:0C:06",
|
|
Identity: "Customer Router", Platform: "MikroTik", Caps: "bridge,router"},
|
|
// Leaked via tunnel — IP not in any directly-connected prefix, exclude.
|
|
{Addr: netip.MustParseAddr("10.250.2.2"), MAC: "B8:69:F4:12:8E:F8",
|
|
Identity: "core to office", Platform: "MikroTik", Caps: "bridge,router"},
|
|
// One of our other routers — exclude via routerIPs map.
|
|
{Addr: netip.MustParseAddr("10.254.254.253"), MAC: "64:D1:54:EF:AD:7D",
|
|
Identity: "Core", Platform: "MikroTik", Caps: "bridge,router"},
|
|
},
|
|
}
|
|
routerIPs := map[netip.Addr]bool{
|
|
netip.MustParseAddr("10.254.254.253"): true,
|
|
netip.MustParseAddr("10.254.254.104"): true,
|
|
}
|
|
switches := collectSwitches(d, routerIPs)
|
|
if len(switches) != 1 {
|
|
t.Fatalf("expected 1 switch, got %d: %+v", len(switches), switches)
|
|
}
|
|
if switches[0].Name != "Culleoka Switch" || switches[0].Model != "Netonix WS-26-400-AC" {
|
|
t.Errorf("unexpected switch: %+v", switches[0])
|
|
}
|
|
}
|
|
|
|
func TestRenderYAMLSampleShape(t *testing.T) {
|
|
s := loadSeed(t)
|
|
site := &outSite{
|
|
Name: "culleoka",
|
|
Router: "10.254.254.104",
|
|
MgmtSubnet: netip.MustParsePrefix("10.10.96.0/20"),
|
|
Cgnats: []cgnat{
|
|
{Subnet: netip.MustParsePrefix("100.64.24.0/22"), Gateway: netip.MustParseAddr("100.64.27.254")},
|
|
},
|
|
AccessPoints: []outRadio{
|
|
{IP: netip.MustParseAddr("10.10.111.1"), MAC: "78:45:58:A0:03:F3", Name: "Culleoka SW AC-1"},
|
|
},
|
|
BackhaulRadios: []outRadio{
|
|
{IP: netip.MustParseAddr("10.250.1.10"), MAC: "7A:8A:20:5F:49:7A",
|
|
Name: "Culleoka to Climax 11ghz", Link: "climax<->culleoka AF11 (disabled)"},
|
|
},
|
|
Switches: []outSwitch{
|
|
{IP: netip.MustParseAddr("100.64.27.250"), MAC: "EC:13:B2:A4:05:CA",
|
|
Name: "Culleoka Switch", Model: "Netonix WS-26-400-AC"},
|
|
},
|
|
}
|
|
body, err := renderYAML(s, []*outSite{site})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(body)
|
|
for _, want := range []string{
|
|
"sites:",
|
|
"culleoka:",
|
|
"router: 10.254.254.104",
|
|
"mgmt_subnet: 10.10.96.0/20",
|
|
"cgnat_subnet: {subnet: 100.64.24.0/22, gateway: 100.64.27.254}",
|
|
"access_points:",
|
|
`{ip: 10.10.111.1, mac: "78:45:58:A0:03:F3", name: "Culleoka SW AC-1"}`,
|
|
"backhaul_radios:",
|
|
`link: "climax<->culleoka AF11 (disabled)"`,
|
|
"switches:",
|
|
`{ip: 100.64.27.250, mac: "EC:13:B2:A4:05:CA", name: "Culleoka Switch", model: "Netonix WS-26-400-AC"}`,
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("rendered yaml missing %q\n--- output ---\n%s", want, got)
|
|
}
|
|
}
|
|
}
|