This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/itworx/pulse/internal/probe"
|
||||
)
|
||||
|
||||
func TestBuildSnapshotDistinguishesConfigurationAndCapabilityStates(t *testing.T) {
|
||||
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
||||
empty, err := BuildSnapshot(now, nil, StatusPolicy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if empty.CapabilityState != CapabilityAvailable || empty.ConfigurationState != ConfigurationNotConfigured || empty.Reason != "no_services_configured" {
|
||||
t.Fatalf("empty configuration was not explicit: %+v", empty)
|
||||
}
|
||||
unavailable := UnknownSnapshot(now, "source_unavailable")
|
||||
if unavailable.CapabilityState != CapabilityUnavailable || unavailable.ConfigurationState != ConfigurationUnknown {
|
||||
t.Fatalf("unavailable capability was presented as configuration: %+v", unavailable)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSnapshotDistinguishesMissingDisabledAndPendingProbes(t *testing.T) {
|
||||
now := time.Date(2026, 8, 12, 1, 0, 0, 0, time.UTC)
|
||||
base := Service{ID: "service-a", Name: "A", State: StateUnknown, Revision: 1, CreatedAt: now, UpdatedAt: now}
|
||||
inputs := []ServiceInput{
|
||||
{Service: base},
|
||||
{Service: Service{ID: "service-b", Name: "B", State: StateUnknown, Revision: 1, CreatedAt: now, UpdatedAt: now}, ProbeConfigs: []ProbeConfig{{ID: "probe-b", Name: "B", Type: probe.TypeHTTP, Enabled: false}}},
|
||||
{Service: Service{ID: "service-c", Name: "C", State: StateUnknown, Revision: 1, CreatedAt: now, UpdatedAt: now}, ProbeConfigs: []ProbeConfig{{ID: "probe-c", Name: "C", Type: probe.TypeHTTP, Enabled: true}}},
|
||||
}
|
||||
snapshot, err := BuildSnapshot(now, inputs, StatusPolicy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot.Services[0].Reason != "no_probe_configured" || snapshot.Services[1].Reason != "probes_disabled" || snapshot.Services[2].Reason != "no_probe_result" {
|
||||
t.Fatalf("probe configuration states were conflated: %+v", snapshot.Services)
|
||||
}
|
||||
}
|
||||
|
||||
func statusService(id string) Service {
|
||||
return Service{ID: id, EntityID: "entity-" + id, Name: "Service " + id, State: StateUnknown, Revision: 1}
|
||||
}
|
||||
|
||||
func statusResult(probeID, state string, observed time.Time, latency int) probe.Result {
|
||||
return probe.Result{ID: probeID + "-" + observed.Format("150405"), ProbeID: probeID, ObservedAt: observed, CompletedAt: observed.Add(time.Second), State: state, ResponseTimeMS: &latency}
|
||||
}
|
||||
|
||||
func TestBuildSnapshotDerivesIndependentServiceStateAndHistory(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
first := now.Add(-90 * time.Second)
|
||||
second := now.Add(-30 * time.Second)
|
||||
snapshot, err := BuildSnapshot(now, []ServiceInput{{Service: statusService("service-1"), Probes: []ProbeHistory{{ProbeID: "probe-1", Results: []probe.Result{statusResult("probe-1", StateUp, first, 80), statusResult("probe-1", StateDown, second, 150)}}}}}, StatusPolicy{FreshnessMaxAge: 2 * time.Minute, MaxHistory: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item := snapshot.Services[0]
|
||||
if item.State != StateDown || item.LastSuccessAt == nil || !item.LastSuccessAt.Equal(first) || item.LastFailureAt == nil || !item.LastFailureAt.Equal(second) {
|
||||
t.Fatalf("unexpected service status: %+v", item)
|
||||
}
|
||||
if item.SampleCount != 2 || item.SuccessfulSampleCount != 1 || item.AvailabilityPercent == nil || *item.AvailabilityPercent != 50 {
|
||||
t.Fatalf("unexpected availability: %+v", item)
|
||||
}
|
||||
if len(item.History) != 2 || item.History[0].ObservedAt != second || item.ResponseTimeMS == nil || *item.ResponseTimeMS != 150 {
|
||||
t.Fatalf("unexpected history: %+v", item.History)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSnapshotMakesStaleProbeUnknownAndBoundsHistory(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
results := make([]probe.Result, 0, 4)
|
||||
for index := 0; index < 4; index++ {
|
||||
results = append(results, statusResult("probe-1", StateUp, now.Add(-time.Duration(index+1)*time.Minute), index))
|
||||
}
|
||||
snapshot, err := BuildSnapshot(now, []ServiceInput{{Service: statusService("service-1"), Probes: []ProbeHistory{{ProbeID: "probe-1", Results: results}}}}, StatusPolicy{FreshnessMaxAge: 30 * time.Second, MaxHistory: 2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item := snapshot.Services[0]
|
||||
if item.State != StateUnknown || item.Reason != "stale_probe" || len(item.History) != 2 {
|
||||
t.Fatalf("stale or history bound incorrect: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransitionEventsAreDeterministicAndTyped(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
previous := Snapshot{ObservedAt: now.Add(-time.Minute), Services: []ServiceStatus{{ID: "service-1", State: StateUp}}}
|
||||
current := Snapshot{ObservedAt: now, Services: []ServiceStatus{{ID: "service-1", State: StateDown, Reason: "status_not_expected"}}}
|
||||
first := TransitionEvents(previous, current)
|
||||
second := TransitionEvents(previous, current)
|
||||
if len(first) != 1 || first[0].Type != "service.down" || first[0].FromState != StateUp || first[0].ToState != StateDown || first[0].ID == "" || first[0].ID != second[0].ID {
|
||||
t.Fatalf("unexpected transition events: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceStatusValidationAndUnknownProviderCancellation(t *testing.T) {
|
||||
if err := (StatusPolicy{MaxHistory: 501}).Validate(); err == nil {
|
||||
t.Fatal("unsafe service policy accepted")
|
||||
}
|
||||
provider := UnknownProvider{Now: func() time.Time { return time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC) }}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, err := provider.Snapshot(ctx); err == nil {
|
||||
t.Fatal("canceled context was ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSnapshotBoundsAttributesWithoutLeakingComplexValues(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
attributes := make(map[string]any, 18)
|
||||
for index := 0; index < 18; index++ {
|
||||
attributes[string(rune('a'+index))] = map[string]any{"nested": "value"}
|
||||
}
|
||||
result := statusResult("probe-1", StateUp, now.Add(-time.Second), 1)
|
||||
result.Attributes = attributes
|
||||
snapshot, err := BuildSnapshot(now, []ServiceInput{{Service: statusService("service-1"), Probes: []ProbeHistory{{ProbeID: "probe-1", Results: []probe.Result{result}}}}}, StatusPolicy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bounded := snapshot.Services[0].History[0].Attributes
|
||||
if len(bounded) != 16 || bounded["a"] != "[redacted]" {
|
||||
t.Fatalf("attributes were not bounded: %#v", bounded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSnapshotTargetScale150Services(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
inputs := make([]ServiceInput, 150)
|
||||
for serviceIndex := range inputs {
|
||||
results := make([]probe.Result, 4)
|
||||
for resultIndex := range results {
|
||||
results[resultIndex] = statusResult(fmt.Sprintf("probe-%03d", serviceIndex), StateUp, now.Add(-time.Duration(resultIndex+1)*time.Second), resultIndex+1)
|
||||
}
|
||||
inputs[serviceIndex] = ServiceInput{Service: statusService(fmt.Sprintf("service-%03d", 150-serviceIndex)), Probes: []ProbeHistory{{ProbeID: results[0].ProbeID, Results: results}}}
|
||||
}
|
||||
started := time.Now()
|
||||
snapshot, err := BuildSnapshot(now, inputs, StatusPolicy{MaxServices: 150, MaxHistory: 4})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(snapshot.Services) != 150 || snapshot.Services[0].ID != "service-001" || snapshot.Services[149].ID != "service-150" || time.Since(started) > 2*time.Second {
|
||||
t.Fatalf("150-service projection exceeded target: count=%d first=%s last=%s duration=%s", len(snapshot.Services), snapshot.Services[0].ID, snapshot.Services[149].ID, time.Since(started))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSnapshotBoundsAndSortsProbeConfigsWithoutSecrets(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
snapshot, err := BuildSnapshot(now, []ServiceInput{{
|
||||
Service: statusService("service-1"),
|
||||
ProbeConfigs: []ProbeConfig{
|
||||
{ID: "probe-b", Name: "B", Type: "https", IntervalSeconds: 30, TimeoutSeconds: 5, Enabled: true},
|
||||
{ID: "probe-a", Name: "A", Type: "tcp", IntervalSeconds: 60, TimeoutSeconds: 10, Enabled: false},
|
||||
},
|
||||
}}, StatusPolicy{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(snapshot.Services[0].Probes) != 2 || snapshot.Services[0].Probes[0].ID != "probe-a" {
|
||||
t.Fatalf("probe configs were not deterministic: %+v", snapshot.Services[0].Probes)
|
||||
}
|
||||
encoded, err := json.Marshal(snapshot.Services[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encoded), "secret") || strings.Contains(string(encoded), "target") {
|
||||
t.Fatalf("probe config exposed forbidden fields: %s", encoded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user