package cli import ( "bytes" "strings" "testing" "time" terminal "golang.org/x/term" ) type mockTerminalWriter struct { buf bytes.Buffer } func (m *mockTerminalWriter) Write(p []byte) (n int, err error) { return m.buf.Write(p) } func TestCompleterRoot(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) // 1. Single tab on empty line: ambiguous, no advance newLine, newPos, ok := completer.Callback("", 0, '\t') if !ok || newLine != "" || newPos != 0 { t.Fatalf("Expected empty line and ok=true, got newLine=%q, newPos=%d, ok=%v", newLine, newPos, ok) } if mock.buf.Len() != 0 { t.Fatalf("Expected no terminal output on 1st tab, got %q", mock.buf.String()) } // 2. Second tab immediately after: displays all root options newLine, newPos, ok = completer.Callback("", 0, '\t') if !ok || newLine != "" || newPos != 0 { t.Fatalf("Expected empty line and ok=true, got newLine=%q, newPos=%d, ok=%v", newLine, newPos, ok) } out := mock.buf.String() if !strings.Contains(out, "interface") || !strings.Contains(out, "ip") || !strings.Contains(out, "routing") { t.Fatalf("Expected root commands in double tab output, got: %s", out) } } func TestCompleterSlashRoot(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) // 1. Single tab on "/": ambiguous, no advance newLine, newPos, ok := completer.Callback("/", 1, '\t') if !ok || newLine != "/" || newPos != 1 { t.Fatalf("Expected '/' and ok=true, got %q", newLine) } // 2. Second tab on "/": displays root choices newLine, newPos, ok = completer.Callback("/", 1, '\t') if !ok || newLine != "/" { t.Fatalf("Expected '/' and ok=true, got %q", newLine) } out := mock.buf.String() if !strings.Contains(out, "interface") || !strings.Contains(out, "ip") { t.Fatalf("Expected options on double tab, got: %s", out) } } func TestCompleterUniqueMatch(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) // "/in" should complete to "/interface " on first tab newLine, newPos, ok := completer.Callback("/in", 3, '\t') if !ok { t.Fatalf("Expected ok=true") } if newLine != "/interface " { t.Fatalf("Expected '/interface ', got %q", newLine) } if newPos != len("/interface ") { t.Fatalf("Expected newPos=%d, got %d", len("/interface "), newPos) } // "rout" should complete to "routing " on first tab newLine, newPos, ok = completer.Callback("rout", 4, '\t') if !ok || newLine != "routing " { t.Fatalf("Expected 'routing ', got %q", newLine) } } func TestCompleterCommonPrefix(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) // "/ip dh" matches "dhcp-client" and "dhcp-server" -> LCP is "dhcp-" newLine, newPos, ok := completer.Callback("/ip dh", 6, '\t') if !ok || newLine != "/ip dhcp-" { t.Fatalf("Expected '/ip dhcp-', got %q", newLine) } if newPos != len("/ip dhcp-") { t.Fatalf("Expected newPos=%d, got %d", len("/ip dhcp-"), newPos) } // Immediate second tab should show the two options mock.buf.Reset() newLine, newPos, ok = completer.Callback(newLine, newPos, '\t') if !ok || newLine != "/ip dhcp-" { t.Fatalf("Expected '/ip dhcp-', got %q", newLine) } out := mock.buf.String() if !strings.Contains(out, "dhcp-client") || !strings.Contains(out, "dhcp-server") { t.Fatalf("Expected dhcp choices, got: %s", out) } } func TestCompleterSubsectionsDoubleTab(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) subsections := []struct { input string expectedIn []string description string }{ { input: "/ip ", expectedIn: []string{"address", "dhcp-client", "dhcp-server", "dns", "firewall", "route", "service"}, description: "/ip subsections", }, { input: "/interface ", expectedIn: []string{"bridge", "eoip", "pppoe-client", "vlan", "vrrp", "wireguard", "print", "enable", "disable"}, description: "/interface subsections", }, { input: "/interface bridge ", expectedIn: []string{"add", "disable", "enable", "port", "print", "remove"}, description: "/interface bridge actions and port", }, { input: "/interface bridge port ", expectedIn: []string{"add", "disable", "enable", "print", "remove"}, description: "/interface bridge port actions", }, { input: "/ip firewall ", expectedIn: []string{"address-list", "filter", "mangle", "nat"}, description: "/ip firewall subsections", }, { input: "/ip firewall filter ", expectedIn: []string{"add", "disable", "enable", "print", "remove"}, description: "/ip firewall filter actions", }, { input: "/ip service ", expectedIn: []string{"disable", "enable", "print", "set"}, description: "/ip service actions", }, { input: "/ip service enable ", expectedIn: []string{"ssh", "telnet", "www", "www-ssl"}, description: "/ip service enable service choices", }, { input: "/certificate ", expectedIn: []string{"add", "add-ca", "export", "issue-letsencrypt", "print", "remove"}, description: "/certificate subsections", }, { input: "/routing ", expectedIn: []string{"bgp", "filter", "ospf"}, description: "/routing subsections", }, { input: "/routing bgp ", expectedIn: []string{"instance", "network", "peer", "routes"}, description: "/routing bgp subsections", }, { input: "/routing ospf ", expectedIn: []string{"area", "instance", "interface", "neighbor"}, description: "/routing ospf subsections", }, { input: "/tool ", expectedIn: []string{"bandwidth-test", "ping", "traceroute"}, description: "/tool subsections", }, { input: "/user ", expectedIn: []string{"add", "print", "remove", "set"}, description: "/user actions", }, { input: "/system ", expectedIn: []string{"identity", "reboot", "resource"}, description: "/system subsections", }, { input: "/system identity ", expectedIn: []string{"print", "set"}, description: "/system identity actions", }, } for _, tc := range subsections { t.Run(tc.description, func(t *testing.T) { mock.buf.Reset() // Tab 1 completer.Callback(tc.input, len(tc.input), '\t') // Tab 2 completer.Callback(tc.input, len(tc.input), '\t') out := mock.buf.String() for _, expected := range tc.expectedIn { if !strings.Contains(out, expected) { t.Errorf("In %s, expected %q in output, got: %s", tc.description, expected, out) } } }) } } func TestCompleterParameters(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) // Under /ip firewall filter add, double tab should show parameter options mock.buf.Reset() completer.Callback("/ip firewall filter add ", len("/ip firewall filter add "), '\t') completer.Callback("/ip firewall filter add ", len("/ip firewall filter add "), '\t') out := mock.buf.String() expectedParams := []string{"action=", "chain=", "comment=", "dst-address=", "protocol=", "src-address="} for _, p := range expectedParams { if !strings.Contains(out, p) { t.Errorf("Expected param %q in output, got: %s", p, out) } } // Complete "ch" -> "chain=" (no space after =) newLine, _, ok := completer.Callback("/ip firewall filter add ch", len("/ip firewall filter add ch"), '\t') if !ok || newLine != "/ip firewall filter add chain=" { t.Fatalf("Expected parameter completion 'chain=', got %q", newLine) } // At "chain=", 2 tabs should show values ("forward", "input", "output") mock.buf.Reset() completer.Callback(newLine, len(newLine), '\t') completer.Callback(newLine, len(newLine), '\t') out = mock.buf.String() if !strings.Contains(out, "forward") || !strings.Contains(out, "input") || !strings.Contains(out, "output") { t.Fatalf("Expected chain values, got: %s", out) } // Complete value "chain=f" -> "chain=forward " (with trailing space!) newLine, _, ok = completer.Callback("/ip firewall filter add chain=f", len("/ip firewall filter add chain=f"), '\t') if !ok || newLine != "/ip firewall filter add chain=forward " { t.Fatalf("Expected 'chain=forward ', got %q", newLine) } // Once "chain=forward " is used, double tab should NOT suggest "chain=" again mock.buf.Reset() completer.Callback(newLine, len(newLine), '\t') completer.Callback(newLine, len(newLine), '\t') out = mock.buf.String() if strings.Contains(out, "chain=") { t.Fatalf("Expected already used 'chain=' to be omitted, got: %s", out) } if !strings.Contains(out, "action=") { t.Fatalf("Expected 'action=' still present, got: %s", out) } } func TestCompleterDoubleTabTimeout(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) // Press 1 completer.Callback("/ip ", 4, '\t') // Artificially advance time past timeout completer.lastTabTime = time.Now().Add(-1500 * time.Millisecond) // Press 2 (after timeout): should be treated as new 1st tab, so NO print! mock.buf.Reset() completer.Callback("/ip ", 4, '\t') if mock.buf.Len() != 0 { t.Fatalf("Expected no output after timeout, got: %s", mock.buf.String()) } // Press 3 (within 1s of Press 2): should be 2nd tab and print! completer.Callback("/ip ", 4, '\t') if mock.buf.Len() == 0 { t.Fatalf("Expected output on 2nd tab within timeout") } } func TestCompleterIgnoredNonTab(t *testing.T) { mock := &mockTerminalWriter{} completer := NewCompleter(mock, nil) _, _, ok := completer.Callback("/ip ", 4, 'a') if ok { t.Fatalf("Expected ok=false for non-tab key 'a'") } } type testTerminalRW struct { in bytes.Buffer out bytes.Buffer } func (rw *testTerminalRW) Read(p []byte) (n int, err error) { return rw.in.Read(p) } func (rw *testTerminalRW) Write(p []byte) (n int, err error) { return rw.out.Write(p) } func TestCompleterWithRealTerminal(t *testing.T) { rw := &testTerminalRW{} term := terminal.NewTerminal(rw, "[admin@DebTik] > ") completer := NewCompleter(term, nil) term.AutoCompleteCallback = completer.Callback // Feed input: "/ip " followed by 2 tabs, then "firewall print\r\n" rw.in.WriteString("/ip \t\tfirewall print\r\n") line, err := term.ReadLine() if err != nil { t.Fatalf("ReadLine error: %v", err) } if line != "/ip firewall print" { t.Fatalf("Expected '/ip firewall print', got %q", line) } out := rw.out.String() // Output should contain the subsections formatted on double tab if !strings.Contains(out, "address") || !strings.Contains(out, "firewall") || !strings.Contains(out, "route") { t.Fatalf("Terminal output missing double-tab options: %s", out) } } func TestCompleterFullInteractiveSession(t *testing.T) { rw := &testTerminalRW{} term := terminal.NewTerminal(rw, "[admin@DebTik] > ") completer := NewCompleter(term, nil) term.AutoCompleteCallback = completer.Callback // Type /intbrpopr\r\n rw.in.WriteString("/int\tbr\tpo\t\t\tpr\t\r\n") line, err := term.ReadLine() if err != nil { t.Fatalf("ReadLine error: %v", err) } if line != "/interface bridge port print " { t.Fatalf("Expected '/interface bridge port print ', got %q", line) } out := rw.out.String() if !strings.Contains(out, "add") || !strings.Contains(out, "disable") || !strings.Contains(out, "print") { t.Fatalf("Expected bridge port options in output, got: %s", out) } }