package activitypool import ( "context" "errors" "testing" "time" proxyDomain "proxy-pool/internal/domain/proxy" ) func TestMemoryPoolAppliesHealthTransitionsAndRejectsStaleObservation(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 29, 15, 0, 0, 0, time.UTC) pool := NewMemoryPool() inserted, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: now, ConfiguredTTL: time.Minute, MaxSize: 10, Proxies: []proxyDomain.Proxy{{ ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, State: proxyDomain.StateFetched, }}, }) if err != nil || inserted.Inserted != 1 { t.Fatalf("UpsertFetched() = %+v, %v", inserted, err) } checking, err := pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(time.Second), NextState: proxyDomain.StateChecking, }) if err != nil || checking.State != proxyDomain.StateChecking { t.Fatalf("ApplyHealth(checking) = %+v, %v", checking, err) } available, err := pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(2 * time.Second), NextState: proxyDomain.StateAvailable, Latency: 25 * time.Millisecond, }) if err != nil || available.State != proxyDomain.StateAvailable || available.Proxy.LastCheckedAt == nil || !available.Proxy.LastCheckedAt.Equal(now.Add(2*time.Second)) || available.Proxy.LastSuccessAt == nil || !available.Proxy.LastSuccessAt.Equal(now.Add(2*time.Second)) || available.Proxy.Latency != 25*time.Millisecond { t.Fatalf("ApplyHealth(available) = %+v, %v", available, err) } _, err = pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(time.Second), NextState: proxyDomain.StateSuspect, }) if !errors.Is(err, ErrStaleHealthUpdate) { t.Fatalf("ApplyHealth(stale) error = %v, want ErrStaleHealthUpdate", err) } replayed, err := pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(2 * time.Second), NextState: proxyDomain.StateAvailable, Latency: time.Second, }) if err != nil || replayed.Proxy.Latency != 25*time.Millisecond { t.Fatalf("ApplyHealth(idempotent replay) = %+v, %v", replayed, err) } _, err = pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(2 * time.Second), NextState: proxyDomain.StateSuspect, }) if !errors.Is(err, ErrStaleHealthUpdate) { t.Fatalf("ApplyHealth(conflicting replay) error = %v, want ErrStaleHealthUpdate", err) } } func TestMemoryPoolRejectsInvalidHealthUpdates(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 29, 15, 0, 0, 0, time.UTC) pool := NewMemoryPool() if _, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: now, ConfiguredTTL: time.Minute, MaxSize: 10, Proxies: []proxyDomain.Proxy{{ ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, State: proxyDomain.StateFetched, }}, }); err != nil { t.Fatalf("UpsertFetched(): %v", err) } tests := []struct { name string update HealthUpdate want error }{ {name: "missing proxy ID", update: HealthUpdate{CheckedAt: now, NextState: proxyDomain.StateChecking}, want: ErrInvalidHealthUpdate}, {name: "zero observation time", update: HealthUpdate{ProxyID: "proxy-a", NextState: proxyDomain.StateChecking}, want: ErrInvalidHealthUpdate}, {name: "negative latency", update: HealthUpdate{ProxyID: "proxy-a", CheckedAt: now, NextState: proxyDomain.StateChecking, Latency: -1}, want: ErrInvalidHealthUpdate}, {name: "missing entry", update: HealthUpdate{ProxyID: "missing", CheckedAt: now, NextState: proxyDomain.StateChecking}, want: ErrActivityNotFound}, {name: "invalid transition", update: HealthUpdate{ProxyID: "proxy-a", CheckedAt: now, NextState: proxyDomain.StateAvailable}, want: ErrInvalidHealthUpdate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := pool.ApplyHealth(context.Background(), tt.update); !errors.Is(err, tt.want) { t.Fatalf("ApplyHealth() error = %v, want %v", err, tt.want) } }) } } func TestMemoryPoolRejectsNonPositiveMaxSize(t *testing.T) { t.Parallel() _, err := NewMemoryPool().UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: time.Date(2026, 7, 29, 15, 0, 0, 0, time.UTC), ConfiguredTTL: time.Minute, }) if !errors.Is(err, ErrInvalidBatch) { t.Fatalf("UpsertFetched() error = %v, want ErrInvalidBatch", err) } } func TestMemoryPoolEnforcesMaxSizePerIncumbentUpstream(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 29, 15, 0, 0, 0, time.UTC) pool := NewMemoryPool() result, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: now, ConfiguredTTL: time.Minute, MaxSize: 1, Proxies: []proxyDomain.Proxy{ {ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080}, {ID: "proxy-b", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.11", Port: 8080}, }, }) if err != nil { t.Fatalf("UpsertFetched(): %v", err) } if result.Accepted != 2 || result.Inserted != 1 || result.Dropped != 1 { t.Fatalf("UpsertFetched() = %+v", result) } retry, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: now.Add(time.Second), ConfiguredTTL: time.Minute, MaxSize: 2, Proxies: []proxyDomain.Proxy{ {ID: "proxy-b", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.11", Port: 8080}, }, }) if err != nil || retry.Inserted != 1 || retry.Refreshed != 0 { t.Fatalf("UpsertFetched(capacity retry) = %+v, %v", retry, err) } inventory, err := pool.Inventory(context.Background(), "provider-a", now) if err != nil || inventory.Managed != 2 { t.Fatalf("Inventory() = %+v, %v", inventory, err) } } func TestMemoryPoolInventoryAndSweepExpiredAreBounded(t *testing.T) { t.Parallel() now := time.Date(2026, 7, 29, 15, 0, 0, 0, time.UTC) pool := NewMemoryPool() result, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: now, ConfiguredTTL: time.Second, MaxSize: 2, Proxies: []proxyDomain.Proxy{ {ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080}, {ID: "proxy-b", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.11", Port: 8080}, }, }) if err != nil || result.Inserted != 2 { t.Fatalf("UpsertFetched() = %+v, %v", result, err) } inventory, err := pool.Inventory(context.Background(), "provider-a", now.Add(2*time.Second)) if err != nil || inventory.Managed != 0 { t.Fatalf("Inventory(expired) = %+v, %v", inventory, err) } first, err := pool.SweepExpired(context.Background(), now.Add(2*time.Second), 1) if err != nil || first != 1 { t.Fatalf("SweepExpired(first) = %d, %v", first, err) } second, err := pool.SweepExpired(context.Background(), now.Add(2*time.Second), 1) if err != nil || second != 1 { t.Fatalf("SweepExpired(second) = %d, %v", second, err) } }