package cisco import ( "testing" ) // --- Abbreviation resolution tests --- func TestResolveAbbreviationExact(t *testing.T) { entries := []commandEntry{ {name: "show"}, {name: "shutdown"}, } got, err := resolveAbbreviation("show", entries) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "show" { t.Errorf("got %q, want %q", got, "show") } } func TestResolveAbbreviationUnique(t *testing.T) { entries := []commandEntry{ {name: "show"}, {name: "enable"}, {name: "exit"}, } got, err := resolveAbbreviation("sh", entries) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "show" { t.Errorf("got %q, want %q", got, "show") } } func TestResolveAbbreviationAmbiguous(t *testing.T) { entries := []commandEntry{ {name: "show"}, {name: "shutdown"}, } _, err := resolveAbbreviation("sh", entries) if err == nil { t.Fatal("expected ambiguous error, got nil") } if err.Error() != "ambiguous" { t.Errorf("got error %q, want %q", err.Error(), "ambiguous") } } func TestResolveAbbreviationUnknown(t *testing.T) { entries := []commandEntry{ {name: "show"}, {name: "enable"}, } _, err := resolveAbbreviation("xyz", entries) if err == nil { t.Fatal("expected unknown error, got nil") } if err.Error() != "unknown" { t.Errorf("got error %q, want %q", err.Error(), "unknown") } } func TestResolveAbbreviationCaseInsensitive(t *testing.T) { entries := []commandEntry{ {name: "show"}, {name: "enable"}, } got, err := resolveAbbreviation("SH", entries) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "show" { t.Errorf("got %q, want %q", got, "show") } } // --- Multi-word command resolution tests --- func TestResolveCommandShowRunningConfig(t *testing.T) { resolved, args, err := resolveCommand([]string{"sh", "run"}, privilegedExecCommands) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(args) != 0 { t.Errorf("unexpected args: %v", args) } want := []string{"show", "running-config"} if len(resolved) != len(want) { t.Fatalf("resolved = %v, want %v", resolved, want) } for i := range want { if resolved[i] != want[i] { t.Errorf("resolved[%d] = %q, want %q", i, resolved[i], want[i]) } } } func TestResolveCommandConfigureTerminal(t *testing.T) { resolved, _, err := resolveCommand([]string{"conf", "t"}, privilegedExecCommands) if err != nil { t.Fatalf("unexpected error: %v", err) } want := []string{"configure", "terminal"} if len(resolved) != len(want) { t.Fatalf("resolved = %v, want %v", resolved, want) } for i := range want { if resolved[i] != want[i] { t.Errorf("resolved[%d] = %q, want %q", i, resolved[i], want[i]) } } } func TestResolveCommandShowIPInterfaceBrief(t *testing.T) { resolved, _, err := resolveCommand([]string{"sh", "ip", "int", "br"}, privilegedExecCommands) if err != nil { t.Fatalf("unexpected error: %v", err) } want := []string{"show", "ip", "interface", "brief"} if len(resolved) != len(want) { t.Fatalf("resolved = %v, want %v", resolved, want) } for i := range want { if resolved[i] != want[i] { t.Errorf("resolved[%d] = %q, want %q", i, resolved[i], want[i]) } } } func TestResolveCommandWithArgs(t *testing.T) { // "hostname MyRouter" → resolved=["hostname"], args=["MyRouter"] resolved, args, err := resolveCommand([]string{"hostname", "MyRouter"}, globalConfigCommands) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(resolved) != 1 || resolved[0] != "hostname" { t.Errorf("resolved = %v, want [hostname]", resolved) } if len(args) != 1 || args[0] != "MyRouter" { t.Errorf("args = %v, want [MyRouter]", args) } } func TestResolveCommandAmbiguous(t *testing.T) { // In user exec, "e" matches "enable" and "exit" — ambiguous _, _, err := resolveCommand([]string{"e"}, userExecCommands) if err == nil { t.Fatal("expected ambiguous error") } } // --- Mode state machine tests --- func TestPromptGeneration(t *testing.T) { tests := []struct { mode iosMode want string }{ {modeUserExec, "Router>"}, {modePrivilegedExec, "Router#"}, {modeGlobalConfig, "Router(config)#"}, {modeInterfaceConfig, "Router(config-if)#"}, } for _, tt := range tests { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = tt.mode if got := s.prompt(); got != tt.want { t.Errorf("prompt(%d) = %q, want %q", tt.mode, got, tt.want) } } } func TestPromptAfterHostnameChange(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modeGlobalConfig s.dispatch("hostname Switch1") if s.hostname != "Switch1" { t.Fatalf("hostname = %q, want %q", s.hostname, "Switch1") } if got := s.prompt(); got != "Switch1(config)#" { t.Errorf("prompt = %q, want %q", got, "Switch1(config)#") } } func TestModeTransitions(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") // Start in user exec. if s.mode != modeUserExec { t.Fatalf("initial mode = %d, want %d", s.mode, modeUserExec) } // Can't skip to config mode directly from user exec. result := s.dispatch("configure terminal") if result.output == "" { t.Error("expected error for conf t in user exec mode") } // Manually set privileged mode (enable tested separately). s.mode = modePrivilegedExec // conf t → global config s.dispatch("configure terminal") if s.mode != modeGlobalConfig { t.Errorf("mode after conf t = %d, want %d", s.mode, modeGlobalConfig) } // interface Gi0/0 → interface config s.dispatch("interface GigabitEthernet0/0") if s.mode != modeInterfaceConfig { t.Errorf("mode after interface = %d, want %d", s.mode, modeInterfaceConfig) } // exit → back to global config s.dispatch("exit") if s.mode != modeGlobalConfig { t.Errorf("mode after exit from if-config = %d, want %d", s.mode, modeGlobalConfig) } // end → back to privileged exec s.dispatch("end") if s.mode != modePrivilegedExec { t.Errorf("mode after end = %d, want %d", s.mode, modePrivilegedExec) } // disable → back to user exec s.dispatch("disable") if s.mode != modeUserExec { t.Errorf("mode after disable = %d, want %d", s.mode, modeUserExec) } } func TestEndFromInterfaceConfig(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modeInterfaceConfig s.currentIf = "GigabitEthernet0/0" s.dispatch("end") if s.mode != modePrivilegedExec { t.Errorf("mode after end = %d, want %d", s.mode, modePrivilegedExec) } if s.currentIf != "" { t.Errorf("currentIf = %q, want empty", s.currentIf) } } func TestExitFromPrivilegedExec(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modePrivilegedExec result := s.dispatch("exit") if !result.exit { t.Error("expected exit=true from privileged exec exit") } } // --- Show command output tests --- func TestShowVersionContainsModel(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") output := showVersion(s) if !contains(output, "C2960") { t.Error("show version missing model") } if !contains(output, "15.0(2)SE11") { t.Error("show version missing IOS version") } if !contains(output, "Router") { t.Error("show version missing hostname") } } func TestShowRunningConfigContainsInterfaces(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") output := showRunningConfig(s) if !contains(output, "hostname Router") { t.Error("running-config missing hostname") } if !contains(output, "interface GigabitEthernet0/0") { t.Error("running-config missing interface") } if !contains(output, "ip address 192.168.1.1") { t.Error("running-config missing IP address") } if !contains(output, "line vty") { t.Error("running-config missing VTY config") } } func TestShowRunningConfigWithEnableSecret(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "secret123") output := showRunningConfig(s) if !contains(output, "enable secret") { t.Error("running-config missing enable secret when password is set") } } func TestShowRunningConfigWithoutEnableSecret(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") output := showRunningConfig(s) if contains(output, "enable secret") { t.Error("running-config should not have enable secret when password is empty") } } func TestShowIPInterfaceBrief(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") output := showIPInterfaceBrief(s) if !contains(output, "GigabitEthernet0/0") { t.Error("ip interface brief missing GigabitEthernet0/0") } if !contains(output, "192.168.1.1") { t.Error("ip interface brief missing 192.168.1.1") } } func TestShowIPRoute(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") output := showIPRoute(s) if !contains(output, "directly connected") { t.Error("ip route missing connected routes") } if !contains(output, "0.0.0.0/0") { t.Error("ip route missing default route") } } func TestShowVLANBrief(t *testing.T) { output := showVLANBrief() if !contains(output, "default") { t.Error("vlan brief missing default vlan") } if !contains(output, "MGMT") { t.Error("vlan brief missing MGMT vlan") } } // --- Interface config tests --- func TestInterfaceShutdownNoShutdown(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modeInterfaceConfig s.currentIf = "GigabitEthernet0/0" s.dispatch("shutdown") iface := s.findInterface("GigabitEthernet0/0") if iface == nil { t.Fatal("interface not found") } if !iface.shutdown { t.Error("interface should be shutdown") } if iface.status != "administratively down" { t.Errorf("status = %q, want %q", iface.status, "administratively down") } s.dispatch("no shutdown") if iface.shutdown { t.Error("interface should not be shutdown after no shutdown") } if iface.status != "up" { t.Errorf("status = %q, want %q", iface.status, "up") } } func TestInterfaceIPAddress(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modeInterfaceConfig s.currentIf = "GigabitEthernet0/0" s.dispatch("ip address 10.10.10.1 255.255.255.0") iface := s.findInterface("GigabitEthernet0/0") if iface == nil { t.Fatal("interface not found") } if iface.ip != "10.10.10.1" { t.Errorf("ip = %q, want %q", iface.ip, "10.10.10.1") } if iface.mask != "255.255.255.0" { t.Errorf("mask = %q, want %q", iface.mask, "255.255.255.0") } } // --- Dispatch / invalid command tests --- func TestInvalidCommandInUserExec(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") result := s.dispatch("foobar") if !contains(result.output, "Invalid input") { t.Errorf("expected invalid input error, got %q", result.output) } } func TestAmbiguousCommandOutput(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") // "e" in user exec is ambiguous (enable, exit) result := s.dispatch("e") if !contains(result.output, "Ambiguous") { t.Errorf("expected ambiguous error, got %q", result.output) } } func TestHelpCommand(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") result := s.dispatch("?") if !contains(result.output, "show") { t.Error("help missing 'show'") } if !contains(result.output, "enable") { t.Error("help missing 'enable'") } } // --- Abbreviation integration tests --- func TestShowAbbreviationInDispatch(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modePrivilegedExec result := s.dispatch("sh ver") if !contains(result.output, "Cisco IOS Software") { t.Error("'sh ver' should produce version output") } } func TestConfTAbbreviation(t *testing.T) { s := newIOSState("Router", "C2960", "15.0(2)SE11", "") s.mode = modePrivilegedExec s.dispatch("conf t") if s.mode != modeGlobalConfig { t.Errorf("mode after conf t = %d, want %d", s.mode, modeGlobalConfig) } } // --- Enable command detection --- func TestIsEnableCommand(t *testing.T) { tests := []struct { input string want bool }{ {"enable", true}, {"en", true}, {"ena", true}, {"e", false}, // too short (single char could be other commands) {"enab", true}, {"ENABLE", true}, {"exit", false}, {"enable 15", false}, // has extra argument } for _, tt := range tests { if got := isEnableCommand(tt.input); got != tt.want { t.Errorf("isEnableCommand(%q) = %v, want %v", tt.input, got, tt.want) } } } // --- configString tests --- func TestConfigString(t *testing.T) { cfg := map[string]any{"hostname": "MySwitch"} if got := configString(cfg, "hostname", "Router"); got != "MySwitch" { t.Errorf("configString() = %q, want %q", got, "MySwitch") } if got := configString(cfg, "missing", "Default"); got != "Default" { t.Errorf("configString() for missing = %q, want %q", got, "Default") } if got := configString(nil, "key", "Default"); got != "Default" { t.Errorf("configString(nil) = %q, want %q", got, "Default") } } // --- Helper --- func TestMaskBits(t *testing.T) { tests := []struct { mask string want int }{ {"255.255.255.0", 24}, {"255.255.255.252", 30}, {"255.255.0.0", 16}, {"255.0.0.0", 8}, } for _, tt := range tests { if got := maskBits(tt.mask); got != tt.want { t.Errorf("maskBits(%q) = %d, want %d", tt.mask, got, tt.want) } } } func TestNetworkFromIP(t *testing.T) { tests := []struct { ip, mask, want string }{ {"192.168.1.1", "255.255.255.0", "192.168.1.0"}, {"10.0.0.1", "255.255.255.252", "10.0.0.0"}, {"172.16.5.100", "255.255.0.0", "172.16.0.0"}, } for _, tt := range tests { if got := networkFromIP(tt.ip, tt.mask); got != tt.want { t.Errorf("networkFromIP(%q, %q) = %q, want %q", tt.ip, tt.mask, got, tt.want) } } } // --- Shell metadata --- func TestShellNameAndDescription(t *testing.T) { s := NewCiscoShell() if s.Name() != "cisco" { t.Errorf("Name() = %q, want %q", s.Name(), "cisco") } if s.Description() == "" { t.Error("Description() should not be empty") } } func contains(s, substr string) bool { return len(s) >= len(substr) && containsHelper(s, substr) } func containsHelper(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }