138 lines
4.5 KiB
C#
138 lines
4.5 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using PLib.Infrastructure.Metadata;
|
|
using PLib.Infrastructure.Storage;
|
|
using Shouldly;
|
|
|
|
namespace PLib.Tests.Metadata;
|
|
|
|
/// <summary>
|
|
/// A picture host that will not answer must cost a few attempts, not one per candidate.
|
|
/// </summary>
|
|
public sealed class RemoteImageCacheTests : IDisposable
|
|
{
|
|
private readonly TempPaths _paths = new();
|
|
private readonly CountingHandler _handler = new();
|
|
|
|
public void Dispose()
|
|
{
|
|
_paths.Dispose();
|
|
_handler.Dispose();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_host_that_keeps_failing_is_left_alone_after_a_few_tries()
|
|
{
|
|
var cache = Create();
|
|
var problems = new List<string>();
|
|
|
|
for (var attempt = 0; attempt < 6; attempt++)
|
|
{
|
|
var image = await cache.GetOrCreateAsync($"https://cdn.example/{attempt}.jpg", Token);
|
|
|
|
image.Path.ShouldBeNull();
|
|
|
|
if (image.Problem is { } problem)
|
|
{
|
|
problems.Add(problem);
|
|
}
|
|
}
|
|
|
|
// A library-wide run produces a cover per candidate, all from the same host. Without a
|
|
// cut-off, a host that answers its headers and then stalls holds a connection open for
|
|
// the full timeout on every single one of them.
|
|
_handler.Requests.ShouldBe(3);
|
|
|
|
// Said once, at the moment the host is given up on. Every miss would put the same line
|
|
// beside every candidate; never saying it leaves rows of empty squares unexplained.
|
|
problems.ShouldHaveSingleItem().ShouldContain("cdn.example");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_different_host_is_judged_on_its_own_behaviour()
|
|
{
|
|
var cache = Create();
|
|
|
|
for (var attempt = 0; attempt < 4; attempt++)
|
|
{
|
|
await cache.GetOrCreateAsync($"https://broken.example/{attempt}.jpg", Token);
|
|
}
|
|
|
|
await cache.GetOrCreateAsync("https://other.example/a.jpg", Token);
|
|
|
|
// Three to the broken host, then it is skipped; the fourth request is the other host.
|
|
_handler.Requests.ShouldBe(4);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_address_that_is_not_a_web_address_is_never_fetched()
|
|
{
|
|
var cache = Create();
|
|
|
|
// The URL comes from somebody else's server, so file:// would turn "fetch a picture"
|
|
// into "read a path of the server's choosing".
|
|
(await cache.GetOrCreateAsync(@"file:///C:/Windows/win.ini", Token)).Path.ShouldBeNull();
|
|
(await cache.GetOrCreateAsync("не адрес", Token)).Path.ShouldBeNull();
|
|
|
|
_handler.Requests.ShouldBe(0);
|
|
}
|
|
|
|
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
|
|
|
private HttpRemoteImageCache Create()
|
|
{
|
|
var factory = Substitute.For<IHttpClientFactory>();
|
|
factory.CreateClient(Arg.Any<string>()).Returns(_ => new HttpClient(_handler, disposeHandler: false));
|
|
|
|
return new HttpRemoteImageCache(factory, _paths, NullLogger<HttpRemoteImageCache>.Instance);
|
|
}
|
|
|
|
/// <summary>Fails every request at once, and counts how many it was asked to make.</summary>
|
|
private sealed class CountingHandler : HttpMessageHandler
|
|
{
|
|
private int _requests;
|
|
|
|
public int Requests => _requests;
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Interlocked.Increment(ref _requests);
|
|
throw new HttpRequestException("no route to host");
|
|
}
|
|
}
|
|
|
|
private sealed class TempPaths : IAppPaths, IDisposable
|
|
{
|
|
public TempPaths()
|
|
{
|
|
DataDirectory = Path.Combine(Path.GetTempPath(), $"plib-images-{Guid.CreateVersion7()}");
|
|
ThumbnailDirectory = Path.Combine(DataDirectory, "thumbnails");
|
|
PreviewDirectory = Path.Combine(DataDirectory, "previews");
|
|
RemoteImageDirectory = Path.Combine(DataDirectory, "images");
|
|
DatabaseFile = Path.Combine(DataDirectory, "library.db");
|
|
|
|
Directory.CreateDirectory(RemoteImageDirectory);
|
|
}
|
|
|
|
public string DataDirectory { get; }
|
|
|
|
public string ThumbnailDirectory { get; }
|
|
|
|
public string PreviewDirectory { get; }
|
|
|
|
public string RemoteImageDirectory { get; }
|
|
|
|
public string DatabaseFile { get; }
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(DataDirectory))
|
|
{
|
|
Directory.Delete(DataDirectory, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
}
|