package provider import ( "context" "errors" "testing" "time" "proxy-pool/internal/controller/pool" "proxy-pool/internal/domain/activitypool" proxyDomain "proxy-pool/internal/domain/proxy" ) func TestFleetRunsAllUpstreamsAndCancelsSiblingsOnFailure(t *testing.T) { started := make(chan string, 2) siblingCancelled := make(chan struct{}, 1) first := newFleetRuntime(t, "provider-a", coordinatorFunc(func( context.Context, string, CoordinationLimits, func(context.Context, LeaderSession) error, ) error { started <- "provider-a" return errors.New("provider-a stopped") })) second := newFleetRuntime(t, "provider-b", coordinatorFunc(func( ctx context.Context, _ string, _ CoordinationLimits, _ func(context.Context, LeaderSession) error, ) error { started <- "provider-b" <-ctx.Done() siblingCancelled <- struct{}{} return ctx.Err() })) fleet, err := NewFleet(first, second) if err != nil { t.Fatalf("NewFleet(): %v", err) } err = fleet.Run(context.Background()) if err == nil || err.Error() != "provider-a stopped" { t.Fatalf("Run() error = %v, want provider-a failure", err) } seen := map[string]bool{<-started: true, <-started: true} if !seen["provider-a"] || !seen["provider-b"] { t.Fatalf("started upstreams = %v", seen) } select { case <-siblingCancelled: case <-time.After(time.Second): t.Fatal("sibling runtime was not cancelled") } } func TestNewFleetRejectsEmptyNilAndDuplicateUpstreams(t *testing.T) { valid := newFleetRuntime(t, "provider-a", coordinatorFunc(func( ctx context.Context, _ string, _ CoordinationLimits, _ func(context.Context, LeaderSession) error, ) error { return ctx.Err() })) tests := []struct { name string runtimes []*UpstreamRuntime }{ {name: "empty"}, {name: "nil", runtimes: []*UpstreamRuntime{nil}}, {name: "duplicate", runtimes: []*UpstreamRuntime{valid, valid}}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { fleet, err := NewFleet(test.runtimes...) if err == nil || fleet != nil { t.Fatalf("NewFleet() = (%v, %v), want invalid fleet", fleet, err) } }) } } func newFleetRuntime(t *testing.T, upstreamID string, coordinator Coordinator) *UpstreamRuntime { t.Helper() runtime, err := NewUpstreamRuntime(UpstreamRuntimeConfig{ Provider: runtimeProviderConfig(upstreamID), ReconcilePolicy: runtimeReconcilePolicy(), ReconcileInterval: time.Second, }, UpstreamRuntimeDependencies{ Coordinator: coordinator, Inventory: inventoryReaderFunc(func(context.Context, string, time.Duration) (pool.InventorySnapshot, error) { return pool.InventorySnapshot{}, nil }), Adapter: adapterFunc(func(context.Context) (FetchResponse, error) { return FetchResponse{Body: []byte("fixture")}, nil }), Parser: parserFunc(func(context.Context, []byte) ([]proxyDomain.Proxy, error) { return []proxyDomain.Proxy{{ID: "proxy-1"}}, nil }), Activity: activitySinkFunc(func(context.Context, string, activitypool.FetchedBatch) (activitypool.UpsertResult, error) { return activitypool.UpsertResult{Accepted: 1, Inserted: 1}, nil }), Results: resultRecorderFunc(func(Result) {}), }) if err != nil { t.Fatalf("NewUpstreamRuntime(%s): %v", upstreamID, err) } return runtime }