Files
Leonid Pershin eb5061ee23 Refactor media source handling and update collection options
- Updated `IMediaSourceCatalog` to support user-added media sources, allowing dynamic editing and management of sources.
- Removed the `UrlListSource` class as its functionality is now integrated into the new catalog structure.
- Enhanced `CollectOptions` to default `RequireProxy` to true, ensuring stricter handling of proxy requirements.
- Improved error handling in `ParseError` to include a `Subject` field for better context on failures.
- Adjusted dependency injection to reflect changes in media source management, removing old source registrations.
- Introduced background proxy checks to ensure a more robust proxy pool management during collection processes.

These changes streamline the media collection process and improve the overall user experience by providing clearer error reporting and more flexible source management.
2026-08-15 14:20:06 +03:00

215 lines
7.1 KiB
C#

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) =>
Build(out source, out pool, new MemoryStateStore());
private static ProxyPoolLoader Build(out CountingSource source, out ProxyPool pool, IProxyStateStore stateStore)
{
source = new CountingSource();
pool = new ProxyPool([source], new NeverProbe(), new ProxyOptions());
return new ProxyPoolLoader(pool, stateStore, NullLogger<ProxyPoolLoader>.Instance);
}
[Fact]
public async Task Loading_fills_the_pool()
{
var loader = Build(out _, out var pool);
(await loader.EnsureLoadedAsync()).Total.ShouldBe(2);
pool.Entries.Count.ShouldBe(2);
loader.IsLoaded.ShouldBeTrue();
}
[Fact]
public async Task What_the_previous_run_learned_is_restored_onto_the_fresh_list()
{
// The feed republishes the same addresses every few minutes; the point of remembering is
// that a proxy known to work is not re-discovered from scratch on every launch.
var stateStore = new MemoryStateStore
{
State =
{
["http://1.2.3.4:8080"] = new ProxyStateRecord("http://1.2.3.4:8080", Alive: true, LatencyMs: 120),
},
};
var loader = Build(out _, out _, stateStore);
var result = await loader.EnsureLoadedAsync();
result.Restored.ShouldBe(1);
}
[Fact]
public async Task The_pool_state_is_written_back_after_a_load()
{
var stateStore = new MemoryStateStore();
var loader = Build(out _, out _, stateStore);
await loader.EnsureLoadedAsync();
stateStore.Saves.ShouldBeGreaterThan(0);
}
[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, new MemoryStateStore(), NullLogger<ProxyPoolLoader>.Instance);
// The app has to start whether or not a public list is reachable.
(await loader.EnsureLoadedAsync()).Total.ShouldBe(0);
}
[Fact]
public async Task What_the_warm_up_skipped_is_checked_in_the_background()
{
// The warm-up stops at the target, which on a real feed leaves thousands unknown. Without
// this pass the pool reports ten live out of a few thousand and the rest were never asked.
var source = new CountingSource();
var pool = new ProxyPool(
[source],
new AliveProbe(),
new ProxyOptions { MinimumLiveProxies = 1, ProbeConcurrency = 1 }
);
using var loader = new ProxyPoolLoader(pool, new MemoryStateStore(), NullLogger<ProxyPoolLoader>.Instance);
var result = await loader.EnsureLoadedAsync();
// One live was enough to finish starting up; the other is still unknown at this point.
result.Live.ShouldBe(1);
await loader.TopUp.ShouldNotBeNull();
pool.LiveCount.ShouldBe(2);
loader.IsToppingUp.ShouldBeFalse();
}
[Fact]
public async Task The_background_check_is_skipped_when_the_user_asked_for_lazy_probing()
{
// Lazy means "check a proxy when you hand it out"; sweeping the list behind the user's back
// is exactly what they switched off.
var pool = new ProxyPool(
[new CountingSource()],
new AliveProbe(),
new ProxyOptions { HealthCheck = ProxyHealthCheck.Lazy }
);
using var loader = new ProxyPoolLoader(pool, new MemoryStateStore(), NullLogger<ProxyPoolLoader>.Instance);
await loader.EnsureLoadedAsync();
loader.TopUp.ShouldBeNull();
}
[Fact]
public async Task Disposing_twice_is_safe()
{
var loader = Build(out _, out _);
await loader.EnsureLoadedAsync();
loader.Dispose();
Should.NotThrow(loader.Dispose);
}
private sealed class MemoryStateStore : IProxyStateStore
{
public Dictionary<string, ProxyStateRecord> State { get; } = new(StringComparer.Ordinal);
public int Saves { get; private set; }
public Task<IReadOnlyDictionary<string, ProxyStateRecord>> LoadAsync(
CancellationToken cancellationToken = default
) => Task.FromResult<IReadOnlyDictionary<string, ProxyStateRecord>>(State);
public Task SaveAsync(IEnumerable<ProxyEntry> entries, CancellationToken cancellationToken = default)
{
Saves++;
return Task.CompletedTask;
}
}
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<IReadOnlyList<ProxyEndpoint>> GetProxiesAsync(CancellationToken cancellationToken = default)
{
Calls++;
return Task.FromResult<IReadOnlyList<ProxyEndpoint>>([
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<IReadOnlyList<ProxyEndpoint>> GetProxiesAsync(CancellationToken cancellationToken = default) =>
throw new HttpRequestException("upstream is down");
}
private sealed class NeverProbe : IProxyProbe
{
public Task<ProxyProbeResult> ProbeAsync(
ProxyEndpoint endpoint,
ProxyOptions options,
CancellationToken cancellationToken = default
) => Task.FromResult(ProxyProbeResult.Failure("not used"));
}
private sealed class AliveProbe : IProxyProbe
{
public Task<ProxyProbeResult> ProbeAsync(
ProxyEndpoint endpoint,
ProxyOptions options,
CancellationToken cancellationToken = default
) => Task.FromResult(ProxyProbeResult.Success(TimeSpan.FromMilliseconds(15)));
}
}