proxy-pool/internal/adapters/redisactivity/target_health_integration_test.go
youfak 19cf3e32fa
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: persist target health profiles
2026-07-31 18:04:05 +08:00

92 lines
4.2 KiB
Go

//go:build integration
package redisactivity
import (
"context"
"errors"
"testing"
"time"
"proxy-pool/internal/domain/activitypool"
healthDomain "proxy-pool/internal/domain/health"
proxyDomain "proxy-pool/internal/domain/proxy"
)
func TestRedisKeepsTargetHealthOutsideGlobalProxyState(t *testing.T) {
fixture := newRedisTestFixture(t)
now := redisTestNow()
if _, err := fixture.Adapter.UpsertFetched(context.Background(), "provider-a", activitypool.FetchedBatch{
ObservedAt: now, ConfiguredTTL: time.Minute, MaxSize: 1,
Proxies: []proxyDomain.Proxy{testProxy("proxy-a", "192.0.2.10")},
}); err != nil {
t.Fatalf("UpsertFetched(): %v", err)
}
firstObservation := redisTargetObservation("target-task-1", false, now.Add(time.Second))
first, err := fixture.Adapter.ApplyTargetObservation(context.Background(), activitypool.TargetHealthCommand{
Observation: firstObservation, MaxConsecutiveFailures: 2,
})
if err != nil || first.Status != healthDomain.TargetSuspect || first.ConsecutiveFailures != 1 ||
first.LastObservedAt != firstObservation.ObservedAt || first.Latency != firstObservation.Latency {
t.Fatalf("ApplyTargetObservation(first) = %+v, %v", first, err)
}
replayed, err := fixture.Adapter.ApplyTargetObservation(context.Background(), activitypool.TargetHealthCommand{
Observation: firstObservation, MaxConsecutiveFailures: 2,
})
if err != nil || replayed != first {
t.Fatalf("ApplyTargetObservation(replay) = %+v, %v", replayed, err)
}
conflicting := firstObservation
conflicting.Success = true
conflicting.FailureClass = ""
if _, err := fixture.Adapter.ApplyTargetObservation(context.Background(), activitypool.TargetHealthCommand{
Observation: conflicting, MaxConsecutiveFailures: 2,
}); !errors.Is(err, healthDomain.ErrConflictingObservation) {
t.Fatalf("ApplyTargetObservation(conflict) error = %v, want ErrConflictingObservation", err)
}
second, err := fixture.Adapter.ApplyTargetObservation(context.Background(), activitypool.TargetHealthCommand{
Observation: redisTargetObservation("target-task-2", false, now.Add(2*time.Second)), MaxConsecutiveFailures: 2,
})
if err != nil || second.Status != healthDomain.TargetUnhealthy || second.ConsecutiveFailures != 2 {
t.Fatalf("ApplyTargetObservation(second failure) = %+v, %v", second, err)
}
recoveredObservation := redisTargetObservation("target-task-3", true, now.Add(3*time.Second))
recovered, err := fixture.Adapter.ApplyTargetObservation(context.Background(), activitypool.TargetHealthCommand{
Observation: recoveredObservation, MaxConsecutiveFailures: 2,
})
if err != nil || recovered.Status != healthDomain.TargetAvailable || recovered.ConsecutiveFailures != 0 ||
recovered.LastSuccessAt != recoveredObservation.ObservedAt {
t.Fatalf("ApplyTargetObservation(recovery) = %+v, %v", recovered, err)
}
profile := healthDomain.TargetProfile{RoutingName: recoveredObservation.RoutingName, TargetURL: recoveredObservation.TargetURL}
profileKey := fixture.Adapter.keys.targetHealth("proxy-a")
field := digestToken(profile.Key())
if ttl, err := fixture.Client.PTTL(context.Background(), profileKey).Result(); err != nil || ttl <= 0 {
t.Fatalf("target health key TTL = (%v, %v), want positive", ttl, err)
}
if exists, err := fixture.Client.HExists(context.Background(), profileKey, field).Result(); err != nil || !exists {
t.Fatalf("target health profile exists = (%v, %v), want true", exists, err)
}
global, err := fixture.Adapter.ApplyHealth(context.Background(), activitypool.HealthUpdate{
ProxyID: "proxy-a", CheckedAt: now.Add(4 * time.Second), NextState: proxyDomain.StateChecking,
})
if err != nil || global.State != proxyDomain.StateChecking || global.GlobalHealth.LastObservedAt != (time.Time{}) ||
global.GlobalHealth.ConsecutiveFailures != 0 {
t.Fatalf("target observation changed global proxy state: %+v, %v", global, err)
}
}
func redisTargetObservation(taskID string, success bool, observedAt time.Time) healthDomain.Observation {
observation := healthDomain.Observation{
TaskID: taskID, ProxyID: "proxy-a", Level: healthDomain.LevelTarget,
RoutingName: "route-a", TargetURL: "https://target.example/check", Success: success,
Latency: 20 * time.Millisecond, ObservedAt: observedAt,
}
if !success {
observation.FailureClass = "target_403"
}
return observation
}