using AvParser.Core.Proxies; using AvParser.Infrastructure.Proxies; using Microsoft.Extensions.Logging.Abstractions; namespace AvParser.Infrastructure.Tests; public class ProxyPoolLoaderTests { private static ProxyPoolLoader Build(out CountingSource source, out ProxyPool pool) { source = new CountingSource(); pool = new ProxyPool([source], new NeverProbe(), new ProxyOptions()); return new ProxyPoolLoader(pool, NullLogger.Instance); } [Fact] public async Task Loading_fills_the_pool() { var loader = Build(out _, out var pool); (await loader.EnsureLoadedAsync()).ShouldBe(2); pool.Entries.Count.ShouldBe(2); loader.IsLoaded.ShouldBeTrue(); } [Fact] public async Task The_feed_is_fetched_once_however_many_callers_there_are() { var loader = Build(out var source, out _); // Startup and the proxy page both ask; a second download of a 600 KB list would be waste. await Task.WhenAll(loader.EnsureLoadedAsync(), loader.EnsureLoadedAsync(), loader.EnsureLoadedAsync()); await loader.EnsureLoadedAsync(); source.Calls.ShouldBe(1); } [Fact] public async Task Concurrent_callers_share_one_operation() { var loader = Build(out _, out _); var first = loader.EnsureLoadedAsync(); var second = loader.EnsureLoadedAsync(); first.ShouldBeSameAs(second); await first; } [Fact] public async Task A_source_that_throws_does_not_take_startup_down() { var pool = new ProxyPool([new ThrowingSource()], new NeverProbe(), new ProxyOptions()); var loader = new ProxyPoolLoader(pool, NullLogger.Instance); // The app has to start whether or not a public list is reachable. (await loader.EnsureLoadedAsync()).ShouldBe(0); } private sealed class CountingSource : IProxySource { public int Calls { get; private set; } public string Id => "counting"; public string DisplayName => "Counting source"; public ProxySourceKind Kind => ProxySourceKind.Feed; public Task> GetProxiesAsync(CancellationToken cancellationToken = default) { Calls++; return Task.FromResult>([ new ProxyEndpoint(ProxyProtocol.Http, "1.2.3.4", 8080), new ProxyEndpoint(ProxyProtocol.Socks5, "5.6.7.8", 1080), ]); } } private sealed class ThrowingSource : IProxySource { public string Id => "throwing"; public string DisplayName => "Throwing source"; public ProxySourceKind Kind => ProxySourceKind.Feed; public Task> GetProxiesAsync(CancellationToken cancellationToken = default) => throw new HttpRequestException("upstream is down"); } private sealed class NeverProbe : IProxyProbe { public Task ProbeAsync( ProxyEndpoint endpoint, ProxyOptions options, CancellationToken cancellationToken = default ) => Task.FromResult(ProxyProbeResult.Failure("not used")); } }