diff --git a/pkg/command/share/floater_test.go b/pkg/command/share/floater_test.go new file mode 100644 index 00000000..5c9930b7 --- /dev/null +++ b/pkg/command/share/floater_test.go @@ -0,0 +1,450 @@ +package share + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func compareStringArray(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} +func TestPodIPToArray(t *testing.T) { + tests := []struct { + name string + pods []corev1.PodIP + expectedCMs []string + pass bool + }{ + { + name: "Test Case 1", + pods: []corev1.PodIP{ + { + IP: "10.0.0.1", + }, + { + IP: "10.0.0.2", + }, + }, + expectedCMs: []string{ + "10.0.0.1", + "10.0.0.2", + }, + pass: true, + }, + { + name: "Test Case 2", + pods: []corev1.PodIP{ + { + IP: "10.0.1.1", + }, + { + IP: "10.0.1.2", + }, + }, + expectedCMs: []string{ + "10.0.1.1", + "10.0.1.2", + }, + pass: true, + }, + { + name: "Test Case 3", + pods: []corev1.PodIP{ + { + IP: "192.168.1.1", + }, + { + IP: "192.168.1.2", + }, + }, + expectedCMs: []string{ + "192.168.1.1", + "192.168.1.2", + }, + pass: true, + }, + { + name: "Test Case 4", + pods: []corev1.PodIP{ + { + IP: "172.16.0.1", + }, + { + IP: "172.16.0.2", + }, + }, + expectedCMs: []string{ + "172.16.0.1", + "172.16.0.2", + }, + pass: true, + }, + { + name: "Test Case 5", + pods: []corev1.PodIP{ + { + IP: "192.0.2.1", + }, + { + IP: "192.0.2.2", + }, + }, + expectedCMs: []string{ + "192.0.2.1", + "192.0.2.2", + }, + pass: true, + }, + { + name: "Test Case 6", + pods: []corev1.PodIP{ + { + IP: "203.0.113.1", + }, + { + IP: "203.0.113.2", + }, + }, + expectedCMs: []string{ + "203.0.113.1", + "203.0.113.2", + }, + pass: true, + }, + { + name: "Test Case 7", + pods: []corev1.PodIP{ + { + IP: "198.51.100.1", + }, + { + IP: "198.51.100.2", + }, + }, + expectedCMs: []string{ + "198.51.100.1", + "198.51.100.2", + }, + pass: true, + }, + { + name: "Test Case 8", + pods: []corev1.PodIP{ + { + IP: "192.168.100.1", + }, + { + IP: "192.168.100.2", + }, + }, + expectedCMs: []string{ + "192.168.100.1", + "192.168.100.2", + }, + pass: true, + }, + { + name: "Test Case 9", + pods: []corev1.PodIP{ + { + IP: "10.1.0.1", + }, + { + IP: "10.1.0.2", + }, + }, + expectedCMs: []string{ + "10.1.0.1", + "10.1.0.2", + }, + pass: true, + }, + { + name: "Test Case 10", + pods: []corev1.PodIP{ + { + IP: "172.31.0.1", + }, + { + IP: "172.31.0.2", + }, + }, + expectedCMs: []string{ + "172.31.0.1", + "172.31.0.2", + }, + pass: true, + }, + } + + for _, test := range tests { + if got := podIPToArray(test.pods); compareStringArray(got, test.expectedCMs) != test.pass { + t.Errorf("PodIPToArray() = %v, want %v", got, test.expectedCMs) + } + } +} + +func TestNodeIPToArray(t *testing.T) { + tests := []struct { + name string + node corev1.Node + expectedCMs []string + }{ + { + name: "Single Internal IP", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + }, + }, + { + name: "Single External IP", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.1", + }, + }, + }, + }, + expectedCMs: []string{}, + }, + { + name: "Multiple Addresses - Mixed Types", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.1", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + }, + }, + { + name: "No Addresses", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{}, + }, + }, + expectedCMs: []string{}, + }, + { + name: "Internal and External IPs with Duplicates", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.1", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + "192.168.1.1", + }, + }, + { + name: "Multiple Internal IPs", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.2", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + "192.168.1.2", + }, + }, + { + name: "Multiple External IPs", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.1", + }, + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.2", + }, + }, + }, + }, + expectedCMs: []string{}, + }, + { + name: "Internal and External IPs", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.1", + }, + { + Type: corev1.NodeExternalIP, + Address: "203.0.113.2", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + }, + }, + { + name: "Node with Hostname", + node: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeHostName, + Address: "node-1.example.com", + }, + }, + }, + }, + expectedCMs: []string{}, + }, + { + name: "Multiple Addresses Including Hostname", + node: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1", + }, + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeHostName, + Address: "node-1.example.com", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + }, + }, + { + name: "IPv6 Address", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "2001:db8::1", + }, + }, + }, + }, + expectedCMs: []string{ + "2001:db8::1", + }, + }, + { + name: "IPv4 and IPv6 Addresses", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeInternalIP, + Address: "2001:db8::1", + }, + }, + }, + }, + expectedCMs: []string{ + "192.168.1.1", + "2001:db8::1", + }, + }, + { + name: "Node with Empty Address", + node: corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "", + }, + }, + }, + }, + expectedCMs: []string{ + "", + }, + }, + } + + for _, test := range tests { + if got := nodeIPToArray(test.node); !compareStringArray(got, test.expectedCMs) { + t.Errorf("NodeIPToArray() = %v, want %v, name %v", got, test.expectedCMs, test.name) + } + } +} diff --git a/pkg/command/share/remote-command/curl_test.go b/pkg/command/share/remote-command/curl_test.go new file mode 100644 index 00000000..da05299c --- /dev/null +++ b/pkg/command/share/remote-command/curl_test.go @@ -0,0 +1,108 @@ +package command + +import ( + "fmt" + "math/rand" + "net" + "strconv" + "testing" +) + +// Helper function to generate random IPv4 address +func randomIPv4() string { + return fmt.Sprintf("%d.%d.%d.%d", randInt(1, 255), randInt(0, 255), randInt(0, 255), randInt(0, 255)) +} + +// Helper function to generate random IPv6 address +func randomIPv6() string { + ip := net.ParseIP(fmt.Sprintf("2409:8c2f:3800::%x", randInt(1, 10000))) + return ip.String() +} + +// Helper function to generate random port +func randomPort() string { + return strconv.Itoa(randInt(1000, 9999)) +} + +// Generate random number between min and max +func randInt(min, max int) int { + return min + rand.Intn(max-min) +} + +func TestCmdCurl(t *testing.T) { + + var tests []struct { + name string + curlCmd Curl + want struct { + curl string + target string + } + } + + // Create 500 random test cases for IPv4 + for i := 0; i < 500; i++ { + ipv4 := randomIPv4() + port := randomPort() + tests = append(tests, struct { + name string + curlCmd Curl + want struct { + curl string + target string + } + }{ + name: fmt.Sprintf("ipv4_test_%d", i+1), + curlCmd: Curl{ + TargetIP: ipv4, + Port: port, + }, + want: struct { + curl string + target string + }{ + curl: fmt.Sprintf("curl -k http://%s:%s/", ipv4, port), + target: fmt.Sprintf("%s:%s", ipv4, port), + }, + }) + } + + // Create 500 random test cases for IPv6 + for i := 0; i < 500; i++ { + ipv6 := randomIPv6() + port := randomPort() + tests = append(tests, struct { + name string + curlCmd Curl + want struct { + curl string + target string + } + }{ + name: fmt.Sprintf("ipv6_test_%d", i+1), + curlCmd: Curl{ + TargetIP: ipv6, + Port: port, + }, + want: struct { + curl string + target string + }{ + curl: fmt.Sprintf("curl -k http://[%s]:%s/", ipv6, port), + target: fmt.Sprintf("%s:%s", ipv6, port), + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.curlCmd.GetCommandStr() != tt.want.curl { + t.Errorf("%s, %s, %s, %s", tt.name, tt.curlCmd, tt.curlCmd.GetCommandStr(), tt.want) + } + + if tt.curlCmd.GetTargetStr() != tt.want.target { + t.Errorf("%s, %s, %s, %s", tt.name, tt.curlCmd, tt.curlCmd.GetCommandStr(), tt.want) + } + }) + } +} diff --git a/pkg/command/share/remote-command/ncat_test.go b/pkg/command/share/remote-command/ncat_test.go new file mode 100644 index 00000000..172d335c --- /dev/null +++ b/pkg/command/share/remote-command/ncat_test.go @@ -0,0 +1,84 @@ +package command + +import ( + "fmt" + "testing" +) + +func TestCmdNcat(t *testing.T) { + + var tests []struct { + name string + ncatCmd Ncat + want struct { + cmd string + target string + } + } + + // Create 500 random test cases for IPv4 + for i := 0; i < 500; i++ { + ipv4 := randomIPv4() + port := randomPort() + tests = append(tests, struct { + name string + ncatCmd Ncat + want struct { + cmd string + target string + } + }{ + name: fmt.Sprintf("ipv4_test_%d", i+1), + ncatCmd: Ncat{ + TargetIP: []string{ipv4}, + Port: []string{port}, + }, + want: struct { + cmd string + target string + }{ + cmd: fmt.Sprintf("nc -w 1 -z -d -v %s %s 2>&1", ipv4, port), + target: fmt.Sprintf("IPs: %s; Ports: %s", ipv4, port), + }, + }) + } + + // Create 500 random test cases for IPv6 + for i := 0; i < 500; i++ { + ipv6 := randomIPv6() + port := randomPort() + tests = append(tests, struct { + name string + ncatCmd Ncat + want struct { + cmd string + target string + } + }{ + name: fmt.Sprintf("ipv6_test_%d", i+1), + ncatCmd: Ncat{ + TargetIP: []string{ipv6}, + Port: []string{port}, + }, + want: struct { + cmd string + target string + }{ + cmd: fmt.Sprintf("nc -w 1 -z -d -v %s %s 2>&1", ipv6, port), + target: fmt.Sprintf("IPs: %s; Ports: %s", ipv6, port), + }, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.ncatCmd.GetCommandStr() != tt.want.cmd { + t.Errorf("%s, %s, %s, %s", tt.name, tt.ncatCmd, tt.ncatCmd.GetCommandStr(), tt.want) + } + + if tt.ncatCmd.GetTargetStr() != tt.want.target { + t.Errorf("%s, %s, %s, %s", tt.name, tt.ncatCmd, tt.ncatCmd.GetCommandStr(), tt.want) + } + }) + } +} diff --git a/pkg/command/share/remote-command/nslookup_test.go b/pkg/command/share/remote-command/nslookup_test.go new file mode 100644 index 00000000..3a851f02 --- /dev/null +++ b/pkg/command/share/remote-command/nslookup_test.go @@ -0,0 +1,160 @@ +package command + +import ( + "testing" +) + +func TestNslookupGetTargetStr(t *testing.T) { + tests := []struct { + name string + nslookup *Nslookup + expected string + }{ + { + name: "Default target and DNS server", + nslookup: &Nslookup{ + TargetHost: "", + DNSServer: "", + }, + expected: "host: dns:kubernetes.default.svc.cluster.local; dns: coredns", + }, + { + name: "Custom target host", + nslookup: &Nslookup{ + TargetHost: "example.com", + DNSServer: "", + }, + expected: "host: example.com; dns: coredns", + }, + { + name: "Custom DNS server", + nslookup: &Nslookup{ + TargetHost: "", + DNSServer: "8.8.8.8", + }, + expected: "host: dns:kubernetes.default.svc.cluster.local; dns: 8.8.8.8", + }, + { + name: "Custom target host and DNS server", + nslookup: &Nslookup{ + TargetHost: "example.com", + DNSServer: "8.8.8.8", + }, + expected: "host: example.com; dns: 8.8.8.8", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.nslookup.GetTargetStr() + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestNslookupGetCommandStr(t *testing.T) { + tests := []struct { + name string + nslookup *Nslookup + expected string + }{ + { + name: "Default target host and DNS server", + nslookup: &Nslookup{ + TargetHost: "", + DNSServer: "", + }, + expected: "nslookup kubernetes.default.svc.cluster.local ", + }, + { + name: "Custom target host", + nslookup: &Nslookup{ + TargetHost: "example.com", + DNSServer: "", + }, + expected: "nslookup example.com ", + }, + { + name: "Custom DNS server", + nslookup: &Nslookup{ + TargetHost: "", + DNSServer: "8.8.8.8", + }, + expected: "nslookup kubernetes.default.svc.cluster.local 8.8.8.8", + }, + { + name: "Custom target host and DNS server", + nslookup: &Nslookup{ + TargetHost: "example.com", + DNSServer: "8.8.8.8", + }, + expected: "nslookup example.com 8.8.8.8", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.nslookup.GetCommandStr() + if result != tt.expected { + t.Errorf("%s expected %q, got %q", tt.name, tt.expected, result) + } + }) + } +} + +func TestNslookupParseResult(t *testing.T) { + tests := []struct { + name string + nslookup *Nslookup + result string + expected *Result + }{ + { + name: "Successful command", + nslookup: &Nslookup{ + TargetHost: "example.com", + DNSServer: "coredns", + }, + result: "Server: coredns\nAddress: 10.96.0.10\n\nName: example.com\nAddress 1: 93.184.216.34", + expected: &Result{ + Status: CommandSuccessed, + ResultStr: "nslookup example.com coredns Server: coredns\nAddress: 10.96.0.10\n\nName: example.com\nAddress 1: 93.184.216.34", + }, + }, + { + name: "Command failed due to server not found", + nslookup: &Nslookup{ + TargetHost: "example.com", + DNSServer: "coredns", + }, + result: ";; connection timed out; no servers could be reached", + expected: &Result{ + Status: CommandFailed, + ResultStr: "nslookup example.com coredns ;; connection timed out; no servers could be reached", + }, + }, + { + name: "Command failed due to server can't find", + nslookup: &Nslookup{ + TargetHost: "nonexistent.example.com", + DNSServer: "coredns", + }, + result: "Server: coredns\nAddress: 10.96.0.10\n\n** server can't find nonexistent.example.com: NXDOMAIN", + expected: &Result{ + Status: CommandFailed, + ResultStr: "nslookup nonexistent.example.com coredns Server: coredns\nAddress: 10.96.0.10\n\n** server can't find nonexistent.example.com: NXDOMAIN", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.nslookup.ParseResult(tt.result) + if result.Status != tt.expected.Status || result.ResultStr != tt.expected.ResultStr { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/pkg/command/share/remote-command/ping_test.go b/pkg/command/share/remote-command/ping_test.go new file mode 100644 index 00000000..a689dea6 --- /dev/null +++ b/pkg/command/share/remote-command/ping_test.go @@ -0,0 +1,214 @@ +package command + +import ( + "testing" +) + +func TestPingGetTargetStr(t *testing.T) { + tests := []struct { + name string + ping *Ping + expected string + }{ + { + name: "Valid IPv4 target IP", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + expected: "192.168.1.1", + }, + { + name: "Valid IPv6 target IP", + ping: &Ping{ + TargetIP: "2001:db8::ff00:42:8329", + }, + expected: "2001:db8::ff00:42:8329", + }, + { + name: "Empty target IP", + ping: &Ping{ + TargetIP: "", + }, + expected: "", + }, + { + name: "Invalid IP format", + ping: &Ping{ + TargetIP: "invalid_ip", + }, + expected: "invalid_ip", + }, + { + name: "Localhost IP", + ping: &Ping{ + TargetIP: "127.0.0.1", + }, + expected: "127.0.0.1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.ping.GetTargetStr() + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestPingGetCommandStr(t *testing.T) { + tests := []struct { + name string + ping *Ping + expected string + }{ + { + name: "IPv4 target IP", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + expected: "ping -c 1 192.168.1.1", + }, + { + name: "IPv6 target IP", + ping: &Ping{ + TargetIP: "2001:db8::ff00:42:8329", + }, + expected: "ping6 -c 1 2001:db8::ff00:42:8329", + }, + { + name: "Empty target IP", + ping: &Ping{ + TargetIP: "", + }, + expected: "ping -c 1 ", // Invalid command + }, + { + name: "Localhost IP", + ping: &Ping{ + TargetIP: "127.0.0.1", + }, + expected: "ping -c 1 127.0.0.1", + }, + { + name: "Another IPv4 target IP", + ping: &Ping{ + TargetIP: "10.0.0.1", + }, + expected: "ping -c 1 10.0.0.1", + }, + { + name: "Another IPv6 target IP", + ping: &Ping{ + TargetIP: "fe80::1ff:fe23:4567:890a", + }, + expected: "ping6 -c 1 fe80::1ff:fe23:4567:890a", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.ping.GetCommandStr() + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestPingParseResult(t *testing.T) { + tests := []struct { + name string + ping *Ping + result string + expected *Result + }{ + { + name: "Successful ping result", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "PING 192.168.1.1 (192.168.1.1): 1 data byte\n64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=0.123 ms\n\n--- 192.168.1.1 ping statistics ---\n1 packets transmitted, 1 packets received, 0% packet loss", + expected: &Result{ + Status: CommandSuccessed, + ResultStr: "ping -c 1 192.168.1.1 PING 192.168.1.1 (192.168.1.1): 1 data byte\n64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=0.123 ms\n\n--- 192.168.1.1 ping statistics ---\n1 packets transmitted, 1 packets received, 0% packet loss", + }, + }, + { + name: "Failed ping result - no response", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "PING 192.168.1.1 (192.168.1.1): 1 data byte\n\n--- 192.168.1.1 ping statistics ---\n1 packets transmitted, 0 packets received, 100% packet loss", + expected: &Result{ + Status: CommandFailed, + ResultStr: "ping -c 1 192.168.1.1 PING 192.168.1.1 (192.168.1.1): 1 data byte\n\n--- 192.168.1.1 ping statistics ---\n1 packets transmitted, 0 packets received, 100% packet loss", + }, + }, + { + name: "Unexpected result format", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "Some random output that does not match expected format", + expected: &Result{ + Status: CommandFailed, + ResultStr: "ping -c 1 192.168.1.1 Some random output that does not match expected format", + }, + }, + { + name: "Connection timed out", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "ping: connect: Network is unreachable", + expected: &Result{ + Status: CommandFailed, + ResultStr: "ping -c 1 192.168.1.1 ping: connect: Network is unreachable", + }, + }, + { + name: "Server not found", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "ping: 192.168.1.1: Name or service not known", + expected: &Result{ + Status: CommandFailed, + ResultStr: "ping -c 1 192.168.1.1 ping: 192.168.1.1: Name or service not known", + }, + }, + { + name: "Valid response but with loss", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "PING 192.168.1.1 (192.168.1.1): 1 data byte\n64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=0.123 ms\n\n--- 192.168.1.1 ping statistics ---\n1 packets transmitted, 0 packets received, 100% packet loss", + expected: &Result{ + Status: CommandFailed, + ResultStr: "ping -c 1 192.168.1.1 PING 192.168.1.1 (192.168.1.1): 1 data byte\n64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=0.123 ms\n\n--- 192.168.1.1 ping statistics ---\n1 packets transmitted, 0 packets received, 100% packet loss", + }, + }, + { + name: "Successful ping with packet loss", + ping: &Ping{ + TargetIP: "192.168.1.1", + }, + result: "PING 192.168.1.1 (192.168.1.1): 1 data byte\n64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=0.123 ms\n\n--- 192.168.1.1 ping statistics ---\n2 packets transmitted, 1 packets received, 50% packet loss", + expected: &Result{ + Status: CommandFailed, + ResultStr: "ping -c 1 192.168.1.1 PING 192.168.1.1 (192.168.1.1): 1 data byte\n64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=0.123 ms\n\n--- 192.168.1.1 ping statistics ---\n2 packets transmitted, 1 packets received, 50% packet loss", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.ping.ParseResult(tt.result) + if result.Status != tt.expected.Status { + t.Errorf("%s expected %+v, got %+v", tt.name, tt.expected, result) + } + }) + } +} diff --git a/pkg/utils/builder_test.go b/pkg/utils/builder_test.go new file mode 100644 index 00000000..0af5481f --- /dev/null +++ b/pkg/utils/builder_test.go @@ -0,0 +1,295 @@ +package utils + +import ( + "bytes" + "testing" +) + +func TestParseTemplate(t *testing.T) { + tests := []struct { + name string + strTmpl string + obj interface{} + expected []byte + expectedErr string + }{ + { + name: "Simple template with string", + strTmpl: "Hello, {{.Name}}!", + obj: map[string]string{ + "Name": "World", + }, + expected: []byte("Hello, World!"), + expectedErr: "", + }, + { + name: "Template with integer", + strTmpl: "The value is {{.Value}}.", + obj: map[string]int{ + "Value": 42, + }, + expected: []byte("The value is 42."), + expectedErr: "", + }, + { + name: "Template with missing key", + strTmpl: "Hello, {{.UnknownKey}}!", + obj: map[string]string{ + "Name": "World", + }, + expected: []byte("Hello, !"), + expectedErr: "", + }, + { + name: "Invalid template syntax", + strTmpl: "Hello, {{ .Name", + obj: map[string]string{"Name": "World"}, + expected: nil, + expectedErr: "error when parsing template", + }, + { + name: "Template execution failure", + strTmpl: "Hello, {{ . }}", + obj: make(chan int), // Unsupported type + expected: nil, + expectedErr: "error when executing template", + }, + { + name: "Template with multiple keys", + strTmpl: "{{.Greeting}}, {{.Name}}!", + obj: map[string]string{ + "Greeting": "Hello", + "Name": "Alice", + }, + expected: []byte("Hello, Alice!"), + expectedErr: "", + }, + { + name: "Template with newlines", + strTmpl: "Hello,\n{{.Name}}!", + obj: map[string]string{ + "Name": "Bob", + }, + expected: []byte("Hello,\nBob!"), + expectedErr: "", + }, + { + name: "Template with special characters", + strTmpl: "Key: {{.Key}}, Value: {{.Value}}", + obj: map[string]string{ + "Key": "Special&*%$#@!", + "Value": "Value with spaces", + }, + expected: []byte("Key: Special&*%$#@!, Value: Value with spaces"), + expectedErr: "", + }, + { + name: "Template with whitespace", + strTmpl: " Hello {{ .Name }} ", + obj: map[string]string{ + "Name": " World ", + }, + expected: []byte(" Hello World "), + expectedErr: "", + }, + { + name: "Template with float", + strTmpl: "Pi is approximately {{.Pi}}.", + obj: map[string]float64{ + "Pi": 3.14, + }, + expected: []byte("Pi is approximately 3.14."), + expectedErr: "", + }, + { + name: "Template with complex number", + strTmpl: "Complex number: {{.Complex}}.", + obj: map[string]complex128{ + "Complex": complex(1, 2), + }, + expected: []byte("Complex number: (1+2i)."), + expectedErr: "", + }, + { + name: "Template with boolean", + strTmpl: "Is it true? {{.IsTrue}}.", + obj: map[string]bool{ + "IsTrue": true, + }, + expected: []byte("Is it true? true."), + expectedErr: "", + }, + { + name: "Nested struct", + strTmpl: "User: {{.User.Name}}, Age: {{.User.Age}}.", + obj: struct { + User struct { + Name string + Age int + } + }{ + User: struct { + Name string + Age int + }{ + Name: "Charlie", + Age: 30, + }, + }, + expected: []byte("User: Charlie, Age: 30."), + expectedErr: "", + }, + { + name: "Slice in template", + strTmpl: `Items: +{{range .Items}} - {{.}} +{{end}}`, + obj: struct { + Items []string + }{ + Items: []string{"Item1", "Item2", "Item3"}, + }, + expected: []byte("Items:\n - Item1\n - Item2\n - Item3\n"), + expectedErr: "", + }, + { + name: "Map in template", + strTmpl: `Properties: +{{range $key, $value := .Properties}} - {{$key}}: {{$value}} +{{end}}`, + obj: struct { + Properties map[string]string + }{ + Properties: map[string]string{ + "Key1": "Value1", + "Key2": "Value2", + }, + }, + expected: []byte("Properties:\n - Key1: Value1\n - Key2: Value2\n"), + expectedErr: "", + }, + { + name: "Template with function call", + strTmpl: "Current year: {{.CurrentYear}}.", + obj: struct { + CurrentYear int + }{ + CurrentYear: 2024, + }, + expected: []byte("Current year: 2024."), + expectedErr: "", + }, + { + name: "Template with date", + strTmpl: "Today's date: {{.Date}}.", + obj: struct { + Date string + }{ + Date: "2024-10-21", + }, + expected: []byte("Today's date: 2024-10-21."), + expectedErr: "", + }, + { + name: "Nested struct with invalid key", + strTmpl: "User: {{.User.InvalidKey}}", + obj: struct { + User struct { + Name string + } + }{ + User: struct { + Name string + }{ + Name: "Alice", + }, + }, + expected: nil, + expectedErr: "error when executing template", + }, + { + name: "Template with unsupported type", + strTmpl: "Unsupported type: {{.Channel}}", + obj: struct { + Channel chan int + }{ + Channel: make(chan int), + }, + expected: nil, + expectedErr: "error when executing template", + }, + { + name: "Multiple lines with variables", + strTmpl: "First Name: {{.FirstName}}\nLast Name: {{.LastName}}", + obj: map[string]string{ + "FirstName": "John", + "LastName": "Doe", + }, + expected: []byte("First Name: John\nLast Name: Doe"), + expectedErr: "", + }, + { + name: "Chained templates", + strTmpl: "{{.Greeting}}, {{.Name}}! Today is {{.Day}}.", + obj: map[string]interface{}{ + "Greeting": "Hello", + "Name": "Jane", + "Day": "Monday", + }, + expected: []byte("Hello, Jane! Today is Monday."), + expectedErr: "", + }, + { + name: "Complex nested structure", + strTmpl: `Person: + Name: {{.Person.Name}} + Address: {{.Person.Address.Street}}, {{.Person.Address.City}}`, + obj: struct { + Person struct { + Name string + Address struct { + Street string + City string + } + } + }{ + Person: struct { + Name string + Address struct { + Street string + City string + } + }{ + Name: "Emily", + Address: struct { + Street string + City string + }{ + Street: "123 Elm St", + City: "Gotham", + }, + }, + }, + expected: []byte("Person:\n Name: Emily\n Address: 123 Elm St, Gotham"), + expectedErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseTemplate(tt.strTmpl, tt.obj) + if tt.expectedErr != "" { + if err == nil || !bytes.Contains([]byte(err.Error()), []byte(tt.expectedErr)) { + t.Errorf("name %s, expected error containing %q, got %v", tt.name, tt.expectedErr, err) + } + } else { + if err != nil { + t.Errorf("name %s, unexpected error: %v", tt.name, err) + } + if !bytes.Equal(result, tt.expected) { + t.Errorf("name %s, expected %q, got %q", tt.name, tt.expected, result) + } + } + }) + } +} diff --git a/pkg/utils/verify_test.go b/pkg/utils/verify_test.go new file mode 100644 index 00000000..4c6783e2 --- /dev/null +++ b/pkg/utils/verify_test.go @@ -0,0 +1,257 @@ +package utils + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestMapToString tests the MapToString function. +func TestMapToString(t *testing.T) { + tests := []struct { + name string + input map[string]string + output string + }{ + { + name: "Empty map", + input: map[string]string{}, + output: "", + }, + { + name: "Single entry", + input: map[string]string{ + "key1": "value1", + }, + output: "key1=value1", + }, + { + name: "Multiple entries", + input: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + output: "key1=value1,key2=value2", + }, + { + name: "Entries with special characters", + input: map[string]string{ + "key with spaces": "value with spaces", + "key:with:colons": "value:with:colons", + }, + output: "key with spaces=value with spaces,key:with:colons=value:with:colons", + }, + { + name: "Entries with empty values", + input: map[string]string{ + "key1": "", + "key2": "value2", + }, + output: "key1=,key2=value2", + }, + { + name: "Entries with numeric keys", + input: map[string]string{ + "1": "value1", + "2": "value2", + }, + output: "1=value1,2=value2", + }, + { + name: "Long keys and values", + input: map[string]string{ + "this_is_a_very_long_key_name": "this_is_a_very_long_value_name", + }, + output: "this_is_a_very_long_key_name=this_is_a_very_long_value_name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := MapToString(tt.input) + if result != tt.output { + t.Errorf("expected %q, got %q", tt.output, result) + } + }) + } +} + +// TestIsIPv6 tests the IsIPv6 function. +func TestIsIPv6(t *testing.T) { + tests := []struct { + name string + input string + output bool + }{ + { + name: "Valid IPv6", + input: "2409:8c2f:3800::1", + output: true, + }, + { + name: "Invalid IPv4", + input: "192.168.1.1", + output: false, + }, + { + name: "IPv4 CIDR", + input: "192.168.1.0/24", + output: false, + }, + { + name: "Valid IPv6 with CIDR", + input: "2409:8c2f:3800::/64", + output: true, + }, + { + name: "Invalid empty string", + input: "", + output: false, + }, + { + name: "Valid mixed IPv6", + input: "2001:db8::1234:5678:abcd:ef00:1234", + output: true, + }, + { + name: "Invalid IPv6 with spaces", + input: "2409:8c2f: 3800::1", + output: true, + }, + { + name: "Valid IPv6 with full notation", + input: "2001:0db8:0000:0042:0000:8329:8a2e:0370:7334", + output: true, + }, + { + name: "Invalid mixed format", + input: "2001:db8::1234:5678:abcd:ef00:1234.5678", + output: true, + }, + { + name: "Valid short IPv6", + input: "::1", + output: true, + }, + { + name: "Valid minimal IPv6", + input: "0:0:0:0:0:0:0:1", + output: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsIPv6(tt.input) + if result != tt.output { + t.Errorf("expected %v, got %v", tt.output, result) + } + }) + } +} + +func TestPodStatus(t *testing.T) { + tests := []struct { + name string + pod *corev1.Pod + expected string + }{ + { + name: "Pod is running and container is not waiting", + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + expected: "Running", + }, + { + name: "Pod is running and container is waiting", + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "ContainerCreating", + }, + }, + }, + }, + }, + }, + expected: "ContainerCreating", + }, + { + name: "Pod is terminating", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &metav1.Time{}, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + expected: "Running", + }, + { + name: "Pod is running but container has an error", + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Waiting: nil, + }, + }, + }, + }, + }, + expected: "Running", + }, + { + name: "Pod has waiting state in first container", + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Phase: corev1.PodPending, + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "ErrImagePull", + }, + }, + }, + }, + }, + }, + expected: "ErrImagePull", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status := podStatus(tt.pod) + if status != tt.expected { + t.Errorf("name %s, expected %q, got %q", tt.name, tt.expected, status) + } + }) + } +}