proxy-pool/internal/domain/activitypool/activity_contracts_test.go

171 lines
6.8 KiB
Go

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)
}
}