proxy-pool/internal/domain/health/health_test.go
youfak 84ed10bd7a
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: reap sustained unhealthy proxies
2026-08-02 10:15:25 +08:00

108 lines
5.0 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 ||
!second.UnhealthySince.Equal(now.Add(time.Second)) {
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 ||
!recovered.UnhealthySince.IsZero() {
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 || !next.UnhealthySince.Equal(now) {
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
}