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 }