| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- 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 /int<Tab>br<Tab>po<Tab><Tab><Tab>pr<Tab>\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)
- }
- }
|