Files
av-parser/tests/AvParser.Infrastructure.Tests/JsonSettingsServiceTests.cs
T
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

229 lines
7.6 KiB
C#

using AvParser.Core.Collecting;
using AvParser.Core.Proxies;
using AvParser.Core.Settings;
using AvParser.Infrastructure.Settings;
using AvParser.Infrastructure.Storage;
using Microsoft.Extensions.Logging.Abstractions;
namespace AvParser.Infrastructure.Tests;
public sealed class JsonSettingsServiceTests : IDisposable
{
private readonly string _directory = Path.Combine(
Path.GetTempPath(),
"AvParserTests",
Guid.NewGuid().ToString("N")
);
private JsonSettingsService Create() => new(new AppPaths(_directory), NullLogger<JsonSettingsService>.Instance);
private void WriteSettings(string json)
{
Directory.CreateDirectory(_directory);
File.WriteAllText(Path.Combine(_directory, "settings.json"), json);
}
public void Dispose()
{
if (Directory.Exists(_directory))
{
Directory.Delete(_directory, recursive: true);
}
}
[Fact]
public void An_absent_file_yields_the_defaults()
{
using var service = Create();
service.Current.ShouldBe(new AppSettings());
}
/// <summary>
/// A settings file written before a setting existed must not zero that setting.
/// </summary>
/// <remarks>
/// This is a regression test with a real bug behind it. Defaults used to live on property
/// initialisers, which the source-generated deserialiser does not run — so an older file came
/// back with <c>default(T)</c> everywhere, switching the proxy feed off, clearing the protocol
/// filter and zeroing the probe timeout. The app then loaded an empty pool and looked as if
/// the network had failed.
/// </remarks>
[Fact]
public void A_file_from_an_older_version_keeps_the_defaults_for_settings_it_predates()
{
WriteSettings(
"""
{
"theme": "System",
"windowWidth": 1280,
"windowHeight": 800,
"windowMaximized": false,
"minimumLogLevel": "Information"
}
"""
);
using var service = Create();
var settings = service.Current;
settings.ProxyUseFeed.ShouldBeTrue();
settings.ProxyProtocols.ShouldBe(ProxyProtocolFilter.All);
settings.ProxyProbeTimeoutSeconds.ShouldBe(8);
settings.ProxyProbeConcurrency.ShouldBe(64);
settings.ProxyProbeUrl.ShouldNotBeNullOrEmpty();
settings.Language.ShouldBe(AppLanguage.System);
// And the values the file did carry must survive.
settings.MinimumLogLevel.ShouldBe("Information");
}
/// <summary>
/// The same guarantee, for the collector settings added afterwards.
/// </summary>
/// <remarks>
/// The failure this prevents is worse here than it was for the proxy pool: a zeroed
/// <c>MaxItemBytes</c> would refuse everything, and a zeroed concurrency would deadlock the
/// run outright.
/// </remarks>
[Fact]
public void A_file_that_predates_the_collector_keeps_the_collector_defaults()
{
WriteSettings("""{ "theme": "Dark", "proxyMinimumLive": 25 }""");
using var service = Create();
var settings = service.Current;
settings.MaxConcurrentDownloads.ShouldBe(4);
settings.MaxConcurrentPerHost.ShouldBe(2);
settings.HostDelayMs.ShouldBe(250);
settings.MaxItemBytes.ShouldBe(33_554_432);
settings.MinItemBytes.ShouldBe(1024);
settings.MaxRedirects.ShouldBe(5);
settings.ConnectTimeoutSeconds.ShouldBe(15);
settings.HeaderTimeoutSeconds.ShouldBe(30);
settings.IdleTimeoutSeconds.ShouldBe(20);
settings.AllowedMediaKinds.ShouldBe(MediaKindFilter.All);
settings.ShowcaseMode.ShouldBe(ShowcaseMode.HardLink);
settings.CollectUserAgent.ShouldNotBeNullOrWhiteSpace();
// And what the file did carry survives.
settings.ProxyMinimumLive.ShouldBe(25);
}
[Fact]
public void Collect_options_from_an_older_file_are_usable()
{
WriteSettings("""{ "theme": "Dark" }""");
using var service = Create();
var options = service.Current.ToCollectOptions();
options.MaxConcurrentDownloads.ShouldBeGreaterThan(0);
options.MaxItemBytes.ShouldBeGreaterThan(0);
options.AllowedKinds.ShouldBe(MediaKindFilter.All);
options.IdleTimeout.ShouldBeGreaterThan(TimeSpan.Zero);
}
[Fact]
public void Collector_values_out_of_range_are_clamped_rather_than_thrown()
{
var options = new AppSettings(
MaxConcurrentDownloads: 0,
MaxConcurrentPerHost: -5,
MaxItemBytes: 0,
MaxRedirects: 9999,
IdleTimeoutSeconds: 0,
CollectUserAgent: " "
).ToCollectOptions();
options.MaxConcurrentDownloads.ShouldBe(1);
options.MaxConcurrentPerHost.ShouldBe(1);
options.MaxItemBytes.ShouldBe(1024);
options.MaxRedirects.ShouldBe(20);
options.IdleTimeout.ShouldBe(TimeSpan.FromSeconds(1));
options.UserAgent.ShouldBe("AvParser/0.1");
}
[Fact]
public void An_empty_media_filter_is_treated_as_everything()
{
// Switching every format off is far more likely to be an accident than an instruction to
// collect nothing at all.
new AppSettings(AllowedMediaKinds: MediaKindFilter.None)
.ToCollectOptions()
.AllowedKinds.ShouldBe(MediaKindFilter.All);
}
[Fact]
public void The_collector_defaults_to_proxy_only()
{
// Whether a source may go direct is that source's own setting now; what the app-wide
// options must never do is default to the permissive answer.
new AppSettings()
.ToCollectOptions()
.RequireProxy.ShouldBeTrue();
}
[Fact]
public void Options_built_from_an_older_file_still_consult_the_feed()
{
WriteSettings("""{ "theme": "Dark" }""");
using var service = Create();
var options = service.Current.ToProxyOptions();
options.UseFeed.ShouldBeTrue();
options.Protocols.ShouldBe(ProxyProtocolFilter.All);
}
[Fact]
public void An_empty_protocol_filter_is_treated_as_all()
{
// Nothing in the UI can produce None, but a hand-edited file can — and a pool that
// silently matches nothing is the least useful possible reading of it.
var options = new AppSettings(ProxyProtocols: ProxyProtocolFilter.None).ToProxyOptions();
options.Protocols.ShouldBe(ProxyProtocolFilter.All);
}
[Fact]
public void Out_of_range_values_are_clamped_rather_than_thrown()
{
var options = new AppSettings(
ProxyProbeTimeoutSeconds: 0,
ProxyProbeConcurrency: 100_000,
ProxyProbeUrl: "not a url"
).ToProxyOptions();
options.ProbeTimeout.ShouldBe(TimeSpan.FromSeconds(1));
options.ProbeConcurrency.ShouldBe(512);
options.ProbeUrl.ShouldBe(new ProxyOptions().ProbeUrl);
}
[Fact]
public void A_corrupt_file_falls_back_to_the_defaults_instead_of_failing_to_start()
{
WriteSettings("{ this is not json");
using var service = Create();
service.Current.ShouldBe(new AppSettings());
}
[Fact]
public async Task Updates_round_trip_through_the_file()
{
using (var service = Create())
{
service.Update(current => current with { Theme = AppTheme.Dark, ProxyUseFeed = false });
await service.FlushAsync(TestContext.Current.CancellationToken);
}
using var reopened = Create();
reopened.Current.Theme.ShouldBe(AppTheme.Dark);
reopened.Current.ProxyUseFeed.ShouldBeFalse();
}
}