Files
av-parser/tests/AvParser.UI.HeadlessTests/ProxiesViewTests.cs
T
Leonid PershinandClaude Opus 5 44fb0d3a5f Gate network parsers on a working proxy and remember what worked
The pool now warms up from what the previous run learned instead of starting
cold every launch. Startup probes the remembered proxies first, stops as soon
as ProxyMinimumLive of them answer, and writes the survivors to
proxies.state.json after the warm-up and again on shutdown. Only proxies that
ever answered are stored: the feed republishes a few thousand dead addresses
every five minutes, and "was dead an hour ago" says almost nothing.

Remembered state is a hint, not a verdict. A restored proxy sorts first in the
warm-up queue but is not counted live until it answers in this session -
otherwise a launch a week later would report live proxies it had never spoken
to, the warm-up would skip the very entries it exists to re-check, and the
parser gate would open on week-old evidence.

That gate is the other half: a parser declaring RequiresNetwork will not run
while the pool has nothing live. The Parse page disables the run button and
shows a banner that leads to the Proxies page. Parsers that work on pasted text
are never gated - they have nothing to route, and blocking them would make the
app useless whenever the public lists are down. Two new settings cover the
escape hatch and the target: "allow network parsers without a proxy" and how
many live proxies to find at startup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 19:11:51 +03:00

84 lines
2.5 KiB
C#

using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AvParser.Core.Proxies;
using AvParser.Infrastructure.Proxies;
using AvParser.UI.ViewModels;
using AvParser.UI.Views;
using Microsoft.Extensions.Logging.Abstractions;
using ReactiveUI.Primitives.Concurrency;
namespace AvParser.UI.HeadlessTests;
public class ProxiesViewTests
{
private static (ProxiesView View, ProxiesViewModel ViewModel, Window Window) ShowPage(
params ProxyEndpoint[] endpoints
)
{
var custom = new FakeMutableProxySource();
custom.Endpoints.AddRange(endpoints);
var pool = new ProxyPool([custom], new FakeProxyProbe(), new ProxyOptions());
var viewModel = new ProxiesViewModel(
pool,
custom,
new FakeProxyPoolLoader(),
NullLogger<ProxiesViewModel>.Instance,
ImmediateSequencer.Instance
);
var view = new ProxiesView { DataContext = viewModel };
var window = new Window
{
Width = 1400,
Height = 800,
Content = view,
};
window.Show();
Dispatcher.UIThread.RunJobs();
return (view, viewModel, window);
}
[AvaloniaFact]
public void The_page_renders_with_an_empty_pool()
{
var (view, _, _) = ShowPage();
view.GetVisualDescendants().OfType<ListBox>().ShouldNotBeEmpty();
}
[AvaloniaFact]
public async Task Refreshing_puts_rows_on_screen()
{
var (view, viewModel, _) = ShowPage(
new ProxyEndpoint(ProxyProtocol.Socks5, "10.0.0.1", 1080),
new ProxyEndpoint(ProxyProtocol.Http, "10.0.0.2", 8080)
);
await viewModel.RefreshCommand.Execute().ToTask(TestContext.Current.CancellationToken);
Dispatcher.UIThread.RunJobs();
var list = view.GetVisualDescendants().OfType<ListBox>().First();
list.ItemCount.ShouldBe(2);
viewModel.TotalCount.ShouldBe(2);
}
[AvaloniaFact]
public async Task Rows_carry_the_address_and_the_source()
{
var (_, viewModel, _) = ShowPage(new ProxyEndpoint(ProxyProtocol.Socks5, "10.0.0.1", 1080));
await viewModel.RefreshCommand.Execute().ToTask(TestContext.Current.CancellationToken);
Dispatcher.UIThread.RunJobs();
var row = viewModel.Proxies.ShouldHaveSingleItem();
row.Address.ShouldBe("socks5://10.0.0.1:1080");
row.Source.ShouldBe("custom");
row.HealthText.ShouldBe("unchecked");
}
}