106 lines
4.9 KiB
Go
106 lines
4.9 KiB
Go
package health
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
|
)
|
|
|
|
func TestReduceGlobalAppliesSuspectUnhealthyAndRecovery(t *testing.T) {
|
|
now := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
|
|
current := GlobalState{State: proxyDomain.StateAvailable}
|
|
first, err := ReduceGlobal(current, globalObservation("task-1", false, now), 2)
|
|
if err != nil || first.State != proxyDomain.StateSuspect || first.ConsecutiveFailures != 1 {
|
|
t.Fatalf("first failure = %+v, %v; want SUSPECT / 1", first, err)
|
|
}
|
|
second, err := ReduceGlobal(first, globalObservation("task-2", false, now.Add(time.Second)), 2)
|
|
if err != nil || second.State != proxyDomain.StateUnhealthy || second.ConsecutiveFailures != 2 {
|
|
t.Fatalf("second failure = %+v, %v; want UNHEALTHY / 2", second, err)
|
|
}
|
|
if next, err := BeginGlobalCheck(second); err != nil || next != proxyDomain.StateChecking {
|
|
t.Fatalf("BeginGlobalCheck(unhealthy) = %s, %v; want CHECKING", next, err)
|
|
}
|
|
recovering := second
|
|
recovering.State = proxyDomain.StateChecking
|
|
recovered, err := ReduceGlobal(recovering, globalObservation("task-3", true, now.Add(2*time.Second)), 2)
|
|
if err != nil || recovered.State != proxyDomain.StateAvailable || recovered.ConsecutiveFailures != 0 {
|
|
t.Fatalf("recovery = %+v, %v; want AVAILABLE / 0", recovered, err)
|
|
}
|
|
}
|
|
|
|
func TestReduceGlobalMarksFailedInitialCheckUnhealthy(t *testing.T) {
|
|
now := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
|
|
next, err := ReduceGlobal(GlobalState{State: proxyDomain.StateChecking}, globalObservation("task-1", false, now), 3)
|
|
if err != nil || next.State != proxyDomain.StateUnhealthy || next.ConsecutiveFailures != 1 {
|
|
t.Fatalf("initial failure = %+v, %v; want UNHEALTHY / 1", next, err)
|
|
}
|
|
}
|
|
|
|
func TestReduceGlobalRejectsStaleAndConflictingReplays(t *testing.T) {
|
|
now := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
|
|
first, err := ReduceGlobal(GlobalState{State: proxyDomain.StateAvailable}, globalObservation("task-1", false, now), 2)
|
|
if err != nil {
|
|
t.Fatalf("ReduceGlobal(first): %v", err)
|
|
}
|
|
replayed, err := ReduceGlobal(first, globalObservation("task-1", false, now), 2)
|
|
if err != nil || replayed != first {
|
|
t.Fatalf("ReduceGlobal(replay) = %+v, %v; want unchanged", replayed, err)
|
|
}
|
|
if _, err := ReduceGlobal(first, globalObservation("task-1", true, now), 2); !errors.Is(err, ErrConflictingObservation) {
|
|
t.Fatalf("conflicting replay error = %v, want ErrConflictingObservation", err)
|
|
}
|
|
if _, err := ReduceGlobal(first, globalObservation("task-2", true, now.Add(-time.Second)), 2); !errors.Is(err, ErrStaleObservation) {
|
|
t.Fatalf("stale observation error = %v, want ErrStaleObservation", err)
|
|
}
|
|
}
|
|
|
|
func TestReduceTargetNeverChangesGlobalState(t *testing.T) {
|
|
now := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
|
|
observation := Observation{
|
|
TaskID: "task-target-1", ProxyID: "proxy-a", Level: LevelTarget, RoutingName: "route-a",
|
|
TargetURL: "https://target.example/path?volatile=true", Success: false, FailureClass: "target_403",
|
|
Latency: 20 * time.Millisecond, ObservedAt: now,
|
|
}
|
|
target, err := ReduceTarget(TargetState{}, observation, 2)
|
|
if err != nil || target.Status != TargetSuspect || target.ConsecutiveFailures != 1 {
|
|
t.Fatalf("ReduceTarget(first) = %+v, %v; want SUSPECT / 1", target, err)
|
|
}
|
|
if _, err := ReduceGlobal(GlobalState{State: proxyDomain.StateAvailable}, observation, 2); !errors.Is(err, ErrNonGlobalObservation) {
|
|
t.Fatalf("ReduceGlobal(target) error = %v, want ErrNonGlobalObservation", err)
|
|
}
|
|
profile, err := NormalizeTargetProfile(TargetProfile{RoutingName: observation.RoutingName, TargetURL: observation.TargetURL})
|
|
if err != nil || profile.TargetURL != "https://target.example/path?volatile=true" {
|
|
t.Fatalf("NormalizeTargetProfile() = %+v, %v", profile, err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeObservationRejectsCrossLevelFields(t *testing.T) {
|
|
now := time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
|
|
_, err := NormalizeObservation(Observation{
|
|
TaskID: "task-a", ProxyID: "proxy-a", Level: LevelBasic, RoutingName: "route-a", Success: true, ObservedAt: now,
|
|
})
|
|
if !errors.Is(err, ErrInvalidObservation) {
|
|
t.Fatalf("NormalizeObservation() error = %v, want ErrInvalidObservation", err)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeEgressTargetAcceptsOnlyHTTPOrHTTPSURL(t *testing.T) {
|
|
target, err := NormalizeEgressTarget("https://egress.example/identity?format=json")
|
|
if err != nil || target != "https://egress.example/identity?format=json" {
|
|
t.Fatalf("NormalizeEgressTarget(valid) = (%q, %v)", target, err)
|
|
}
|
|
if _, err := NormalizeEgressTarget("ftp://egress.example/identity"); !errors.Is(err, ErrInvalidObservation) {
|
|
t.Fatalf("NormalizeEgressTarget(ftp) error = %v, want ErrInvalidObservation", err)
|
|
}
|
|
}
|
|
|
|
func globalObservation(taskID string, success bool, observedAt time.Time) Observation {
|
|
observation := Observation{TaskID: taskID, ProxyID: "proxy-a", Level: LevelBasic, Success: success, ObservedAt: observedAt}
|
|
if !success {
|
|
observation.FailureClass = "timeout"
|
|
}
|
|
return observation
|
|
}
|