package cli import ( "path/filepath" "reflect" "strings" "testing" "debtik/internal/aerotun" "debtik/internal/auth" "debtik/internal/dynamicrouting" "debtik/internal/sdwan" "debtik/internal/vpn" ) func TestQuotedCommandArguments(t *testing.T) { got, err := splitCommand(`/interface aerotun set test comment="office uplink" psk='literal $\\key' name=""`) want := []string{"/interface", "aerotun", "set", "test", "comment=office uplink", `psk=literal $\\key`, "name="} if err != nil || !reflect.DeepEqual(got, want) { t.Fatalf("%q %v", got, err) } if _, err := splitCommand(`comment="unfinished`); err == nil { t.Fatal("accepted unterminated quote") } } func TestBareLogAndHandlerRecovery(t *testing.T) { e := &Executor{} user := &auth.PublicUser{Role: auth.RoleFull} output, _ := e.Execute("/log", user) if commandExitStatus(output) != 0 { t.Fatal(output) } output, _ = e.Execute("/interface print", user) // absent manager simulates a handler failure if commandExitStatus(output) == 0 || !strings.Contains(output, "still running") { t.Fatal(output) } output, _ = e.Execute("nonsense", user) if commandExitStatus(output) == 0 { t.Fatal("unknown command returned success") } output, _ = e.Execute("/help", user) if commandExitStatus(output) != 0 { t.Fatal("help returned failure") } } func TestAeroTUNCLIConfigurationAndPermissions(t *testing.T) { m := aerotun.NewManagerWithPath(filepath.Join(t.TempDir(), "aerotun.json")) defer m.StopAll() e := &Executor{vpnMgr: &vpn.Manager{AeroTUN: m}} admin := &auth.PublicUser{Role: auth.RoleFull} psk := strings.Repeat("12", 32) command := `/interface aerotun add name=atest mode=server local-address=10.90.0.1/30 peer-address=10.90.0.2/32 allowed-ips=10.90.0.2/32 psk=` + psk + ` comment="office uplink"` output, _ := e.Execute(command, admin) if commandExitStatus(output) != 0 { t.Fatal(output) } inst := m.List()[0] if !inst.Disabled || inst.PublicKey == "" || inst.Comment != "office uplink" { t.Fatal("draft configuration not retained") } for _, role := range []auth.Role{auth.RoleRead, auth.RoleWrite} { output, _ = e.Execute("/interface aerotun remove atest", &auth.PublicUser{Role: role}) if commandExitStatus(output) == 0 { t.Fatal("non-admin removed tunnel") } } output, _ = e.Execute("/interface aerotun print detail=yes", admin) if strings.Contains(output, psk) || strings.Contains(output, inst.PrivateKey) || !strings.Contains(output, inst.PublicKey) { t.Fatal("secret disclosure or missing public identity") } output, _ = e.Execute(`/interface aerotun set atest comment="updated uplink"`, admin) if commandExitStatus(output) != 0 { t.Fatal(output) } updated, _ := m.Get(inst.ID) if updated.PSK != psk || updated.PublicKey != inst.PublicKey { t.Fatal("edit changed identity") } output, _ = e.Execute("/interface aerotun remove atest", admin) if commandExitStatus(output) != 0 || len(m.List()) != 0 { t.Fatal(output) } } func TestVXLANCLIConfigurationAndPermissions(t *testing.T) { vxlanMgr := vpn.NewVXLANManager(filepath.Join(t.TempDir(), "vxlan.json")) e := &Executor{vpnMgr: &vpn.Manager{VXLAN: vxlanMgr}} admin := &auth.PublicUser{Role: auth.RoleFull} viewer := &auth.PublicUser{Role: auth.RoleRead} // 1. Add VXLAN output, _ := e.Execute(`/interface vxlan add name=vxlan99 vni=99 port=4789 mtu=1500 comment="test overlay"`, admin) if strings.Contains(output, "Error") { t.Fatalf("unexpected error adding vxlan: %s", output) } // 2. Print VXLAN output, _ = e.Execute("/interface vxlan print", viewer) if !strings.Contains(output, "vxlan99") || !strings.Contains(output, "99") { t.Fatalf("vxlan99 not found in print output: %s", output) } // 3. Add VTEP output, _ = e.Execute("/interface vxlan vteps add interface=vxlan99 remote-ip=192.168.1.50 comment=peera", admin) if strings.Contains(output, "Error") { t.Fatalf("unexpected error adding vtep: %s", output) } // 4. Print VTEP output, _ = e.Execute("/interface vxlan vteps print", viewer) if !strings.Contains(output, "192.168.1.50") { t.Fatalf("vtep not found in print output: %s", output) } // 5. Viewer cannot modify output, _ = e.Execute("/interface vxlan remove vxlan99", viewer) if !strings.Contains(output, "Permission denied") { t.Fatalf("expected permission denied for viewer, got: %s", output) } // 6. Set comment output, _ = e.Execute(`/interface vxlan set vxlan99 comment="updated vxlan"`, admin) if strings.Contains(output, "Error") { t.Fatalf("unexpected error setting vxlan: %s", output) } // 7. Remove VTEP output, _ = e.Execute("/interface vxlan vteps remove interface=vxlan99 remote-ip=192.168.1.50", admin) if strings.Contains(output, "Error") { t.Fatalf("unexpected error removing vtep: %s", output) } // 8. Remove VXLAN output, _ = e.Execute("/interface vxlan remove vxlan99", admin) if strings.Contains(output, "Error") { t.Fatalf("unexpected error removing vxlan: %s", output) } } func TestSDWANCLIConfigurationAndPermissions(t *testing.T) { sdwanMgr := sdwan.NewManager(filepath.Join(t.TempDir(), "sdwan.json")) defer sdwanMgr.Stop() e := &Executor{sdwanMgr: sdwanMgr} admin := &auth.PublicUser{Role: auth.RoleFull} viewer := &auth.PublicUser{Role: auth.RoleRead} // 1. Add SD-WAN group output, _ := e.Execute(`/ip sdwan add name=sdwan0 mode=weighted auto-nat=yes probe-interval=5 comment="Multi-WAN LB"`, admin) if strings.Contains(output, "Error") { t.Fatalf("unexpected error adding sdwan group: %s", output) } // 2. Print SD-WAN groups output, _ = e.Execute("/ip sdwan print", viewer) if !strings.Contains(output, "sdwan0") || !strings.Contains(output, "weighted") { t.Fatalf("sdwan0 not found in print output: %s", output) } // 3. Status output, _ = e.Execute("/ip sdwan status", viewer) if !strings.Contains(output, "SD-WAN Telemetry Status") { t.Fatalf("status output unexpected: %s", output) } // 4. Add Members output, _ = e.Execute("/ip sdwan member add group=sdwan0 interface=eth0 gateway=192.168.1.1 weight=70 target-probe=1.1.1.1", admin) if strings.Contains(output, "Error") { t.Fatalf("error adding member eth0: %s", output) } output, _ = e.Execute("/ip sdwan member add group=sdwan0 interface=eth1 gateway=192.168.2.1 weight=30 target-probe=8.8.8.8", admin) if strings.Contains(output, "Error") { t.Fatalf("error adding member eth1: %s", output) } // 5. Print Members output, _ = e.Execute("/ip sdwan member print", viewer) if !strings.Contains(output, "eth0") || !strings.Contains(output, "eth1") { t.Fatalf("members not found in member print output: %s", output) } // 6. Permission check for viewer output, _ = e.Execute("/ip sdwan remove sdwan0", viewer) if !strings.Contains(output, "Permission denied") { t.Fatalf("expected permission denied for viewer, got: %s", output) } // 7. Member set & disable output, _ = e.Execute("/ip sdwan member set group=sdwan0 interface=eth0 weight=80", admin) if strings.Contains(output, "Error") { t.Fatalf("error setting member weight: %s", output) } output, _ = e.Execute("/ip sdwan member disable group=sdwan0 interface=eth1", admin) if strings.Contains(output, "Error") { t.Fatalf("error disabling member eth1: %s", output) } // 8. Group disable & enable output, _ = e.Execute("/ip sdwan disable sdwan0", admin) if strings.Contains(output, "Error") { t.Fatalf("error disabling sdwan0: %s", output) } output, _ = e.Execute("/ip sdwan enable sdwan0", admin) if strings.Contains(output, "Error") { t.Fatalf("error enabling sdwan0: %s", output) } // 9. Remove member output, _ = e.Execute("/ip sdwan member remove group=sdwan0 interface=eth1", admin) if strings.Contains(output, "Error") { t.Fatalf("error removing member eth1: %s", output) } // 10. Remove group output, _ = e.Execute("/ip sdwan remove sdwan0", admin) if strings.Contains(output, "Error") { t.Fatalf("error removing group sdwan0: %s", output) } } func TestRoutingOSPFCLI(t *testing.T) { dynMgr := dynamicrouting.NewManager(filepath.Join(t.TempDir(), "dynamic.json")) e := &Executor{dynRoutingMgr: dynMgr} admin := &auth.PublicUser{Role: auth.RoleFull} viewer := &auth.PublicUser{Role: auth.RoleRead} // 1. Instance commands out, _ := e.Execute("/routing ospf instance print", admin) if !strings.Contains(out, "NAME") { t.Fatalf("expected NAME table header in instance print: %s", out) } out, _ = e.Execute("/routing ospf instance add name=ospf-test router-id=10.255.0.1 redistribute-connected=yes redistribute-static=no default-metric=30", admin) if !strings.Contains(out, "saved") { t.Fatalf("failed to add ospf instance: %s", out) } out, _ = e.Execute("/routing ospf instance set ospf-test default-metric=45", admin) if !strings.Contains(out, "updated") { t.Fatalf("failed to set ospf instance: %s", out) } out, _ = e.Execute("/routing ospf instance disable ospf-test", admin) if !strings.Contains(out, "disabled") { t.Fatalf("failed to disable ospf instance: %s", out) } out, _ = e.Execute("/routing ospf instance enable ospf-test", admin) if !strings.Contains(out, "enabled") { t.Fatalf("failed to enable ospf instance: %s", out) } // Viewer permission check out, _ = e.Execute("/routing ospf instance remove ospf-test", viewer) if !strings.Contains(out, "Permission denied") { t.Fatalf("expected permission denied for viewer: %s", out) } // 2. Area commands out, _ = e.Execute("/routing ospf area print", admin) if !strings.Contains(out, "AREA-ID") { t.Fatalf("expected AREA-ID table header in area print: %s", out) } out, _ = e.Execute("/routing ospf area add name=area1 area-id=0.0.0.1 type=stub default-cost=20 no-summaries=yes", admin) if !strings.Contains(out, "saved") { t.Fatalf("failed to add ospf area: %s", out) } out, _ = e.Execute("/routing ospf area set area1 default-cost=35", admin) if !strings.Contains(out, "updated") { t.Fatalf("failed to set ospf area: %s", out) } // 3. Area range commands out, _ = e.Execute("/routing ospf area range print", admin) if !strings.Contains(out, "PREFIX") { t.Fatalf("expected PREFIX in area range print: %s", out) } out, _ = e.Execute("/routing ospf area range add prefix=10.10.0.0/16 cost=10 advertise=no", admin) if !strings.Contains(out, "added") { t.Fatalf("failed to add area range: %s", out) } out, _ = e.Execute("/routing ospf area range set 10.10.0.0/16 cost=20 advertise=yes", admin) if !strings.Contains(out, "updated") { t.Fatalf("failed to set area range: %s", out) } out, _ = e.Execute("/routing ospf area range remove 10.10.0.0/16", admin) if !strings.Contains(out, "removed") { t.Fatalf("failed to remove area range: %s", out) } // 4. Interface template commands out, _ = e.Execute("/routing ospf interface print", admin) if !strings.Contains(out, "INTERFACE") { t.Fatalf("expected INTERFACE in interface print: %s", out) } out, _ = e.Execute("/routing ospf interface add interface=eth0,eth1 area=Default cost=15 network-type=nonbroadcast auth=md5 auth-key=secret123", admin) if !strings.Contains(out, "saved") { t.Fatalf("failed to add ospf interface template: %s", out) } out, _ = e.Execute("/routing ospf interface set eth0,eth1 cost=25", admin) if !strings.Contains(out, "updated") { t.Fatalf("failed to set ospf interface template: %s", out) } out, _ = e.Execute("/routing ospf interface remove eth0,eth1", admin) if !strings.Contains(out, "removed") { t.Fatalf("failed to remove ospf interface template: %s", out) } // 5. Static neighbor commands out, _ = e.Execute("/routing ospf static-neighbor print", admin) if !strings.Contains(out, "ADDRESS") { t.Fatalf("expected ADDRESS in static-neighbor print: %s", out) } out, _ = e.Execute("/routing ospf static-neighbor add address=192.168.100.2 poll-interval=30 cost=5", admin) if !strings.Contains(out, "saved") { t.Fatalf("failed to add static neighbor: %s", out) } out, _ = e.Execute("/routing ospf static-neighbor set 192.168.100.2 poll-interval=60", admin) if !strings.Contains(out, "updated") { t.Fatalf("failed to update static neighbor: %s", out) } out, _ = e.Execute("/routing ospf static-neighbor disable 192.168.100.2", admin) if !strings.Contains(out, "disabled") { t.Fatalf("failed to disable static neighbor: %s", out) } out, _ = e.Execute("/routing ospf static-neighbor enable 192.168.100.2", admin) if !strings.Contains(out, "enabled") { t.Fatalf("failed to enable static neighbor: %s", out) } out, _ = e.Execute("/routing ospf static-neighbor remove 192.168.100.2", admin) if !strings.Contains(out, "removed") { t.Fatalf("failed to remove static neighbor: %s", out) } // 6. Live status commands out, _ = e.Execute("/routing ospf neighbor print", admin) if !strings.Contains(out, "ROUTER-ID") { t.Fatalf("expected ROUTER-ID in neighbor print: %s", out) } out, _ = e.Execute("/routing ospf lsa print", admin) if !strings.Contains(out, "TYPE") { t.Fatalf("expected TYPE in lsa print: %s", out) } out, _ = e.Execute("/routing ospf route print", admin) if !strings.Contains(out, "PREFIX") { t.Fatalf("expected PREFIX in route print: %s", out) } // Cleanup out, _ = e.Execute("/routing ospf area remove area1", admin) if !strings.Contains(out, "removed") { t.Fatalf("failed to remove area1: %s", out) } out, _ = e.Execute("/routing ospf instance remove ospf-test", admin) if !strings.Contains(out, "removed") { t.Fatalf("failed to remove ospf-test: %s", out) } }