mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
ca3e4b5709
Move the ICMP, TCP, HTTP and UDP probe core from the monitoring availability poller into internal/availabilityprobe so the host agent can execute the same checks without importing the monitoring package. Monitoring keeps type aliases and thin wrappers, so callers and the canonical guardrail contracts are unchanged. Pure refactor for the upcoming external probe feature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
239 lines
8.9 KiB
Go
239 lines
8.9 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
func TestProbeAvailabilityTargetHTTPFallsBackToGETWhenHeadNotAllowed(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodHead:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
case http.MethodGet:
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
Address: server.URL,
|
|
Protocol: config.AvailabilityProbeHTTP,
|
|
Enabled: true,
|
|
TimeoutMillis: 1000,
|
|
})
|
|
|
|
if err := ProbeAvailabilityTarget(context.Background(), target); err != nil {
|
|
t.Fatalf("ProbeAvailabilityTarget() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// TestAvailabilityHTTPOutboundOptionsUsesSharedPeerCertificateCapture moved to
|
|
// internal/availabilityprobe alongside the outbound options it asserts on.
|
|
|
|
func TestProbeAvailabilityTargetHTTPTreatsServerErrorsAsUnavailable(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
}))
|
|
defer server.Close()
|
|
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
Address: server.URL,
|
|
Protocol: config.AvailabilityProbeHTTP,
|
|
Enabled: true,
|
|
TimeoutMillis: 1000,
|
|
})
|
|
|
|
if err := ProbeAvailabilityTarget(context.Background(), target); err == nil {
|
|
t.Fatal("ProbeAvailabilityTarget() error = nil, want HTTP 5xx error")
|
|
}
|
|
}
|
|
|
|
func TestProbeAvailabilityTargetHTTPRejectsMetadataService(t *testing.T) {
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
Address: "http://169.254.169.254/latest/meta-data",
|
|
Protocol: config.AvailabilityProbeHTTP,
|
|
Enabled: true,
|
|
TimeoutMillis: 1000,
|
|
})
|
|
|
|
err := ProbeAvailabilityTarget(context.Background(), target)
|
|
if err == nil {
|
|
t.Fatal("ProbeAvailabilityTarget() error = nil, want metadata-service rejection")
|
|
}
|
|
if got := err.Error(); !strings.Contains(got, "metadata service") {
|
|
t.Fatalf("error = %q, want metadata-service rejection", got)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityPollProviderSupplementalRecordsProjectNetworkEndpointIncident(t *testing.T) {
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
ID: "sensor-1",
|
|
Name: "Energy monitor",
|
|
TargetKind: config.AvailabilityTargetDevice,
|
|
Address: "192.0.2.10",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
FailureThreshold: 2,
|
|
})
|
|
if err := persistence.SaveAvailabilityTargets([]config.AvailabilityTarget{target}); err != nil {
|
|
t.Fatalf("SaveAvailabilityTargets() error = %v", err)
|
|
}
|
|
|
|
checkedAt := time.Date(2026, 5, 6, 10, 0, 0, 0, time.UTC)
|
|
monitor := &Monitor{
|
|
configPersist: persistence,
|
|
availabilityStatuses: map[string]AvailabilityProbeStatus{
|
|
target.ID: {
|
|
TargetID: target.ID,
|
|
Name: target.DisplayName(),
|
|
Address: target.Address,
|
|
Protocol: string(target.Protocol),
|
|
Enabled: true,
|
|
Available: false,
|
|
LastChecked: checkedAt,
|
|
ConsecutiveFailures: 2,
|
|
LastError: "timeout",
|
|
FailureThreshold: 2,
|
|
},
|
|
},
|
|
}
|
|
|
|
records := availabilityPollProvider{}.SupplementalRecords(monitor, "org-a")
|
|
if len(records) != 1 {
|
|
t.Fatalf("SupplementalRecords() length = %d, want 1", len(records))
|
|
}
|
|
|
|
resource := records[0].Resource
|
|
if resource.Type != unifiedresources.ResourceTypeNetworkEndpoint {
|
|
t.Fatalf("resource type = %q, want network-endpoint", resource.Type)
|
|
}
|
|
if resource.Status != unifiedresources.StatusOffline {
|
|
t.Fatalf("resource status = %q, want offline", resource.Status)
|
|
}
|
|
if resource.Availability == nil || resource.Availability.TargetID != target.ID {
|
|
t.Fatalf("availability payload = %+v, want target %q", resource.Availability, target.ID)
|
|
}
|
|
if resource.Availability.TargetKind != string(config.AvailabilityTargetDevice) {
|
|
t.Fatalf("availability target kind = %q, want %q", resource.Availability.TargetKind, config.AvailabilityTargetDevice)
|
|
}
|
|
if resource.Availability.Evidence == nil {
|
|
t.Fatal("availability evidence = nil")
|
|
}
|
|
if err := resource.Availability.Evidence.Validate(); err != nil {
|
|
t.Fatalf("availability evidence validation error = %v", err)
|
|
}
|
|
if resource.Availability.Evidence.Subject.ProviderRef != target.ID ||
|
|
resource.Availability.Evidence.Subject.ProviderScope != "availability-target" {
|
|
t.Fatalf("availability evidence subject = %+v", resource.Availability.Evidence.Subject)
|
|
}
|
|
if resource.Availability.Evidence.ObservedAt != checkedAt {
|
|
t.Fatalf("evidence observed at = %v, want %v", resource.Availability.Evidence.ObservedAt, checkedAt)
|
|
}
|
|
if len(resource.Incidents) != 1 || resource.Incidents[0].Code != "availability_unreachable" {
|
|
t.Fatalf("incidents = %+v, want availability_unreachable", resource.Incidents)
|
|
}
|
|
if len(records[0].Identity.IPAddresses) != 1 || records[0].Identity.IPAddresses[0] != "192.0.2.10" {
|
|
t.Fatalf("identity IPs = %+v, want 192.0.2.10", records[0].Identity.IPAddresses)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityPollProviderListsOnlyEnabledTargets(t *testing.T) {
|
|
persistence := config.NewConfigPersistence(t.TempDir())
|
|
targets := []config.AvailabilityTarget{
|
|
{ID: "enabled", Name: "Enabled", Address: "enabled.local", Protocol: config.AvailabilityProbeICMP, Enabled: true},
|
|
{ID: "paused", Name: "Paused", Address: "paused.local", Protocol: config.AvailabilityProbeICMP, Enabled: false},
|
|
}
|
|
if err := persistence.SaveAvailabilityTargets(targets); err != nil {
|
|
t.Fatalf("SaveAvailabilityTargets() error = %v", err)
|
|
}
|
|
|
|
monitor := &Monitor{configPersist: persistence}
|
|
got := availabilityPollProvider{}.ListInstances(monitor)
|
|
if len(got) != 1 || got[0] != "enabled" {
|
|
t.Fatalf("ListInstances() = %+v, want [enabled]", got)
|
|
}
|
|
records := availabilityPollProvider{}.SupplementalRecords(monitor, "org-a")
|
|
if len(records) != 2 {
|
|
t.Fatalf("SupplementalRecords() length = %d, want every configured target", len(records))
|
|
}
|
|
foundPaused := false
|
|
for _, record := range records {
|
|
if record.SourceID == "paused" {
|
|
foundPaused = true
|
|
if record.Resource.Availability == nil || record.Resource.Availability.Enabled {
|
|
t.Fatalf("paused target projection = %+v, want disabled check row", record.Resource.Availability)
|
|
}
|
|
}
|
|
}
|
|
if !foundPaused {
|
|
t.Fatal("disabled configured target is missing from supplemental records")
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityResourceFromTargetOmitsUnsetProbeTimes(t *testing.T) {
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
ID: "router",
|
|
Name: "Router",
|
|
Address: "192.0.2.1",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
})
|
|
now := time.Date(2026, 7, 9, 12, 0, 0, 0, time.UTC)
|
|
|
|
resource, _ := availabilityResourceFromTarget(target, availabilityStatusFromTarget(target), "org-a", now)
|
|
if resource.Availability == nil {
|
|
t.Fatal("availability payload = nil")
|
|
}
|
|
if resource.Availability.LastChecked != nil {
|
|
t.Fatalf("last checked = %v, want nil before the first probe", resource.Availability.LastChecked)
|
|
}
|
|
if resource.Availability.LastSuccess != nil {
|
|
t.Fatalf("last success = %v, want nil before the first successful probe", resource.Availability.LastSuccess)
|
|
}
|
|
if resource.Availability.Evidence == nil {
|
|
t.Fatal("availability evidence = nil before first probe")
|
|
}
|
|
if resource.Availability.Evidence.Completeness != operationaltrust.EvidencePartial ||
|
|
resource.Availability.Evidence.Confidence != operationaltrust.EvidenceUnknown {
|
|
t.Fatalf("initial availability evidence = %+v, want partial/unknown", resource.Availability.Evidence)
|
|
}
|
|
}
|
|
|
|
func TestAvailabilityResourceFromTargetPreservesProbeTimes(t *testing.T) {
|
|
target := config.NormalizeAvailabilityTarget(config.AvailabilityTarget{
|
|
ID: "router",
|
|
Name: "Router",
|
|
Address: "192.0.2.1",
|
|
Protocol: config.AvailabilityProbeICMP,
|
|
Enabled: true,
|
|
})
|
|
checkedAt := time.Date(2026, 7, 9, 11, 59, 55, 0, time.UTC)
|
|
succeededAt := checkedAt.Add(-time.Minute)
|
|
status := availabilityStatusFromTarget(target)
|
|
status.LastChecked = checkedAt
|
|
status.LastSuccess = succeededAt
|
|
|
|
resource, _ := availabilityResourceFromTarget(target, status, "org-a", checkedAt.Add(time.Second))
|
|
if resource.Availability == nil {
|
|
t.Fatal("availability payload = nil")
|
|
}
|
|
if resource.Availability.LastChecked == nil || !resource.Availability.LastChecked.Equal(checkedAt) {
|
|
t.Fatalf("last checked = %v, want %v", resource.Availability.LastChecked, checkedAt)
|
|
}
|
|
if resource.Availability.LastSuccess == nil || !resource.Availability.LastSuccess.Equal(succeededAt) {
|
|
t.Fatalf("last success = %v, want %v", resource.Availability.LastSuccess, succeededAt)
|
|
}
|
|
}
|