package dynamicrouting import ( "os" "path/filepath" "strings" "testing" ) func TestOSPFStatusIsPerInstance(t *testing.T) { dir := t.TempDir() text := "#!/bin/sh\nprintf 'ospf_a OSPF master4 up 00:00\\nospf_b OSPF master4 down 00:00\\n'\n" if err := os.WriteFile(filepath.Join(dir, "birdc"), []byte(text), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", dir+":"+os.Getenv("PATH")) m := &Manager{} m.config.OSPFInstances = []OSPFInstance{{Name: "a"}, {Name: "b"}, {Name: "c"}} got := m.ListOSPFInstances() if !got[0].Running || got[1].Running || got[2].Running { t.Fatal("one OSPF instance marked other instances running", got) } } func TestOSPFBirdConfigGeneration(t *testing.T) { m := &Manager{} m.config.OSPFInstances = []OSPFInstance{ { ID: "inst-1", Name: "core", Version: 2, VRF: "vrf-mgmt", RoutingTable: "t100", RouterID: "10.255.0.1", Enabled: true, RedistributeConnected: true, RedistributeStatic: false, // Must NOT export RTS_STATIC RedistributeBGP: true, DefaultMetric: 55, }, } m.config.OSPFAreas = []OSPFArea{ { ID: "area-1", Name: "stub-area", Instance: "core", AreaID: "0.0.0.10", AreaType: "stub", DefaultCost: 25, NoSummaries: true, }, } m.config.OSPFAreaRanges = []OSPFAreaRange{ { ID: "range-1", Area: "stub-area", Prefix: "10.10.0.0/16", Cost: 10, Advertise: false, // Must emit hidden; }, { ID: "range-2", Area: "stub-area", Prefix: "10.20.0.0/16", Cost: 5, Advertise: true, }, } m.config.OSPFInterfaces = []OSPFInterface{ { ID: "iface-1", Interfaces: "eth0, eth1", Area: "stub-area", Networks: "10.10.1.0/24", NetworkType: "nonbroadcast", Cost: 15, Priority: 64, Auth: "md5", AuthKey: "secretpass", }, } m.config.OSPFStaticNbrs = []OSPFStaticNeighbor{ { ID: "snbr-1", Address: "10.10.1.5", Area: "stub-area", PollInterval: 30, Cost: 15, }, { ID: "snbr-2", Address: "192.168.99.1", Area: "stub-area", PollInterval: 45, Cost: 20, }, } conf := m.generateBirdConfig() // 1. Verify instance settings if !strings.Contains(conf, "protocol ospf v2 ospf_core {") { t.Fatalf("missing protocol ospf v2 ospf_core in config:\n%s", conf) } if !strings.Contains(conf, "vrf vrf-mgmt;") { t.Fatalf("missing vrf vrf-mgmt in config:\n%s", conf) } if !strings.Contains(conf, "router id 10.255.0.1;") { t.Fatalf("missing router id 10.255.0.1 in config:\n%s", conf) } if !strings.Contains(conf, "table t100;") { t.Fatalf("missing table t100 in config:\n%s", conf) } // 2. Verify redistribution & default metric ospfBlock := conf[strings.Index(conf, "protocol ospf v2 ospf_core"):] if strings.Contains(ospfBlock, "RTS_STATIC") { t.Fatalf("RTS_STATIC exported even though RedistributeStatic is false:\n%s", ospfBlock) } if !strings.Contains(ospfBlock, "RTS_DEVICE, RTS_BGP") { t.Fatalf("expected RTS_DEVICE, RTS_BGP in export filter:\n%s", ospfBlock) } if !strings.Contains(ospfBlock, "ospf_metric1 = 55;") { t.Fatalf("missing ospf_metric1 = 55; in export filter:\n%s", ospfBlock) } // 3. Verify area options: stub, default cost, summary no if !strings.Contains(conf, "area 0.0.0.10 {") { t.Fatalf("missing area 0.0.0.10 in config:\n%s", conf) } if !strings.Contains(conf, "stub;") { t.Fatalf("missing stub; in area:\n%s", conf) } if !strings.Contains(conf, "default cost 25;") { t.Fatalf("missing default cost 25; in area:\n%s", conf) } if !strings.Contains(conf, "summary no;") { t.Fatalf("missing summary no; in area:\n%s", conf) } // 4. Verify area range advertising: hidden; for advertise=false if !strings.Contains(conf, "stubnet 10.10.0.0/16 { cost 10; hidden; };") { t.Fatalf("missing stubnet 10.10.0.0/16 { cost 10; hidden; }; in config:\n%s", conf) } if !strings.Contains(conf, "stubnet 10.20.0.0/16 { cost 5; };") { t.Fatalf("missing stubnet 10.20.0.0/16 { cost 5; }; in config:\n%s", conf) } // 5. Verify interface template formatting: comma separated interfaces if !strings.Contains(conf, "interface \"eth0\", \"eth1\", 10.10.1.0/24 {") { t.Fatalf("missing interface \"eth0\", \"eth1\", 10.10.1.0/24 { in config:\n%s", conf) } if !strings.Contains(conf, "type nonbroadcast;") { t.Fatalf("missing type nonbroadcast; in config:\n%s", conf) } if !strings.Contains(conf, "cost 15;") { t.Fatalf("missing cost 15; in config:\n%s", conf) } if !strings.Contains(conf, "priority 64;") { t.Fatalf("missing priority 64; in config:\n%s", conf) } if !strings.Contains(conf, "authentication cryptographic;\n password \"secretpass\";") { t.Fatalf("missing authentication cryptographic and password in config:\n%s", conf) } // 6. Verify static neighbor attached to matching interface template if !strings.Contains(conf, "10.10.1.5;") { t.Fatalf("missing static neighbor 10.10.1.5 in config:\n%s", conf) } // 7. Verify remaining static neighbor in fallback interface "*" if !strings.Contains(conf, "192.168.99.1;") { t.Fatalf("missing static neighbor 192.168.99.1 in config:\n%s", conf) } } func TestOSPFStaticNeighborManagement(t *testing.T) { m := &Manager{} snbr := OSPFStaticNeighbor{ Address: "10.0.0.2", Area: "Default", PollInterval: 60, Cost: 5, } if err := m.SaveOSPFStaticNeighbor(snbr); err != nil { // applyBirdConfig may fail if bird is not running in test env, but config must be in memory t.Logf("Save returned error (expected in mock env): %v", err) } list := m.ListOSPFStaticNeighbors() if len(list) != 1 || list[0].Address != "10.0.0.2" { t.Fatalf("expected 1 static neighbor with address 10.0.0.2, got: %+v", list) } id := list[0].ID _ = m.SetOSPFStaticNeighborState(id, true) list2 := m.ListOSPFStaticNeighbors() if !list2[0].Disabled { t.Fatalf("expected neighbor to be disabled, got: %+v", list2[0]) } _ = m.DeleteOSPFStaticNeighbor(id) list3 := m.ListOSPFStaticNeighbors() if len(list3) != 0 { t.Fatalf("expected 0 static neighbors after delete, got: %+v", list3) } }