171 lines
6.8 KiB
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)
|
|
}
|
|
}
|