Adds tests.

This commit is contained in:
2025-11-08 20:10:13 +00:00
parent 7cd434aabf
commit b330b2aea7
9 changed files with 5462 additions and 159 deletions

View File

@@ -0,0 +1,750 @@
package tools
import (
"testing"
"time"
)
func TestNewKubectl(t *testing.T) {
tests := []struct {
name string
kubeconfigPath string
}{
{
name: "creates Kubectl with kubeconfig path",
kubeconfigPath: "/path/to/kubeconfig",
},
{
name: "creates Kubectl with empty path",
kubeconfigPath: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k := NewKubectl(tt.kubeconfigPath)
if k == nil {
t.Fatal("NewKubectl() returned nil")
}
if k.kubeconfigPath != tt.kubeconfigPath {
t.Errorf("kubeconfigPath = %q, want %q", k.kubeconfigPath, tt.kubeconfigPath)
}
})
}
}
func TestKubectlDeploymentExists(t *testing.T) {
tests := []struct {
name string
depName string
namespace string
skipTest bool
}{
{
name: "check deployment exists",
depName: "test-deployment",
namespace: "default",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
exists := k.DeploymentExists(tt.depName, tt.namespace)
_ = exists // Result depends on actual cluster state
})
}
}
func TestKubectlGetPods(t *testing.T) {
tests := []struct {
name string
namespace string
detailed bool
skipTest bool
}{
{
name: "get pods basic",
namespace: "default",
detailed: false,
skipTest: true,
},
{
name: "get pods detailed",
namespace: "kube-system",
detailed: true,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
pods, err := k.GetPods(tt.namespace, tt.detailed)
if err == nil {
if pods == nil {
t.Error("GetPods() returned nil slice without error")
}
// Verify pod structure
for i, pod := range pods {
if pod.Name == "" {
t.Errorf("pod[%d].Name is empty", i)
}
if pod.Status == "" {
t.Errorf("pod[%d].Status is empty", i)
}
if pod.Ready == "" {
t.Errorf("pod[%d].Ready is empty", i)
}
if pod.Age == "" {
t.Errorf("pod[%d].Age is empty", i)
}
if tt.detailed && pod.Containers == nil {
t.Errorf("pod[%d].Containers is nil in detailed mode", i)
}
}
}
})
}
}
func TestKubectlGetFirstPodName(t *testing.T) {
tests := []struct {
name string
namespace string
skipTest bool
}{
{
name: "get first pod name",
namespace: "kube-system",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
podName, err := k.GetFirstPodName(tt.namespace)
if err == nil {
if podName == "" {
t.Error("GetFirstPodName() returned empty string without error")
}
}
})
}
}
func TestKubectlGetPodContainers(t *testing.T) {
tests := []struct {
name string
namespace string
podName string
skipTest bool
}{
{
name: "get pod containers",
namespace: "kube-system",
podName: "coredns-123",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
containers, err := k.GetPodContainers(tt.namespace, tt.podName)
if err == nil {
if containers == nil {
t.Error("GetPodContainers() returned nil slice without error")
}
}
})
}
}
func TestKubectlGetDeployment(t *testing.T) {
tests := []struct {
name string
depName string
namespace string
skipTest bool
}{
{
name: "get deployment info",
depName: "test-deployment",
namespace: "default",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
depInfo, err := k.GetDeployment(tt.depName, tt.namespace)
if err == nil {
if depInfo == nil {
t.Error("GetDeployment() returned nil without error")
}
// Desired should be non-negative
if depInfo.Desired < 0 {
t.Errorf("Desired = %d, should be non-negative", depInfo.Desired)
}
}
})
}
}
func TestKubectlGetReplicas(t *testing.T) {
tests := []struct {
name string
namespace string
skipTest bool
}{
{
name: "get replicas for namespace",
namespace: "default",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
replicaInfo, err := k.GetReplicas(tt.namespace)
if err == nil {
if replicaInfo == nil {
t.Error("GetReplicas() returned nil without error")
}
// All values should be non-negative
if replicaInfo.Desired < 0 {
t.Error("Desired < 0")
}
if replicaInfo.Current < 0 {
t.Error("Current < 0")
}
if replicaInfo.Ready < 0 {
t.Error("Ready < 0")
}
if replicaInfo.Available < 0 {
t.Error("Available < 0")
}
}
})
}
}
func TestKubectlGetResources(t *testing.T) {
tests := []struct {
name string
namespace string
skipTest bool
}{
{
name: "get resources for namespace",
namespace: "default",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
usage, err := k.GetResources(tt.namespace)
if err == nil {
if usage == nil {
t.Error("GetResources() returned nil without error")
}
}
})
}
}
func TestKubectlGetRecentEvents(t *testing.T) {
tests := []struct {
name string
namespace string
limit int
skipTest bool
}{
{
name: "get recent events",
namespace: "default",
limit: 10,
skipTest: true,
},
{
name: "get all events with zero limit",
namespace: "default",
limit: 0,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
events, err := k.GetRecentEvents(tt.namespace, tt.limit)
if err == nil {
if events == nil {
t.Error("GetRecentEvents() returned nil slice without error")
}
if tt.limit > 0 && len(events) > tt.limit {
t.Errorf("len(events) = %d, want <= %d", len(events), tt.limit)
}
}
})
}
}
func TestKubectlGetLogs(t *testing.T) {
tests := []struct {
name string
namespace string
podName string
opts LogOptions
skipTest bool
}{
{
name: "get logs with tail",
namespace: "kube-system",
podName: "coredns-123",
opts: LogOptions{Tail: 100},
skipTest: true,
},
{
name: "get logs with container",
namespace: "kube-system",
podName: "coredns-123",
opts: LogOptions{Container: "coredns", Tail: 50},
skipTest: true,
},
{
name: "get previous logs",
namespace: "default",
podName: "test-pod",
opts: LogOptions{Previous: true, Tail: 100},
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
logs, err := k.GetLogs(tt.namespace, tt.podName, tt.opts)
if err == nil {
if logs == nil {
t.Error("GetLogs() returned nil slice without error")
}
}
})
}
}
func TestKubectlStreamLogs(t *testing.T) {
tests := []struct {
name string
namespace string
podName string
opts LogOptions
skipTest bool
}{
{
name: "stream logs",
namespace: "default",
podName: "test-pod",
opts: LogOptions{Tail: 10},
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires kubectl and running cluster")
}
k := NewKubectl("")
cmd, err := k.StreamLogs(tt.namespace, tt.podName, tt.opts)
if err == nil {
if cmd == nil {
t.Error("StreamLogs() returned nil command without error")
}
}
})
}
}
func TestFormatAge(t *testing.T) {
tests := []struct {
name string
duration time.Duration
want string
}{
{
name: "seconds",
duration: 45 * time.Second,
want: "45s",
},
{
name: "minutes",
duration: 5 * time.Minute,
want: "5m",
},
{
name: "hours",
duration: 3 * time.Hour,
want: "3h",
},
{
name: "days",
duration: 48 * time.Hour,
want: "2d",
},
{
name: "less than minute",
duration: 30 * time.Second,
want: "30s",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatAge(tt.duration)
if got != tt.want {
t.Errorf("formatAge(%v) = %q, want %q", tt.duration, got, tt.want)
}
})
}
}
func TestParseResourceQuantity(t *testing.T) {
tests := []struct {
name string
quantity string
want int64
}{
{
name: "millicores",
quantity: "500m",
want: 500,
},
{
name: "cores as plain number",
quantity: "2",
want: 2,
},
{
name: "Ki suffix",
quantity: "100Ki",
want: 100 * 1024,
},
{
name: "Mi suffix",
quantity: "512Mi",
want: 512 * 1024 * 1024,
},
{
name: "Gi suffix",
quantity: "2Gi",
want: 2 * 1024 * 1024 * 1024,
},
{
name: "K suffix",
quantity: "100K",
want: 100 * 1000,
},
{
name: "M suffix",
quantity: "500M",
want: 500 * 1000 * 1000,
},
{
name: "G suffix",
quantity: "1G",
want: 1 * 1000 * 1000 * 1000,
},
{
name: "empty string",
quantity: "",
want: 0,
},
{
name: "whitespace",
quantity: " ",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseResourceQuantity(tt.quantity)
if got != tt.want {
t.Errorf("parseResourceQuantity(%q) = %d, want %d", tt.quantity, got, tt.want)
}
})
}
}
func TestFormatCPU(t *testing.T) {
tests := []struct {
name string
millicores int64
want string
}{
{
name: "zero",
millicores: 0,
want: "0",
},
{
name: "millicores",
millicores: 500,
want: "500m",
},
{
name: "one core",
millicores: 1000,
want: "1.0",
},
{
name: "two and half cores",
millicores: 2500,
want: "2.5",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatCPU(tt.millicores)
if got != tt.want {
t.Errorf("formatCPU(%d) = %q, want %q", tt.millicores, got, tt.want)
}
})
}
}
func TestFormatMemory(t *testing.T) {
tests := []struct {
name string
bytes int64
want string
}{
{
name: "zero",
bytes: 0,
want: "0",
},
{
name: "bytes",
bytes: 512,
want: "512B",
},
{
name: "kibibytes",
bytes: 1024,
want: "1.0Ki",
},
{
name: "mebibytes",
bytes: 1024 * 1024,
want: "1.0Mi",
},
{
name: "gibibytes",
bytes: 2 * 1024 * 1024 * 1024,
want: "2.0Gi",
},
{
name: "tebibytes",
bytes: 1024 * 1024 * 1024 * 1024,
want: "1.0Ti",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatMemory(tt.bytes)
if got != tt.want {
t.Errorf("formatMemory(%d) = %q, want %q", tt.bytes, got, tt.want)
}
})
}
}
func TestPodInfoStruct(t *testing.T) {
t.Run("PodInfo has required fields", func(t *testing.T) {
pod := PodInfo{
Name: "test-pod",
Status: "Running",
Ready: "1/1",
Restarts: 0,
Age: "5m",
Node: "node-1",
IP: "10.0.0.1",
}
if pod.Name != "test-pod" {
t.Errorf("Name = %q, want %q", pod.Name, "test-pod")
}
if pod.Status != "Running" {
t.Errorf("Status = %q, want %q", pod.Status, "Running")
}
if pod.Ready != "1/1" {
t.Errorf("Ready = %q, want %q", pod.Ready, "1/1")
}
if pod.Restarts != 0 {
t.Errorf("Restarts = %d, want %d", pod.Restarts, 0)
}
})
}
func TestContainerInfoStruct(t *testing.T) {
t.Run("ContainerInfo has required fields", func(t *testing.T) {
container := ContainerInfo{
Name: "test-container",
Image: "nginx:latest",
Ready: true,
RestartCount: 0,
State: ContainerState{
Status: "running",
Since: time.Now(),
},
}
if container.Name != "test-container" {
t.Errorf("Name = %q, want %q", container.Name, "test-container")
}
if !container.Ready {
t.Error("Ready should be true")
}
if container.State.Status != "running" {
t.Errorf("State.Status = %q, want %q", container.State.Status, "running")
}
})
}
func TestDeploymentInfoStruct(t *testing.T) {
t.Run("DeploymentInfo has required fields", func(t *testing.T) {
dep := DeploymentInfo{
Desired: 3,
Current: 3,
Ready: 3,
Available: 3,
}
if dep.Desired != 3 {
t.Errorf("Desired = %d, want %d", dep.Desired, 3)
}
if dep.Current != 3 {
t.Errorf("Current = %d, want %d", dep.Current, 3)
}
})
}
func TestResourceMetricStruct(t *testing.T) {
t.Run("ResourceMetric has required fields", func(t *testing.T) {
metric := ResourceMetric{
Used: "1.5",
Requested: "2.0",
Limit: "4.0",
Percentage: 37.5,
}
if metric.Used != "1.5" {
t.Errorf("Used = %q, want %q", metric.Used, "1.5")
}
if metric.Percentage != 37.5 {
t.Errorf("Percentage = %f, want %f", metric.Percentage, 37.5)
}
})
}
func TestLogOptionsStruct(t *testing.T) {
t.Run("LogOptions has all option fields", func(t *testing.T) {
opts := LogOptions{
Container: "nginx",
Tail: 100,
Previous: true,
Since: "5m",
SinceSeconds: 300,
}
if opts.Container != "nginx" {
t.Errorf("Container = %q, want %q", opts.Container, "nginx")
}
if opts.Tail != 100 {
t.Errorf("Tail = %d, want %d", opts.Tail, 100)
}
if !opts.Previous {
t.Error("Previous should be true")
}
})
}
func TestKubernetesEventStruct(t *testing.T) {
t.Run("KubernetesEvent has required fields", func(t *testing.T) {
now := time.Now()
event := KubernetesEvent{
Type: "Warning",
Reason: "BackOff",
Message: "Back-off restarting failed container",
Count: 5,
FirstSeen: now.Add(-5 * time.Minute),
LastSeen: now,
Object: "Pod/test-pod",
}
if event.Type != "Warning" {
t.Errorf("Type = %q, want %q", event.Type, "Warning")
}
if event.Count != 5 {
t.Errorf("Count = %d, want %d", event.Count, 5)
}
})
}

View File

@@ -0,0 +1,558 @@
package tools
import (
"os"
"path/filepath"
"testing"
)
func TestNewTalosctl(t *testing.T) {
t.Run("creates Talosctl instance without config", func(t *testing.T) {
tc := NewTalosctl()
if tc == nil {
t.Fatal("NewTalosctl() returned nil")
}
if tc.talosconfigPath != "" {
t.Error("talosconfigPath should be empty for NewTalosctl()")
}
})
t.Run("creates Talosctl instance with config", func(t *testing.T) {
configPath := "/path/to/talosconfig"
tc := NewTalosconfigWithConfig(configPath)
if tc == nil {
t.Fatal("NewTalosconfigWithConfig() returned nil")
}
if tc.talosconfigPath != configPath {
t.Errorf("talosconfigPath = %q, want %q", tc.talosconfigPath, configPath)
}
})
}
func TestTalosconfigBuildArgs(t *testing.T) {
tests := []struct {
name string
talosconfigPath string
baseArgs []string
wantPrefix []string
}{
{
name: "no talosconfig adds no prefix",
talosconfigPath: "",
baseArgs: []string{"version", "--short"},
wantPrefix: nil,
},
{
name: "with talosconfig adds prefix",
talosconfigPath: "/path/to/talosconfig",
baseArgs: []string{"version", "--short"},
wantPrefix: []string{"--talosconfig", "/path/to/talosconfig"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tc := &Talosctl{talosconfigPath: tt.talosconfigPath}
got := tc.buildArgs(tt.baseArgs)
if tt.wantPrefix == nil {
// Should return baseArgs unchanged
if len(got) != len(tt.baseArgs) {
t.Errorf("buildArgs() length = %d, want %d", len(got), len(tt.baseArgs))
}
for i, arg := range tt.baseArgs {
if i >= len(got) || got[i] != arg {
t.Errorf("buildArgs()[%d] = %q, want %q", i, got[i], arg)
}
}
} else {
// Should have prefix + baseArgs
expectedLen := len(tt.wantPrefix) + len(tt.baseArgs)
if len(got) != expectedLen {
t.Errorf("buildArgs() length = %d, want %d", len(got), expectedLen)
}
// Check prefix
for i, arg := range tt.wantPrefix {
if i >= len(got) || got[i] != arg {
t.Errorf("buildArgs() prefix[%d] = %q, want %q", i, got[i], arg)
}
}
// Check baseArgs follow prefix
for i, arg := range tt.baseArgs {
idx := len(tt.wantPrefix) + i
if idx >= len(got) || got[idx] != arg {
t.Errorf("buildArgs()[%d] = %q, want %q", idx, got[idx], arg)
}
}
}
})
}
}
func TestTalosconfigGenConfig(t *testing.T) {
tests := []struct {
name string
clusterName string
endpoint string
outputDir string
skipTest bool
}{
{
name: "gen config with valid params",
clusterName: "test-cluster",
endpoint: "https://192.168.1.100:6443",
outputDir: "testdata",
skipTest: true, // Skip actual execution
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary")
}
tmpDir := t.TempDir()
tc := NewTalosctl()
err := tc.GenConfig(tt.clusterName, tt.endpoint, tmpDir)
// This will fail without talosctl, but tests the method signature
if err == nil {
// If it somehow succeeds, verify files were created
expectedFiles := []string{
"controlplane.yaml",
"worker.yaml",
"talosconfig",
}
for _, file := range expectedFiles {
path := filepath.Join(tmpDir, file)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("Expected file not created: %s", file)
}
}
}
})
}
}
func TestTalosconfigApplyConfig(t *testing.T) {
tests := []struct {
name string
nodeIP string
configFile string
insecure bool
talosconfigPath string
skipTest bool
}{
{
name: "apply config with all params",
nodeIP: "192.168.1.100",
configFile: "/path/to/config.yaml",
insecure: true,
skipTest: true,
},
{
name: "apply config with talosconfig",
nodeIP: "192.168.1.100",
configFile: "/path/to/config.yaml",
insecure: false,
talosconfigPath: "/path/to/talosconfig",
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary")
}
tc := NewTalosctl()
err := tc.ApplyConfig(tt.nodeIP, tt.configFile, tt.insecure, tt.talosconfigPath)
// Will fail without talosctl, but tests method signature
_ = err
})
}
}
func TestTalosconfigGetDisks(t *testing.T) {
tests := []struct {
name string
nodeIP string
insecure bool
skipTest bool
}{
{
name: "get disks in insecure mode",
nodeIP: "192.168.1.100",
insecure: true,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary and running node")
}
tc := NewTalosctl()
disks, err := tc.GetDisks(tt.nodeIP, tt.insecure)
if err == nil {
// If successful, verify return type
if disks == nil {
t.Error("GetDisks() returned nil slice without error")
}
// Each disk should have path and size
for i, disk := range disks {
if disk.Path == "" {
t.Errorf("disk[%d].Path is empty", i)
}
if disk.Size <= 0 {
t.Errorf("disk[%d].Size = %d, want > 0", i, disk.Size)
}
// Size should be > 10GB per filtering
if disk.Size <= 10000000000 {
t.Errorf("disk[%d].Size = %d, should be filtered (> 10GB)", i, disk.Size)
}
}
}
})
}
}
func TestTalosconfigGetLinks(t *testing.T) {
tests := []struct {
name string
nodeIP string
insecure bool
skipTest bool
}{
{
name: "get links in insecure mode",
nodeIP: "192.168.1.100",
insecure: true,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary and running node")
}
tc := NewTalosctl()
links, err := tc.GetLinks(tt.nodeIP, tt.insecure)
if err == nil {
if links == nil {
t.Error("GetLinks() returned nil slice without error")
}
}
})
}
}
func TestTalosconfigGetRoutes(t *testing.T) {
tests := []struct {
name string
nodeIP string
insecure bool
skipTest bool
}{
{
name: "get routes in insecure mode",
nodeIP: "192.168.1.100",
insecure: true,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary and running node")
}
tc := NewTalosctl()
routes, err := tc.GetRoutes(tt.nodeIP, tt.insecure)
if err == nil {
if routes == nil {
t.Error("GetRoutes() returned nil slice without error")
}
}
})
}
}
func TestTalosconfigGetDefaultInterface(t *testing.T) {
tests := []struct {
name string
nodeIP string
insecure bool
skipTest bool
}{
{
name: "get default interface",
nodeIP: "192.168.1.100",
insecure: true,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary and running node")
}
tc := NewTalosctl()
iface, err := tc.GetDefaultInterface(tt.nodeIP, tt.insecure)
if err == nil {
if iface == "" {
t.Error("GetDefaultInterface() returned empty string without error")
}
}
})
}
}
func TestTalosconfigGetPhysicalInterface(t *testing.T) {
tests := []struct {
name string
nodeIP string
insecure bool
skipTest bool
}{
{
name: "get physical interface",
nodeIP: "192.168.1.100",
insecure: true,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary and running node")
}
tc := NewTalosctl()
iface, err := tc.GetPhysicalInterface(tt.nodeIP, tt.insecure)
if err == nil {
if iface == "" {
t.Error("GetPhysicalInterface() returned empty string without error")
}
// Should not be loopback
if iface == "lo" {
t.Error("GetPhysicalInterface() returned loopback interface")
}
}
})
}
}
func TestTalosconfigGetVersion(t *testing.T) {
tests := []struct {
name string
nodeIP string
insecure bool
want string // Expected for maintenance mode or version string
skipTest bool
}{
{
name: "get version in insecure mode",
nodeIP: "192.168.1.100",
insecure: true,
skipTest: true,
},
{
name: "get version in secure mode",
nodeIP: "192.168.1.100",
insecure: false,
skipTest: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.skipTest {
t.Skip("Skipping test that requires talosctl binary and running node")
}
tc := NewTalosctl()
version, err := tc.GetVersion(tt.nodeIP, tt.insecure)
if err == nil {
if version == "" {
t.Error("GetVersion() returned empty string without error")
}
// Version should be either "maintenance" or start with "v"
if version != "maintenance" && version[0] != 'v' {
t.Errorf("GetVersion() = %q, expected 'maintenance' or version starting with 'v'", version)
}
}
})
}
}
func TestTalosconfigValidate(t *testing.T) {
t.Run("validate checks for talosctl", func(t *testing.T) {
tc := NewTalosctl()
err := tc.Validate()
// This will pass if talosctl is installed, fail otherwise
// We can't guarantee talosctl is installed in all test environments
_ = err
})
}
func TestDiskInfoStruct(t *testing.T) {
t.Run("DiskInfo has required fields", func(t *testing.T) {
disk := DiskInfo{
Path: "/dev/sda",
Size: 1000000000000, // 1TB
}
if disk.Path != "/dev/sda" {
t.Errorf("Path = %q, want %q", disk.Path, "/dev/sda")
}
if disk.Size != 1000000000000 {
t.Errorf("Size = %d, want %d", disk.Size, 1000000000000)
}
})
}
func TestTalosconfigResourceJSONParsing(t *testing.T) {
// This test verifies the logic of getResourceJSON without actually calling talosctl
t.Run("getResourceJSON uses correct command structure", func(t *testing.T) {
tc := &Talosctl{talosconfigPath: "/path/to/talosconfig"}
// We can't easily test the actual command execution without mocking,
// but we can verify buildArgs works correctly
baseArgs := []string{"get", "disks", "--nodes", "192.168.1.100", "-o", "json"}
finalArgs := tc.buildArgs(baseArgs)
// Should have talosconfig prepended
if len(finalArgs) < 2 || finalArgs[0] != "--talosconfig" {
t.Error("buildArgs() should prepend --talosconfig")
}
})
}
func TestTalosconfigInterfaceFiltering(t *testing.T) {
// Test the logic for filtering physical interfaces
tests := []struct {
name string
interfaceName string
linkType string
operState string
shouldAccept bool
}{
{
name: "eth0 up and ethernet",
interfaceName: "eth0",
linkType: "ether",
operState: "up",
shouldAccept: true,
},
{
name: "eno1 up and ethernet",
interfaceName: "eno1",
linkType: "ether",
operState: "up",
shouldAccept: true,
},
{
name: "loopback should be filtered",
interfaceName: "lo",
linkType: "loopback",
operState: "up",
shouldAccept: false,
},
{
name: "cni interface should be filtered",
interfaceName: "cni0",
linkType: "ether",
operState: "up",
shouldAccept: false,
},
{
name: "flannel interface should be filtered",
interfaceName: "flannel.1",
linkType: "ether",
operState: "up",
shouldAccept: false,
},
{
name: "docker interface should be filtered",
interfaceName: "docker0",
linkType: "ether",
operState: "up",
shouldAccept: false,
},
{
name: "bridge interface should be filtered",
interfaceName: "br-1234",
linkType: "ether",
operState: "up",
shouldAccept: false,
},
{
name: "veth interface should be filtered",
interfaceName: "veth123",
linkType: "ether",
operState: "up",
shouldAccept: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// This simulates the filtering logic in GetPhysicalInterface
id := tt.interfaceName
linkType := tt.linkType
operState := tt.operState
shouldAccept := (linkType == "ether" && operState == "up" &&
id != "lo" &&
(id[:3] == "eth" || id[:2] == "en") &&
!containsAny(id, []string{"cni", "flannel", "docker", "br-", "veth"}))
if shouldAccept != tt.shouldAccept {
t.Errorf("Interface %q filtering = %v, want %v", id, shouldAccept, tt.shouldAccept)
}
})
}
}
// Helper function for interface filtering test
func containsAny(s string, substrs []string) bool {
for _, substr := range substrs {
if len(substr) > 0 {
if substr[len(substr)-1] == '-' {
// Prefix match for things like "br-"
if len(s) >= len(substr) && s[:len(substr)] == substr {
return true
}
} else {
// Contains match
if len(s) >= len(substr) {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
}
}
}
}
return false
}

469
internal/tools/yq_test.go Normal file
View File

@@ -0,0 +1,469 @@
package tools
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestNewYQ(t *testing.T) {
t.Run("creates YQ instance with default path", func(t *testing.T) {
yq := NewYQ()
if yq == nil {
t.Fatal("NewYQ() returned nil")
}
if yq.yqPath == "" {
t.Error("yqPath should not be empty")
}
})
}
func TestYQGet(t *testing.T) {
tests := []struct {
name string
setup func(tmpDir string) (string, string)
expression string
want string
wantErr bool
}{
{
name: "get simple value",
setup: func(tmpDir string) (string, string) {
yamlContent := `name: test
version: "1.0"
`
filePath := filepath.Join(tmpDir, "test.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath, ".name"
},
want: "test",
wantErr: false,
},
{
name: "get nested value",
setup: func(tmpDir string) (string, string) {
yamlContent := `person:
name: John
age: 30
`
filePath := filepath.Join(tmpDir, "nested.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath, ".person.name"
},
want: "John",
wantErr: false,
},
{
name: "non-existent file returns error",
setup: func(tmpDir string) (string, string) {
return filepath.Join(tmpDir, "nonexistent.yaml"), ".name"
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip if yq is not available
if _, err := os.Stat("/usr/bin/yq"); os.IsNotExist(err) {
t.Skip("yq not installed, skipping test")
}
tmpDir := t.TempDir()
filePath, expression := tt.setup(tmpDir)
yq := NewYQ()
got, err := yq.Get(filePath, expression)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("Get() = %q, want %q", got, tt.want)
}
})
}
}
func TestYQSet(t *testing.T) {
tests := []struct {
name string
setup func(tmpDir string) string
expression string
value string
verify func(t *testing.T, filePath string)
wantErr bool
}{
{
name: "set simple value",
setup: func(tmpDir string) string {
yamlContent := `name: old`
filePath := filepath.Join(tmpDir, "test.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath
},
expression: ".name",
value: "new",
verify: func(t *testing.T, filePath string) {
content, err := os.ReadFile(filePath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), "new") {
t.Errorf("File does not contain expected value 'new': %s", content)
}
},
wantErr: false,
},
{
name: "set value with special characters",
setup: func(tmpDir string) string {
yamlContent := `message: hello`
filePath := filepath.Join(tmpDir, "special.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath
},
expression: ".message",
value: `hello "world"`,
verify: func(t *testing.T, filePath string) {
content, err := os.ReadFile(filePath)
if err != nil {
t.Fatal(err)
}
// Should contain escaped quotes
if !strings.Contains(string(content), "hello") {
t.Errorf("File does not contain expected value: %s", content)
}
},
wantErr: false,
},
{
name: "expression without leading dot gets dot prepended",
setup: func(tmpDir string) string {
yamlContent := `key: value`
filePath := filepath.Join(tmpDir, "nodot.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath
},
expression: "key",
value: "newvalue",
verify: func(t *testing.T, filePath string) {
content, err := os.ReadFile(filePath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), "newvalue") {
t.Errorf("File does not contain expected value: %s", content)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip if yq is not available
if _, err := os.Stat("/usr/bin/yq"); os.IsNotExist(err) {
t.Skip("yq not installed, skipping test")
}
tmpDir := t.TempDir()
filePath := tt.setup(tmpDir)
yq := NewYQ()
err := yq.Set(filePath, tt.expression, tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tt.verify != nil {
tt.verify(t, filePath)
}
})
}
}
func TestYQDelete(t *testing.T) {
tests := []struct {
name string
setup func(tmpDir string) string
expression string
verify func(t *testing.T, filePath string)
wantErr bool
}{
{
name: "delete simple key",
setup: func(tmpDir string) string {
yamlContent := `name: test
version: "1.0"
`
filePath := filepath.Join(tmpDir, "delete.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath
},
expression: ".name",
verify: func(t *testing.T, filePath string) {
content, err := os.ReadFile(filePath)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(content), "name:") {
t.Errorf("Key 'name' was not deleted: %s", content)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip if yq is not available
if _, err := os.Stat("/usr/bin/yq"); os.IsNotExist(err) {
t.Skip("yq not installed, skipping test")
}
tmpDir := t.TempDir()
filePath := tt.setup(tmpDir)
yq := NewYQ()
err := yq.Delete(filePath, tt.expression)
if (err != nil) != tt.wantErr {
t.Errorf("Delete() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tt.verify != nil {
tt.verify(t, filePath)
}
})
}
}
func TestYQValidate(t *testing.T) {
tests := []struct {
name string
setup func(tmpDir string) string
wantErr bool
}{
{
name: "valid YAML",
setup: func(tmpDir string) string {
yamlContent := `name: test
version: "1.0"
nested:
key: value
`
filePath := filepath.Join(tmpDir, "valid.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath
},
wantErr: false,
},
{
name: "invalid YAML",
setup: func(tmpDir string) string {
invalidYaml := `name: test
invalid indentation
version: "1.0"
`
filePath := filepath.Join(tmpDir, "invalid.yaml")
if err := os.WriteFile(filePath, []byte(invalidYaml), 0644); err != nil {
t.Fatal(err)
}
return filePath
},
wantErr: true,
},
{
name: "non-existent file",
setup: func(tmpDir string) string {
return filepath.Join(tmpDir, "nonexistent.yaml")
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip if yq is not available
if _, err := os.Stat("/usr/bin/yq"); os.IsNotExist(err) {
t.Skip("yq not installed, skipping test")
}
tmpDir := t.TempDir()
filePath := tt.setup(tmpDir)
yq := NewYQ()
err := yq.Validate(filePath)
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestYQExec(t *testing.T) {
tests := []struct {
name string
setup func(tmpDir string) (string, []string)
wantErr bool
}{
{
name: "exec with valid args",
setup: func(tmpDir string) (string, []string) {
yamlContent := `name: test`
filePath := filepath.Join(tmpDir, "exec.yaml")
if err := os.WriteFile(filePath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
return filePath, []string{"eval", ".name", filePath}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip if yq is not available
if _, err := os.Stat("/usr/bin/yq"); os.IsNotExist(err) {
t.Skip("yq not installed, skipping test")
}
tmpDir := t.TempDir()
_, args := tt.setup(tmpDir)
yq := NewYQ()
output, err := yq.Exec(args...)
if (err != nil) != tt.wantErr {
t.Errorf("Exec() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && len(output) == 0 {
t.Error("Exec() returned empty output")
}
})
}
}
func TestCleanYQOutput(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "removes trailing newline",
input: "value\n",
want: "value",
},
{
name: "converts null to empty string",
input: "null",
want: "",
},
{
name: "removes whitespace",
input: " value \n",
want: "value",
},
{
name: "handles empty string",
input: "",
want: "",
},
{
name: "handles multiple newlines",
input: "value\n\n",
want: "value",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CleanYQOutput(tt.input)
if got != tt.want {
t.Errorf("CleanYQOutput(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestYQMerge(t *testing.T) {
tests := []struct {
name string
setup func(tmpDir string) (string, string, string)
verify func(t *testing.T, outputPath string)
wantErr bool
}{
{
name: "merge two files",
setup: func(tmpDir string) (string, string, string) {
file1 := filepath.Join(tmpDir, "file1.yaml")
file2 := filepath.Join(tmpDir, "file2.yaml")
output := filepath.Join(tmpDir, "output.yaml")
if err := os.WriteFile(file1, []byte("key1: value1\n"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(file2, []byte("key2: value2\n"), 0644); err != nil {
t.Fatal(err)
}
return file1, file2, output
},
verify: func(t *testing.T, outputPath string) {
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
t.Error("Output file was not created")
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip if yq is not available
if _, err := os.Stat("/usr/bin/yq"); os.IsNotExist(err) {
t.Skip("yq not installed, skipping test")
}
tmpDir := t.TempDir()
file1, file2, output := tt.setup(tmpDir)
yq := NewYQ()
err := yq.Merge(file1, file2, output)
if (err != nil) != tt.wantErr {
t.Errorf("Merge() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && tt.verify != nil {
tt.verify(t, output)
}
})
}
}