network/mikrotik-tool/inventory_test.go
2026-05-08 17:47:42 -05:00

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