diff --git a/pkg/agents/docker/report_test.go b/pkg/agents/docker/report_test.go new file mode 100644 index 0000000..d7c6545 --- /dev/null +++ b/pkg/agents/docker/report_test.go @@ -0,0 +1,485 @@ +package dockeragent + +import ( + "encoding/json" + "testing" + "time" +) + +func TestReport_AgentKey(t *testing.T) { + tests := []struct { + name string + report Report + expected string + }{ + { + name: "uses agent ID first", + report: Report{ + Agent: AgentInfo{ID: "agent-123"}, + Host: HostInfo{MachineID: "machine-456", Hostname: "myhost"}, + }, + expected: "agent-123", + }, + { + name: "falls back to machine ID", + report: Report{ + Agent: AgentInfo{ID: ""}, + Host: HostInfo{MachineID: "machine-456", Hostname: "myhost"}, + }, + expected: "machine-456", + }, + { + name: "falls back to hostname", + report: Report{ + Agent: AgentInfo{ID: ""}, + Host: HostInfo{MachineID: "", Hostname: "myhost"}, + }, + expected: "myhost", + }, + { + name: "empty report returns empty hostname", + report: Report{ + Agent: AgentInfo{}, + Host: HostInfo{}, + }, + expected: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.report.AgentKey() + if result != tc.expected { + t.Errorf("AgentKey() = %q, want %q", result, tc.expected) + } + }) + } +} + +func TestReport_JSONMarshal(t *testing.T) { + now := time.Now() + report := Report{ + Agent: AgentInfo{ + ID: "docker-agent-1", + Version: "1.0.0", + Type: "docker", + IntervalSeconds: 30, + }, + Host: HostInfo{ + Hostname: "docker-host", + DockerVersion: "24.0.0", + TotalCPU: 8, + TotalMemoryBytes: 16000000000, + }, + Containers: []Container{ + { + ID: "abc123", + Name: "nginx", + Image: "nginx:latest", + State: "running", + }, + }, + Timestamp: now, + } + + data, err := json.Marshal(report) + if err != nil { + t.Fatalf("Failed to marshal Report: %v", err) + } + + var decoded Report + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("Failed to unmarshal Report: %v", err) + } + + if decoded.Agent.ID != report.Agent.ID { + t.Errorf("Agent.ID = %q, want %q", decoded.Agent.ID, report.Agent.ID) + } + if len(decoded.Containers) != 1 { + t.Errorf("Containers count = %d, want 1", len(decoded.Containers)) + } +} + +func TestAgentInfo_Fields(t *testing.T) { + agent := AgentInfo{ + ID: "agent-456", + Version: "2.0.0", + Type: "unified", + IntervalSeconds: 60, + } + + if agent.ID != "agent-456" { + t.Errorf("ID = %q, want agent-456", agent.ID) + } + if agent.Version != "2.0.0" { + t.Errorf("Version = %q, want 2.0.0", agent.Version) + } + if agent.Type != "unified" { + t.Errorf("Type = %q, want unified", agent.Type) + } + if agent.IntervalSeconds != 60 { + t.Errorf("IntervalSeconds = %d, want 60", agent.IntervalSeconds) + } +} + +func TestHostInfo_Fields(t *testing.T) { + host := HostInfo{ + Hostname: "docker-server", + Name: "Docker Server", + MachineID: "machine123", + OS: "linux", + Runtime: "docker", + RuntimeVersion: "24.0.0", + KernelVersion: "5.15.0", + Architecture: "x86_64", + DockerVersion: "24.0.0", + TotalCPU: 16, + TotalMemoryBytes: 32000000000, + UptimeSeconds: 86400, + CPUUsagePercent: 25.5, + LoadAverage: []float64{1.0, 0.8, 0.6}, + } + + if host.Hostname != "docker-server" { + t.Errorf("Hostname = %q, want docker-server", host.Hostname) + } + if host.Runtime != "docker" { + t.Errorf("Runtime = %q, want docker", host.Runtime) + } + if host.TotalCPU != 16 { + t.Errorf("TotalCPU = %d, want 16", host.TotalCPU) + } + if len(host.LoadAverage) != 3 { + t.Errorf("LoadAverage length = %d, want 3", len(host.LoadAverage)) + } +} + +func TestContainer_Fields(t *testing.T) { + now := time.Now() + container := Container{ + ID: "container123", + Name: "web-app", + Image: "myapp:v1.2.3", + CreatedAt: now, + State: "running", + Status: "Up 2 hours", + Health: "healthy", + CPUPercent: 15.5, + MemoryUsageBytes: 512000000, + MemoryLimitBytes: 1024000000, + MemoryPercent: 50.0, + UptimeSeconds: 7200, + RestartCount: 0, + ExitCode: 0, + } + + if container.ID != "container123" { + t.Errorf("ID = %q, want container123", container.ID) + } + if container.State != "running" { + t.Errorf("State = %q, want running", container.State) + } + if container.Health != "healthy" { + t.Errorf("Health = %q, want healthy", container.Health) + } + if container.CPUPercent != 15.5 { + t.Errorf("CPUPercent = %f, want 15.5", container.CPUPercent) + } + if container.MemoryPercent != 50.0 { + t.Errorf("MemoryPercent = %f, want 50.0", container.MemoryPercent) + } +} + +func TestContainerPort_Fields(t *testing.T) { + port := ContainerPort{ + PrivatePort: 80, + PublicPort: 8080, + Protocol: "tcp", + IP: "0.0.0.0", + } + + if port.PrivatePort != 80 { + t.Errorf("PrivatePort = %d, want 80", port.PrivatePort) + } + if port.PublicPort != 8080 { + t.Errorf("PublicPort = %d, want 8080", port.PublicPort) + } + if port.Protocol != "tcp" { + t.Errorf("Protocol = %q, want tcp", port.Protocol) + } +} + +func TestContainerNetwork_Fields(t *testing.T) { + network := ContainerNetwork{ + Name: "bridge", + IPv4: "172.17.0.2", + IPv6: "fe80::1", + } + + if network.Name != "bridge" { + t.Errorf("Name = %q, want bridge", network.Name) + } + if network.IPv4 != "172.17.0.2" { + t.Errorf("IPv4 = %q, want 172.17.0.2", network.IPv4) + } +} + +func TestContainerBlockIO_Fields(t *testing.T) { + blockIO := ContainerBlockIO{ + ReadBytes: 1000000, + WriteBytes: 500000, + } + + if blockIO.ReadBytes != 1000000 { + t.Errorf("ReadBytes = %d, want 1000000", blockIO.ReadBytes) + } + if blockIO.WriteBytes != 500000 { + t.Errorf("WriteBytes = %d, want 500000", blockIO.WriteBytes) + } +} + +func TestContainerMount_Fields(t *testing.T) { + mount := ContainerMount{ + Type: "bind", + Source: "/host/data", + Destination: "/data", + Mode: "rw", + RW: true, + Propagation: "rprivate", + } + + if mount.Type != "bind" { + t.Errorf("Type = %q, want bind", mount.Type) + } + if mount.Source != "/host/data" { + t.Errorf("Source = %q, want /host/data", mount.Source) + } + if !mount.RW { + t.Error("RW should be true") + } +} + +func TestPodmanContainer_Fields(t *testing.T) { + podman := PodmanContainer{ + PodName: "mypod", + PodID: "pod123", + Infra: false, + ComposeProject: "myproject", + ComposeService: "web", + AutoUpdatePolicy: "registry", + AutoUpdateRestart: "always", + } + + if podman.PodName != "mypod" { + t.Errorf("PodName = %q, want mypod", podman.PodName) + } + if podman.ComposeProject != "myproject" { + t.Errorf("ComposeProject = %q, want myproject", podman.ComposeProject) + } + if podman.AutoUpdatePolicy != "registry" { + t.Errorf("AutoUpdatePolicy = %q, want registry", podman.AutoUpdatePolicy) + } +} + +func TestSwarmInfo_Fields(t *testing.T) { + swarm := SwarmInfo{ + NodeID: "node123", + NodeRole: "manager", + LocalState: "active", + ControlAvailable: true, + ClusterID: "cluster456", + ClusterName: "my-swarm", + Scope: "swarm", + } + + if swarm.NodeID != "node123" { + t.Errorf("NodeID = %q, want node123", swarm.NodeID) + } + if swarm.NodeRole != "manager" { + t.Errorf("NodeRole = %q, want manager", swarm.NodeRole) + } + if !swarm.ControlAvailable { + t.Error("ControlAvailable should be true") + } +} + +func TestService_Fields(t *testing.T) { + service := Service{ + ID: "service123", + Name: "web", + Stack: "mystack", + Image: "nginx:latest", + Mode: "replicated", + DesiredTasks: 3, + RunningTasks: 3, + CompletedTasks: 0, + } + + if service.ID != "service123" { + t.Errorf("ID = %q, want service123", service.ID) + } + if service.Mode != "replicated" { + t.Errorf("Mode = %q, want replicated", service.Mode) + } + if service.DesiredTasks != 3 { + t.Errorf("DesiredTasks = %d, want 3", service.DesiredTasks) + } +} + +func TestServicePort_Fields(t *testing.T) { + port := ServicePort{ + Name: "http", + Protocol: "tcp", + TargetPort: 80, + PublishedPort: 8080, + PublishMode: "ingress", + } + + if port.Name != "http" { + t.Errorf("Name = %q, want http", port.Name) + } + if port.TargetPort != 80 { + t.Errorf("TargetPort = %d, want 80", port.TargetPort) + } + if port.PublishMode != "ingress" { + t.Errorf("PublishMode = %q, want ingress", port.PublishMode) + } +} + +func TestServiceUpdate_Fields(t *testing.T) { + now := time.Now() + update := ServiceUpdate{ + State: "completed", + Message: "update completed", + CompletedAt: &now, + } + + if update.State != "completed" { + t.Errorf("State = %q, want completed", update.State) + } + if update.CompletedAt == nil { + t.Error("CompletedAt should not be nil") + } +} + +func TestTask_Fields(t *testing.T) { + now := time.Now() + task := Task{ + ID: "task123", + ServiceID: "service456", + ServiceName: "web", + Slot: 1, + NodeID: "node789", + NodeName: "docker-node-1", + DesiredState: "running", + CurrentState: "running", + ContainerID: "container000", + CreatedAt: now, + } + + if task.ID != "task123" { + t.Errorf("ID = %q, want task123", task.ID) + } + if task.Slot != 1 { + t.Errorf("Slot = %d, want 1", task.Slot) + } + if task.DesiredState != "running" { + t.Errorf("DesiredState = %q, want running", task.DesiredState) + } +} + +func TestCommand_Fields(t *testing.T) { + cmd := Command{ + ID: "cmd-123", + Type: CommandTypeStop, + Payload: map[string]any{ + "reason": "shutdown", + }, + } + + if cmd.ID != "cmd-123" { + t.Errorf("ID = %q, want cmd-123", cmd.ID) + } + if cmd.Type != CommandTypeStop { + t.Errorf("Type = %q, want %q", cmd.Type, CommandTypeStop) + } + if cmd.Payload["reason"] != "shutdown" { + t.Errorf("Payload[reason] = %v, want shutdown", cmd.Payload["reason"]) + } +} + +func TestReportResponse_Fields(t *testing.T) { + resp := ReportResponse{ + Success: true, + Commands: []Command{ + {ID: "cmd-1", Type: CommandTypeStop}, + }, + } + + if !resp.Success { + t.Error("Success should be true") + } + if len(resp.Commands) != 1 { + t.Errorf("Commands count = %d, want 1", len(resp.Commands)) + } +} + +func TestCommandAck_Fields(t *testing.T) { + ack := CommandAck{ + HostID: "host-123", + Status: CommandStatusCompleted, + Message: "Command executed successfully", + } + + if ack.HostID != "host-123" { + t.Errorf("HostID = %q, want host-123", ack.HostID) + } + if ack.Status != CommandStatusCompleted { + t.Errorf("Status = %q, want %q", ack.Status, CommandStatusCompleted) + } +} + +func TestCommandConstants(t *testing.T) { + // Verify constants are defined + if CommandTypeStop != "stop" { + t.Errorf("CommandTypeStop = %q, want stop", CommandTypeStop) + } + if CommandStatusAcknowledged != "acknowledged" { + t.Errorf("CommandStatusAcknowledged = %q, want acknowledged", CommandStatusAcknowledged) + } + if CommandStatusCompleted != "completed" { + t.Errorf("CommandStatusCompleted = %q, want completed", CommandStatusCompleted) + } + if CommandStatusFailed != "failed" { + t.Errorf("CommandStatusFailed = %q, want failed", CommandStatusFailed) + } +} + +func TestReport_WithSwarm(t *testing.T) { + report := Report{ + Host: HostInfo{ + Hostname: "swarm-manager", + Swarm: &SwarmInfo{ + NodeID: "node1", + NodeRole: "manager", + ControlAvailable: true, + }, + }, + Services: []Service{ + {ID: "svc1", Name: "web", DesiredTasks: 3, RunningTasks: 3}, + }, + Tasks: []Task{ + {ID: "task1", ServiceName: "web", CurrentState: "running"}, + }, + } + + if report.Host.Swarm == nil { + t.Error("Swarm should not be nil") + } + if len(report.Services) != 1 { + t.Errorf("Services count = %d, want 1", len(report.Services)) + } + if len(report.Tasks) != 1 { + t.Errorf("Tasks count = %d, want 1", len(report.Tasks)) + } +}