The parser will need to move between proxies, so this adds the module it will sit on: pluggable sources, a pool that hands proxies out and learns from the outcome, three rotation strategies, and a page to drive it. Sources are IProxySource implementations. The public proxifly/free-proxy-list feed is fetched as the combined all/data.json through jsDelivr and filtered locally — one conditional request beats four per-protocol ones that can disagree mid-publish — and cached for the five minutes upstream takes to regenerate. A feed that is down keeps serving its last payload rather than emptying the pool. The user's own list lives in proxies.custom.json beside the settings, takes a pasted blob, and names the lines it could not parse instead of quietly dropping them. Both knobs the pool exposes are settings, as asked: rotation is Sticky (default, the only one that keeps site sessions coherent), RoundRobin or WeightedRandom; liveness is either a parallel sweep of the whole pool or a probe at hand-out time. Free lists are a few percent alive, so skipping verification entirely means mostly waiting on timeouts. Two invariants worth keeping, both of which cost a bug to find: Availability is decided by the quarantine, never by Health. Excluding everything that has ever failed made the quarantine window dead code and discarded proxies permanently on their first hiccup, which is exactly wrong for addresses that flap constantly. Health only orders the candidates now. A probe verdict does not touch the success/failure counters. Those are about real requests, and letting a sweep over a few thousand proxies rewrite them would drown the evidence weighted selection reads. SOCKS needs no extra package — .NET resolves socks4/socks4a/socks5 in WebProxy — but a proxifly record with "protocol": "https" is still an HTTP proxy reached over http:// with CONNECT, not an https:// scheme. 115 new tests. Also fixes a pre-existing flake: a command gated on another command's IsExecuting cannot be driven straight after its Execute() completes, because IsExecuting is published on the output scheduler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
2.3 KiB
C#
78 lines
2.3 KiB
C#
using Avalonia.Controls;
|
|
using Avalonia.Headless.XUnit;
|
|
using AvParser.UI;
|
|
using AvParser.UI.ViewModels;
|
|
using AvParser.UI.Views;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace AvParser.UI.HeadlessTests;
|
|
|
|
public class ViewLocatorTests
|
|
{
|
|
private static ViewLocator Locator(Action<IServiceCollection>? configure = null)
|
|
{
|
|
var services = new ServiceCollection();
|
|
configure?.Invoke(services);
|
|
return new ViewLocator(services.BuildServiceProvider());
|
|
}
|
|
|
|
[Fact]
|
|
public void Matches_view_models_only()
|
|
{
|
|
var locator = Locator();
|
|
|
|
locator.Match(new FakePage("x")).ShouldBeTrue();
|
|
locator.Match("a string").ShouldBeFalse();
|
|
locator.Match(null).ShouldBeFalse();
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Resolves_a_view_by_naming_convention()
|
|
{
|
|
var locator = Locator();
|
|
var viewModel = new AboutViewModel(new TestPaths());
|
|
|
|
var view = locator.Build(viewModel);
|
|
|
|
view.ShouldBeOfType<AboutView>();
|
|
view.DataContext.ShouldBeSameAs(viewModel);
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Prefers_a_registered_view_over_activation()
|
|
{
|
|
var registered = new AboutView();
|
|
var locator = Locator(services => services.AddSingleton(registered));
|
|
|
|
var view = locator.Build(new AboutViewModel(new TestPaths()));
|
|
|
|
view.ShouldBeSameAs(registered);
|
|
}
|
|
|
|
[AvaloniaFact]
|
|
public void Reports_a_missing_view_instead_of_throwing()
|
|
{
|
|
var locator = Locator();
|
|
|
|
// "FakePage" matches neither half of the convention, so the locator must report a miss
|
|
// rather than resolving the view model as its own view and failing to activate it.
|
|
var view = locator.Build(new FakePage("x"));
|
|
|
|
view.ShouldBeOfType<TextBlock>().Text!.ShouldContain("View not found");
|
|
}
|
|
|
|
[Fact]
|
|
public void Builds_a_placeholder_for_a_null_view_model() => Locator().Build(null).ShouldBeOfType<TextBlock>();
|
|
|
|
private sealed class TestPaths : Infrastructure.Storage.IAppPaths
|
|
{
|
|
public string DataDirectory => Path.Combine(Path.GetTempPath(), "AvParserTests");
|
|
|
|
public string SettingsFile => Path.Combine(DataDirectory, "settings.json");
|
|
|
|
public string CustomProxiesFile => Path.Combine(DataDirectory, "proxies.custom.json");
|
|
|
|
public string LogDirectory => Path.Combine(DataDirectory, "logs");
|
|
}
|
|
}
|