mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
118 lines
4.3 KiB
Go
118 lines
4.3 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"
|
|
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
authpkg "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
)
|
|
|
|
type stateSupplementalRecordsProvider struct {
|
|
records []unified.IngestRecord
|
|
}
|
|
|
|
func (p stateSupplementalRecordsProvider) GetCurrentRecords() []unified.IngestRecord {
|
|
return append([]unified.IngestRecord(nil), p.records...)
|
|
}
|
|
|
|
func (p stateSupplementalRecordsProvider) SupplementalRecords(*monitoring.Monitor, string) []unified.IngestRecord {
|
|
return p.GetCurrentRecords()
|
|
}
|
|
|
|
func TestStateEndpointDerivesProxmoxWorkloadParentFromSupplementalRecords(t *testing.T) {
|
|
now := time.Date(2026, 5, 14, 10, 0, 0, 0, time.UTC)
|
|
hashedPassword, err := authpkg.HashPassword("password")
|
|
if err != nil {
|
|
t.Fatalf("hash password: %v", err)
|
|
}
|
|
dataPath := t.TempDir()
|
|
InitSessionStore(dataPath)
|
|
InitCSRFStore(dataPath)
|
|
cfg := &config.Config{DataPath: dataPath, AuthUser: "admin", AuthPass: hashedPassword}
|
|
monitor, err := monitoring.New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("new monitor: %v", err)
|
|
}
|
|
t.Cleanup(func() { monitor.Stop() })
|
|
|
|
monitor.SetResourceStore(unified.NewMonitorAdapter(nil))
|
|
monitor.SetSupplementalRecordsProvider(unified.SourceProxmox, stateSupplementalRecordsProvider{records: []unified.IngestRecord{
|
|
{
|
|
SourceID: "homelab-delly",
|
|
Resource: unified.Resource{
|
|
Type: unified.ResourceTypeAgent, Name: "delly", Status: unified.StatusOnline, LastSeen: now,
|
|
Proxmox: &unified.ProxmoxData{SourceID: "homelab-delly", NodeName: "delly", ClusterName: "homelab", Instance: "delly"},
|
|
},
|
|
Identity: unified.ResourceIdentity{MachineID: "machine-delly", Hostnames: []string{"delly"}},
|
|
},
|
|
{
|
|
SourceID: "delly:delly:104",
|
|
Resource: unified.Resource{
|
|
Type: unified.ResourceTypeSystemContainer, Name: "cloudflared", Status: unified.StatusOnline, LastSeen: now,
|
|
Proxmox: &unified.ProxmoxData{SourceID: "delly:delly:104", NodeName: "delly", ClusterName: "homelab", Instance: "delly", VMID: 104},
|
|
},
|
|
Identity: unified.ResourceIdentity{Hostnames: []string{"cloudflared"}},
|
|
},
|
|
}})
|
|
|
|
router := &Router{config: cfg, monitor: monitor}
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/state", nil)
|
|
req.SetBasicAuth("admin", "password")
|
|
router.handleState(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("/api/state status = %d, body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var state models.StateFrontend
|
|
if err := json.NewDecoder(rec.Body).Decode(&state); err != nil {
|
|
t.Fatalf("decode /api/state: %v", err)
|
|
}
|
|
dellyCount, dellyID, cloudflaredParentID := 0, "", ""
|
|
for _, resource := range state.Resources {
|
|
switch {
|
|
case resource.Type == string(unified.ResourceTypeAgent) && resource.Name == "delly":
|
|
dellyCount++
|
|
dellyID = resource.ID
|
|
case resource.Type == string(unified.ResourceTypeSystemContainer) && resource.Name == "cloudflared":
|
|
cloudflaredParentID = resource.ParentID
|
|
}
|
|
}
|
|
if dellyCount != 1 || dellyID == "" || cloudflaredParentID != dellyID {
|
|
t.Fatalf("unexpected /api/state resource projection: dellyCount=%d dellyID=%q cloudflaredParentID=%q resources=%#v", dellyCount, dellyID, cloudflaredParentID, state.Resources)
|
|
}
|
|
}
|
|
|
|
// Closing must be safe to repeat and safe on a nil compatibility facade: the
|
|
// shutdown path runs before every dependency is guaranteed to be wired.
|
|
func TestResourceHandlers_CloseIsIdempotentAndNilSafe(t *testing.T) {
|
|
var nilHandlers *ResourceHandlers
|
|
if err := nilHandlers.CloseStores(); err != nil {
|
|
t.Fatalf("nil handler CloseStores: %v", err)
|
|
}
|
|
if err := nilHandlers.CloseTenantStore("client-a"); err != nil {
|
|
t.Fatalf("nil handler CloseTenantStore: %v", err)
|
|
}
|
|
|
|
handlers := NewResourceHandlers(&config.Config{DataPath: t.TempDir()})
|
|
if _, err := handlers.getStore("client-a"); err != nil {
|
|
t.Fatalf("getStore: %v", err)
|
|
}
|
|
if err := handlers.CloseStores(); err != nil {
|
|
t.Fatalf("first CloseStores: %v", err)
|
|
}
|
|
if err := handlers.CloseStores(); err != nil {
|
|
t.Fatalf("second CloseStores must be a no-op: %v", err)
|
|
}
|
|
if err := handlers.CloseTenantStore("client-a"); err != nil {
|
|
t.Fatalf("CloseTenantStore on an evicted org must be a no-op: %v", err)
|
|
}
|
|
}
|