mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
415 lines
12 KiB
Go
415 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
)
|
|
|
|
func TestHandleGetInfraUpdates(t *testing.T) {
|
|
// We can't easily create a real Monitor, so we'll test the core logic
|
|
t.Run("collectDockerUpdates filters correctly", func(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{}
|
|
// Can't test directly without a monitor, but we can verify the behavior
|
|
// through the HTTP handlers
|
|
|
|
// For now, verify the response structure when no updates
|
|
req := httptest.NewRequest(http.MethodGet, "/api/infra-updates", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdates(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", rr.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
// Verify structure
|
|
if _, ok := response["updates"]; !ok {
|
|
t.Error("Expected 'updates' field in response")
|
|
}
|
|
if _, ok := response["total"]; !ok {
|
|
t.Error("Expected 'total' field in response")
|
|
}
|
|
})
|
|
|
|
t.Run("method not allowed for POST", func(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/infra-updates", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdates(rr, req)
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("Expected status 405, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleGetInfraUpdatesSummary(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{}
|
|
|
|
t.Run("returns empty summary without monitor", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/infra-updates/summary", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdatesSummary(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", rr.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if response["totalUpdates"].(float64) != 0 {
|
|
t.Error("Expected totalUpdates to be 0")
|
|
}
|
|
})
|
|
|
|
t.Run("method not allowed for POST", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/infra-updates/summary", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdatesSummary(rr, req)
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("Expected status 405, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleGetInfraUpdatesForHost(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{}
|
|
|
|
t.Run("returns empty for non-existent host", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/infra-updates/host/nonexistent", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdatesForHost(rr, req, "nonexistent")
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200, got %d", rr.Code)
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if response["hostId"] != "nonexistent" {
|
|
t.Error("Expected hostId in response")
|
|
}
|
|
if response["total"].(float64) != 0 {
|
|
t.Error("Expected total to be 0")
|
|
}
|
|
})
|
|
|
|
t.Run("method not allowed for POST", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/infra-updates/host/test", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdatesForHost(rr, req, "test")
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("Expected status 405, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleGetInfraUpdateForResource(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{}
|
|
|
|
t.Run("returns 404 for non-existent resource", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/infra-updates/resource-123", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdateForResource(rr, req, "resource-123")
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Errorf("Expected status 404, got %d", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("method not allowed for POST", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/infra-updates/resource-123", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleGetInfraUpdateForResource(rr, req, "resource-123")
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("Expected status 405, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHandleTriggerInfraUpdateCheck(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{}
|
|
|
|
t.Run("returns 503 without monitor", func(t *testing.T) {
|
|
body := strings.NewReader(`{"hostId":"test-host"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/infra-updates/check", body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleTriggerInfraUpdateCheck(rr, req)
|
|
|
|
if rr.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("Expected status 503, got %d", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("method not allowed for GET", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/infra-updates/check", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.HandleTriggerInfraUpdateCheck(rr, req)
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("Expected status 405, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestContainerUpdateInfo_JSONSerialization(t *testing.T) {
|
|
info := ContainerUpdateInfo{
|
|
HostID: "host-1",
|
|
HostName: "Test Host",
|
|
ContainerID: "container-abc",
|
|
ContainerName: "nginx",
|
|
Image: "nginx:latest",
|
|
CurrentDigest: "sha256:current",
|
|
LatestDigest: "sha256:latest",
|
|
UpdateAvailable: true,
|
|
LastChecked: time.Now().Unix(),
|
|
ResourceType: "docker",
|
|
}
|
|
|
|
data, err := json.Marshal(info)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal ContainerUpdateInfo: %v", err)
|
|
}
|
|
|
|
var decoded ContainerUpdateInfo
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("Failed to unmarshal ContainerUpdateInfo: %v", err)
|
|
}
|
|
|
|
if decoded.HostID != info.HostID {
|
|
t.Errorf("Expected HostID %q, got %q", info.HostID, decoded.HostID)
|
|
}
|
|
if decoded.UpdateAvailable != info.UpdateAvailable {
|
|
t.Errorf("Expected UpdateAvailable %v, got %v", info.UpdateAvailable, decoded.UpdateAvailable)
|
|
}
|
|
if decoded.ResourceType != "docker" {
|
|
t.Errorf("Expected ResourceType 'docker', got %q", decoded.ResourceType)
|
|
}
|
|
}
|
|
|
|
func TestUpdateDetectionHandlers_WithMonitorState(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
state := models.NewState()
|
|
state.DockerHosts = []models.DockerHost{
|
|
{
|
|
ID: "host-1",
|
|
DisplayName: "Host One",
|
|
Containers: []models.DockerContainer{
|
|
{
|
|
ID: "c1",
|
|
Name: "/web",
|
|
Image: "nginx:latest",
|
|
UpdateStatus: &models.DockerContainerUpdateStatus{
|
|
UpdateAvailable: true,
|
|
CurrentDigest: "sha256:old",
|
|
LatestDigest: "sha256:new",
|
|
LastChecked: now,
|
|
},
|
|
},
|
|
{
|
|
ID: "c2",
|
|
Name: "/ok",
|
|
Image: "redis:latest",
|
|
UpdateStatus: &models.DockerContainerUpdateStatus{
|
|
UpdateAvailable: false,
|
|
},
|
|
},
|
|
{
|
|
ID: "c3",
|
|
Name: "/err",
|
|
Image: "mysql:latest",
|
|
UpdateStatus: &models.DockerContainerUpdateStatus{
|
|
UpdateAvailable: false,
|
|
Error: "rate limited",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "host-2",
|
|
DisplayName: "Host Two",
|
|
Containers: []models.DockerContainer{
|
|
{
|
|
ID: "c2b",
|
|
Name: "/api",
|
|
Image: "postgres:latest",
|
|
UpdateStatus: &models.DockerContainerUpdateStatus{
|
|
UpdateAvailable: true,
|
|
CurrentDigest: "sha256:old2",
|
|
LatestDigest: "sha256:new2",
|
|
LastChecked: now,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
monitor := &monitoring.Monitor{}
|
|
setUnexportedField(t, monitor, "state", state)
|
|
handler := NewUpdateDetectionHandlers(monitor)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/infra-updates?hostId=host-1", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.HandleGetInfraUpdates(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
|
|
var updatesResp struct {
|
|
Updates []ContainerUpdateInfo `json:"updates"`
|
|
Total int `json:"total"`
|
|
}
|
|
if err := json.NewDecoder(rr.Body).Decode(&updatesResp); err != nil {
|
|
t.Fatalf("decode updates response: %v", err)
|
|
}
|
|
if updatesResp.Total != 2 {
|
|
t.Fatalf("expected 2 updates, got %d", updatesResp.Total)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/api/infra-updates/summary", nil)
|
|
rr = httptest.NewRecorder()
|
|
handler.HandleGetInfraUpdatesSummary(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
var summaryResp struct {
|
|
Summaries map[string]map[string]any `json:"summaries"`
|
|
TotalUpdates int `json:"totalUpdates"`
|
|
}
|
|
if err := json.NewDecoder(rr.Body).Decode(&summaryResp); err != nil {
|
|
t.Fatalf("decode summary response: %v", err)
|
|
}
|
|
if summaryResp.TotalUpdates != 3 {
|
|
t.Fatalf("expected 3 total updates, got %d", summaryResp.TotalUpdates)
|
|
}
|
|
if _, ok := summaryResp.Summaries["host-1"]; !ok {
|
|
t.Fatalf("expected summary for host-1")
|
|
}
|
|
if _, ok := summaryResp.Summaries["host-2"]; !ok {
|
|
t.Fatalf("expected summary for host-2")
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/api/infra-updates/docker:host-1/c1", nil)
|
|
rr = httptest.NewRecorder()
|
|
handler.HandleGetInfraUpdateForResource(rr, req, "docker:host-1/c1")
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
|
|
var update ContainerUpdateInfo
|
|
if err := json.NewDecoder(rr.Body).Decode(&update); err != nil {
|
|
t.Fatalf("decode update response: %v", err)
|
|
}
|
|
if update.ContainerID != "c1" {
|
|
t.Fatalf("expected container c1, got %q", update.ContainerID)
|
|
}
|
|
if update.ContainerName != "web" {
|
|
t.Fatalf("expected container name stripped, got %q", update.ContainerName)
|
|
}
|
|
|
|
body := strings.NewReader(`{"hostId":"host-1"}`)
|
|
req = httptest.NewRequest(http.MethodPost, "/api/infra-updates/check", body)
|
|
rr = httptest.NewRecorder()
|
|
handler.HandleTriggerInfraUpdateCheck(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
|
|
body = strings.NewReader(`{"resourceId":"docker:host-2/c2b"}`)
|
|
req = httptest.NewRequest(http.MethodPost, "/api/infra-updates/check", body)
|
|
rr = httptest.NewRecorder()
|
|
handler.HandleTriggerInfraUpdateCheck(rr, req)
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code)
|
|
}
|
|
}
|
|
|
|
// Integration test with real monitor (requires more setup)
|
|
func TestUpdateDetectionHandlersWithMonitor(t *testing.T) {
|
|
// Skip in short mode - this requires more infrastructure
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
// This would require setting up a real Monitor instance
|
|
// For now, we test the handler methods in isolation above
|
|
t.Log("Full integration test requires Monitor setup - see manual testing instructions")
|
|
}
|
|
|
|
// Verify handler doesn't panic with nil monitor
|
|
func TestHandlerNilMonitorSafety(t *testing.T) {
|
|
handler := &UpdateDetectionHandlers{monitor: nil}
|
|
|
|
tests := []struct {
|
|
name string
|
|
handler func(w http.ResponseWriter, r *http.Request)
|
|
method string
|
|
path string
|
|
}{
|
|
{
|
|
name: "GetInfraUpdates",
|
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
handler.HandleGetInfraUpdates(w, r)
|
|
},
|
|
method: http.MethodGet,
|
|
path: "/api/infra-updates",
|
|
},
|
|
{
|
|
name: "GetInfraUpdatesSummary",
|
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
handler.HandleGetInfraUpdatesSummary(w, r)
|
|
},
|
|
method: http.MethodGet,
|
|
path: "/api/infra-updates/summary",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tt.method, tt.path, nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
// Should not panic
|
|
tt.handler(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected OK status, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Placeholder for monitor interface - actual implementation is in monitoring package
|
|
var _ = (*monitoring.Monitor)(nil) // Ensure we reference the package
|