mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Tighten v5-to-v6 upgrade safety, release installability, provider MSP mode handling, AI cost accounting, metrics flushing, and frontend guardrails for the v6.0.0 GA candidate.
201 lines
6.6 KiB
Go
201 lines
6.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestRouter_HandleState_MockIsolation(t *testing.T) {
|
|
// Enable mock mode to control GetState output
|
|
setMockModeForTest(t, true)
|
|
|
|
dataPath := t.TempDir()
|
|
hp, _ := auth.HashPassword("password")
|
|
cfg := &config.Config{
|
|
DataPath: dataPath,
|
|
MultiTenantEnabled: true,
|
|
AuthUser: "admin",
|
|
AuthPass: hp,
|
|
}
|
|
|
|
// Initialize persistent stores to avoid permission issues with /etc/pulse
|
|
InitSessionStore(dataPath)
|
|
InitCSRFStore(dataPath)
|
|
|
|
// Create a router with a real monitor
|
|
// Since monitor.ReadSnapshot() checks IsMockEnabled(), it will return mock data.
|
|
monitor, _ := monitoring.New(cfg)
|
|
router := &Router{
|
|
config: cfg,
|
|
monitor: monitor,
|
|
persistence: config.NewConfigPersistence(dataPath),
|
|
}
|
|
|
|
t.Run("basic state access", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/state", nil)
|
|
// Set credentials for CheckAuth (Basic Auth)
|
|
req.SetBasicAuth("admin", "password")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.handleState(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var payload map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &payload)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify v6 contract: legacy per-type arrays are omitted from /api/state.
|
|
for _, key := range []string{"nodes", "vms", "containers", "dockerHosts", "hosts", "storage"} {
|
|
if _, ok := payload[key]; ok {
|
|
t.Fatalf("expected %q to be omitted from /api/state payload", key)
|
|
}
|
|
}
|
|
|
|
var state models.StateFrontend
|
|
err = json.Unmarshal(w.Body.Bytes(), &state)
|
|
assert.NoError(t, err)
|
|
assert.Greater(t, state.LastUpdate, int64(0))
|
|
assert.NotNil(t, state.RecentlyResolved)
|
|
assert.NotNil(t, state.Metrics)
|
|
assert.NotNil(t, state.ConnectionHealth)
|
|
|
|
metricsPayload, ok := payload["metrics"].([]any)
|
|
if !ok {
|
|
t.Fatalf("expected metrics payload to be an array, got %T", payload["metrics"])
|
|
}
|
|
if metricsPayload == nil {
|
|
t.Fatal("expected metrics payload array to be non-nil")
|
|
}
|
|
|
|
if _, ok := payload["performance"].(map[string]any); !ok {
|
|
t.Fatalf("expected performance payload to be an object, got %T", payload["performance"])
|
|
}
|
|
if _, ok := payload["stats"].(map[string]any); !ok {
|
|
t.Fatalf("expected stats payload to be an object, got %T", payload["stats"])
|
|
}
|
|
if _, ok := payload["resources"].([]any); !ok {
|
|
t.Fatalf("expected resources payload to be an array, got %T", payload["resources"])
|
|
}
|
|
if _, ok := payload["connectedInfrastructure"].([]any); !ok {
|
|
t.Fatalf("expected connectedInfrastructure payload to be an array, got %T", payload["connectedInfrastructure"])
|
|
}
|
|
})
|
|
|
|
t.Run("invalid method", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/state", nil)
|
|
w := httptest.NewRecorder()
|
|
router.handleState(w, req)
|
|
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
|
})
|
|
|
|
t.Run("unauthorized", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/state", nil)
|
|
w := httptest.NewRecorder()
|
|
router.handleState(w, req)
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
}
|
|
|
|
func TestRouter_HandleStateSummary(t *testing.T) {
|
|
dataPath := t.TempDir()
|
|
hp, _ := auth.HashPassword("password")
|
|
cfg := &config.Config{
|
|
DataPath: dataPath,
|
|
AuthUser: "admin",
|
|
AuthPass: hp,
|
|
}
|
|
InitSessionStore(dataPath)
|
|
InitCSRFStore(dataPath)
|
|
monitor, state, _ := newTestMonitor(t)
|
|
lastUpdate := time.Date(2026, 5, 24, 10, 11, 12, 0, time.UTC)
|
|
state.Nodes = []models.Node{{ID: "node-1"}, {ID: "node-2"}}
|
|
state.VMs = []models.VM{{ID: "vm-1"}}
|
|
state.Containers = []models.Container{{ID: "ct-1"}, {ID: "ct-2"}, {ID: "ct-3"}}
|
|
state.ActiveAlerts = []models.Alert{{ID: "alert-1"}}
|
|
state.DockerHosts = []models.DockerHost{
|
|
{
|
|
ID: "docker-1",
|
|
Hostname: "docker.local",
|
|
DisplayName: "Docker Host",
|
|
UptimeSeconds: 3600,
|
|
CPUUsage: 12.5,
|
|
Containers: []models.DockerContainer{
|
|
{ID: "container-1"},
|
|
{ID: "container-2"},
|
|
},
|
|
},
|
|
}
|
|
state.LastUpdate = lastUpdate
|
|
syncTestResourceStore(t, monitor, state)
|
|
|
|
router := &Router{config: cfg, monitor: monitor}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/state/summary", nil)
|
|
req.SetBasicAuth("admin", "password")
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleStateSummary(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
|
|
var summary stateSummaryResponse
|
|
err := json.Unmarshal(rec.Body.Bytes(), &summary)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 1, summary.ActiveAlerts)
|
|
assert.Equal(t, 2, summary.Nodes)
|
|
assert.Equal(t, 1, summary.VMs)
|
|
assert.Equal(t, 3, summary.Containers)
|
|
assert.Equal(t, lastUpdate, summary.LastUpdate)
|
|
if assert.Len(t, summary.DockerHosts, 1) {
|
|
assert.Equal(t, "Docker Host", summary.DockerHosts[0].Name)
|
|
assert.Equal(t, 2, summary.DockerHosts[0].Containers)
|
|
assert.Equal(t, int64(3600), summary.DockerHosts[0].UptimeSeconds)
|
|
assert.Equal(t, 12.5, summary.DockerHosts[0].CPUUsagePercent)
|
|
}
|
|
}
|
|
|
|
func TestBuildStateSummaryCountsSnapshotResources(t *testing.T) {
|
|
lastUpdate := time.Date(2026, 5, 24, 10, 11, 12, 0, time.UTC)
|
|
snapshot := models.StateSnapshot{
|
|
ActiveAlerts: []models.Alert{{ID: "alert-1"}},
|
|
Nodes: []models.Node{{ID: "node-1"}, {ID: "node-2"}},
|
|
VMs: []models.VM{{ID: "vm-1"}},
|
|
Containers: []models.Container{{ID: "ct-1"}, {ID: "ct-2"}, {ID: "ct-3"}},
|
|
DockerHosts: []models.DockerHost{{
|
|
ID: "docker-1",
|
|
Hostname: "docker.local",
|
|
DisplayName: "Docker Host",
|
|
CustomDisplayName: "Custom Docker Host",
|
|
UptimeSeconds: 3600,
|
|
CPUUsage: 12.5,
|
|
Containers: []models.DockerContainer{{ID: "container-1"}, {ID: "container-2"}},
|
|
}},
|
|
LastUpdate: lastUpdate,
|
|
}
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
registry.IngestSnapshot(snapshot)
|
|
|
|
summary := buildStateSummary(registry, len(snapshot.ActiveAlerts), snapshot.LastUpdate)
|
|
|
|
assert.Equal(t, 1, summary.ActiveAlerts)
|
|
assert.Equal(t, 2, summary.Nodes)
|
|
assert.Equal(t, 1, summary.VMs)
|
|
assert.Equal(t, 3, summary.Containers)
|
|
assert.Equal(t, lastUpdate, summary.LastUpdate)
|
|
if assert.Len(t, summary.DockerHosts, 1) {
|
|
assert.Equal(t, "Custom Docker Host", summary.DockerHosts[0].Name)
|
|
assert.Equal(t, 2, summary.DockerHosts[0].Containers)
|
|
assert.Equal(t, int64(3600), summary.DockerHosts[0].UptimeSeconds)
|
|
assert.Equal(t, 12.5, summary.DockerHosts[0].CPUUsagePercent)
|
|
}
|
|
}
|