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 }