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