package cli import ( "path/filepath" "reflect" "strings" "testing" "debtik/internal/aerotun" "debtik/internal/auth" "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) } }