//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 TestRedisAppliesGlobalHealthObservationAtomically(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) } if _, err := fixture.Adapter.ApplyHealth(context.Background(), activitypool.HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(time.Second), NextState: proxyDomain.StateChecking, }); err != nil { t.Fatalf("ApplyHealth(checking): %v", err) } first := redisGlobalObservation("task-1", false, now.Add(2*time.Second)) failed, err := fixture.Adapter.ApplyGlobalObservation(context.Background(), activitypool.GlobalHealthCommand{ Observation: first, MaxConsecutiveFailures: 3, }) if err != nil || failed.State != proxyDomain.StateUnhealthy || failed.GlobalHealth.ConsecutiveFailures != 1 { t.Fatalf("ApplyGlobalObservation(initial failure) = %+v, %v", failed, err) } replayed, err := fixture.Adapter.ApplyGlobalObservation(context.Background(), activitypool.GlobalHealthCommand{ Observation: first, MaxConsecutiveFailures: 3, }) if err != nil || replayed.GlobalHealth != failed.GlobalHealth { t.Fatalf("ApplyGlobalObservation(replay) = %+v, %v", replayed, err) } conflicting := first conflicting.Success = true conflicting.FailureClass = "" if _, err := fixture.Adapter.ApplyGlobalObservation(context.Background(), activitypool.GlobalHealthCommand{ Observation: conflicting, MaxConsecutiveFailures: 3, }); !errors.Is(err, healthDomain.ErrConflictingObservation) { t.Fatalf("ApplyGlobalObservation(conflict) error = %v, want ErrConflictingObservation", err) } if _, err := fixture.Adapter.ApplyHealth(context.Background(), activitypool.HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(3 * time.Second), NextState: proxyDomain.StateChecking, }); err != nil { t.Fatalf("ApplyHealth(recheck): %v", err) } recovered, err := fixture.Adapter.ApplyGlobalObservation(context.Background(), activitypool.GlobalHealthCommand{ Observation: redisGlobalObservation("task-2", true, now.Add(4*time.Second)), MaxConsecutiveFailures: 3, }) if err != nil || recovered.State != proxyDomain.StateAvailable || recovered.GlobalHealth.ConsecutiveFailures != 0 || recovered.GlobalHealth.LastObservedAt != now.Add(4*time.Second) { t.Fatalf("ApplyGlobalObservation(recovery) = %+v, %v", recovered, err) } } func redisGlobalObservation(taskID string, success bool, observedAt time.Time) healthDomain.Observation { observation := healthDomain.Observation{ TaskID: taskID, ProxyID: "proxy-a", Level: healthDomain.LevelBasic, Success: success, Latency: 10 * time.Millisecond, ObservedAt: observedAt, } if !success { observation.FailureClass = "timeout" } return observation }