proxy-pool/internal/controller/operations/reader_test.go

131 lines
4.9 KiB
Go

package operations
import (
"context"
"errors"
"testing"
"time"
"proxy-pool/internal/config"
"proxy-pool/internal/controller/provider"
"proxy-pool/internal/domain/activitypool"
)
func TestReaderMapsCurrentUpstreamsToAdminOperationalStatus(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 30, 10, 0, 0, 0, time.UTC)
inventory := &recordingStateInventoryReader{result: []activitypool.StateInventory{
{UpstreamID: "provider-a", Fetched: 3, Checking: 2, Available: 11, Suspect: 1, Draining: 4, Extracted: 8},
{UpstreamID: "provider-b", Available: 7},
}}
reader, err := NewReader(staticConfigurationReader{configuration: &config.Config{
Upstreams: map[string]config.Upstream{"provider-b": {}, "provider-a": {}},
}}, inventory, func() time.Time { return now }, staticProviderStatsReader{result: []provider.Stats{
{UpstreamID: "provider-a", ConsecutiveEmptyFetch: 3, FetchErrorCount: 4},
{UpstreamID: "provider-b", FetchErrorCount: 1},
}})
if err != nil {
t.Fatalf("NewReader() error = %v", err)
}
status, err := reader.ReadOperationalStatus(context.Background())
if err != nil {
t.Fatalf("ReadOperationalStatus() error = %v", err)
}
if len(inventory.upstreamIDs) != 2 || inventory.upstreamIDs[0] != "provider-a" || inventory.upstreamIDs[1] != "provider-b" ||
!inventory.now.Equal(now) {
t.Fatalf("ReadStateInventory() input = %+v at %v", inventory.upstreamIDs, inventory.now)
}
if status.SnapshotVersion != 0 || len(status.Workers) != 0 || len(status.Upstreams) != 2 {
t.Fatalf("operational status shape = %+v", status)
}
first := status.Upstreams[0]
if first.Name != "provider-a" || first.Available != 11 || first.Checking != 2 || first.Suspect != 1 ||
first.Draining != 4 || first.Extracted != 8 || first.ConsecutiveEmptyFetch != 3 || first.FetchErrorCount != 4 {
t.Fatalf("first upstream = %+v", first)
}
if status.Upstreams[1].Name != "provider-b" || status.Upstreams[1].Available != 7 {
t.Fatalf("second upstream = %+v", status.Upstreams[1])
}
}
func TestReaderRejectsMissingOrMalformedDependencies(t *testing.T) {
t.Parallel()
now := func() time.Time { return time.Now().UTC() }
validConfig := staticConfigurationReader{configuration: &config.Config{
Upstreams: map[string]config.Upstream{"provider-a": {}},
}}
validInventory := &recordingStateInventoryReader{result: []activitypool.StateInventory{{UpstreamID: "provider-a"}}}
if _, err := NewReader(nil, validInventory, now); !errors.Is(err, ErrInvalidReader) {
t.Fatalf("NewReader(nil config) error = %v", err)
}
if _, err := NewReader(validConfig, nil, now); !errors.Is(err, ErrInvalidReader) {
t.Fatalf("NewReader(nil inventory) error = %v", err)
}
if _, err := NewReader(validConfig, validInventory, nil); !errors.Is(err, ErrInvalidReader) {
t.Fatalf("NewReader(nil clock) error = %v", err)
}
malformed := &recordingStateInventoryReader{result: []activitypool.StateInventory{{UpstreamID: "wrong"}}}
reader, err := NewReader(validConfig, malformed, now)
if err != nil {
t.Fatalf("NewReader() error = %v", err)
}
if _, err := reader.ReadOperationalStatus(context.Background()); !errors.Is(err, ErrUnavailable) {
t.Fatalf("ReadOperationalStatus(malformed) error = %v", err)
}
}
func TestReaderPreservesCancellationAndClassifiesDependencyFailures(t *testing.T) {
t.Parallel()
dependencyError := errors.New("redis down")
inventory := &recordingStateInventoryReader{err: dependencyError}
reader, err := NewReader(staticConfigurationReader{configuration: &config.Config{
Upstreams: map[string]config.Upstream{"provider-a": {}},
}}, inventory, time.Now)
if err != nil {
t.Fatalf("NewReader() error = %v", err)
}
if _, err := reader.ReadOperationalStatus(context.Background()); !errors.Is(err, ErrUnavailable) || !errors.Is(err, dependencyError) {
t.Fatalf("ReadOperationalStatus(dependency) error = %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := reader.ReadOperationalStatus(ctx); !errors.Is(err, context.Canceled) || errors.Is(err, ErrUnavailable) {
t.Fatalf("ReadOperationalStatus(canceled) error = %v", err)
}
}
type staticConfigurationReader struct {
configuration *config.Config
}
func (reader staticConfigurationReader) Current() *config.Config { return reader.configuration }
type recordingStateInventoryReader struct {
result []activitypool.StateInventory
err error
upstreamIDs []string
now time.Time
}
type staticProviderStatsReader struct {
result []provider.Stats
}
func (reader staticProviderStatsReader) ReadProviderStats([]string) []provider.Stats {
return append([]provider.Stats(nil), reader.result...)
}
func (reader *recordingStateInventoryReader) ReadStateInventory(
_ context.Context,
upstreamIDs []string,
now time.Time,
) ([]activitypool.StateInventory, error) {
reader.upstreamIDs = append([]string(nil), upstreamIDs...)
reader.now = now
return append([]activitypool.StateInventory(nil), reader.result...), reader.err
}