mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 18:58:59 -04:00
1031 lines
42 KiB
C#
1031 lines
42 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Net;
|
|
using Cleanuparr.Domain.Entities.Arr;
|
|
using Cleanuparr.Domain.Entities.Arr.ManualImport;
|
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
|
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Infrastructure.Events.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.Arr.ForceImport;
|
|
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.Context;
|
|
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
|
using Cleanuparr.Infrastructure.Helpers;
|
|
using Cleanuparr.Infrastructure.Interceptors;
|
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
|
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Time.Testing;
|
|
using NSubstitute;
|
|
using NSubstitute.ClearExtensions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace Cleanuparr.Infrastructure.Tests.Features.Arr;
|
|
|
|
[Collection(ForceImportDryRunCollection.Name)]
|
|
public class ForceImportServiceTests : IDisposable
|
|
{
|
|
/// <summary>One of the reasons the service treats as safe to force past.</summary>
|
|
private const string SafeReason = "Unable to determine if file is a sample";
|
|
|
|
private readonly IArrClient _arrClient;
|
|
private readonly IStriker _striker;
|
|
private readonly IEventPublisher _eventPublisher;
|
|
private readonly IMemoryCache _cache;
|
|
private readonly FakeTimeProvider _timeProvider;
|
|
private readonly IDryRunInterceptor _dryRunInterceptor;
|
|
private readonly ForceImportService _sut;
|
|
private readonly ArrInstance _instance;
|
|
|
|
/// <summary>How many imports the arr has recorded, which is what its history reports.</summary>
|
|
private int _importedByTheArr;
|
|
|
|
public ForceImportServiceTests()
|
|
{
|
|
_arrClient = Substitute.For<IArrClient>();
|
|
_striker = Substitute.For<IStriker>();
|
|
_eventPublisher = Substitute.For<IEventPublisher>();
|
|
_cache = new MemoryCache(new MemoryCacheOptions());
|
|
_timeProvider = new FakeTimeProvider();
|
|
_dryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
|
_dryRunInterceptor.IsDryRunEnabled().Returns(false);
|
|
_sut = new ForceImportService(
|
|
Substitute.For<ILogger<ForceImportService>>(), _cache, _striker, _eventPublisher, _timeProvider,
|
|
_dryRunInterceptor);
|
|
|
|
_instance = new ArrInstance
|
|
{
|
|
Name = "sonarr",
|
|
Url = new Uri("http://localhost:8989/"),
|
|
ApiKey = "api-key",
|
|
ArrConfig = new ArrConfig { Type = InstanceType.Sonarr },
|
|
};
|
|
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>())
|
|
.Returns(_ =>
|
|
{
|
|
_importedByTheArr++;
|
|
return Task.FromResult(true);
|
|
});
|
|
|
|
_arrClient.SupportsForceImport.Returns(true);
|
|
_arrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
_arrClient.GetCommandsAsync(Arg.Any<ArrInstance>()).Returns([]);
|
|
// The arr records one import per asked file, unless a test says otherwise.
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns(_ => _importedByTheArr);
|
|
_arrClient.MapCandidate(Arg.Any<QueueRecord>(), Arg.Any<ManualImportCandidate>())
|
|
.Returns(new ManualImportFile { Path = "/downloads/show.mkv", SeriesId = 7, EpisodeIds = [9] });
|
|
|
|
SetConfig();
|
|
}
|
|
|
|
// Resets the static dry-run eviction token between tests.
|
|
public void Dispose() => ForceImportService.ForgetDryRun();
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_AsksTheArrAndSaysNothingYet()
|
|
{
|
|
// Arrange: importBlocked is a settled state, so one run is enough
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: an accepted request is not an import, so nothing is announced yet
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Is<List<ManualImportFile>>(files => files.Count == 1));
|
|
await _striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_Disabled_DoesNothing()
|
|
{
|
|
// Arrange
|
|
SetConfig(forceImport: false);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_UnsupportedArr_DoesNothing()
|
|
{
|
|
// Arrange
|
|
_arrClient.SupportsForceImport.Returns(false);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("downloading", "warning")]
|
|
[InlineData("importBlocked", "ok")]
|
|
public async Task TryImportAsync_NotABlockedImport_DoesNothing(string state, string status)
|
|
{
|
|
// Arrange
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: state, status: status));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_AnUnmatchedMessage_DoesNothing()
|
|
{
|
|
// Arrange: one unsafe reason is enough to leave it alone
|
|
QueueRecord record = BuildRecord(state: "importBlocked", messages: [SafeReason, "Not an upgrade for existing episode file"]);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_NoContentId_DoesNothing()
|
|
{
|
|
// Arrange: a zero content id would match a candidate carrying a zero id
|
|
_arrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(false);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().GetManualImportCandidatesAsync(Arg.Any<ArrInstance>(), Arg.Any<string>());
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_NoStatusMessage_DoesNothing()
|
|
{
|
|
// Arrange: the arr reports a warning record with nothing under it
|
|
QueueRecord record = BuildRecord(state: "importBlocked", withStatusMessage: false);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_AStatusMessageWithoutReasons_DoesNothing()
|
|
{
|
|
// Arrange: the arr states a block on the whole download as a bare title
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
record.StatusMessages!.Add(new TrackedDownloadStatusMessage
|
|
{
|
|
Title = "One or more episodes expected in this release were not imported or missing",
|
|
Messages = [],
|
|
});
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("importPending")]
|
|
[InlineData("importFailed")]
|
|
public async Task TryImportAsync_TransitionalState_WaitsForASecondSighting(string state)
|
|
{
|
|
// Arrange: the arr's own importer may still pick up a transitional download
|
|
QueueRecord record = BuildRecord(state: state);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome firstRun = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome secondRun = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
firstRun.ShouldBe(ForceImportOutcome.Deferred);
|
|
secondRun.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("RefreshMonitoredDownloads")]
|
|
[InlineData("ManualImport")]
|
|
[InlineData("RescanSeries")]
|
|
[InlineData("RenameFiles")]
|
|
[InlineData("RenameSeries")]
|
|
[InlineData("RenameMovie")]
|
|
public async Task TryImportAsync_AFileMovingCommandInFlight_Defers(string command)
|
|
{
|
|
// Arrange: a manual import call during a move reads the files mid-move
|
|
_arrClient.GetCommandsAsync(_instance).Returns([
|
|
new ArrCommandStatus(1, ArrCommandState.Started, null, command),
|
|
]);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_UnrelatedCommandInFlight_AsksTheArr()
|
|
{
|
|
// Arrange
|
|
_arrClient.GetCommandsAsync(_instance).Returns([
|
|
new ArrCommandStatus(1, ArrCommandState.Started, null, "EpisodeSearch"),
|
|
]);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_CommandListUnavailable_Defers()
|
|
{
|
|
// Arrange: without the command list there is no proof the arr is idle
|
|
_arrClient.GetCommandsAsync(_instance).Returns<List<ArrCommandStatus>>(_ => throw new HttpRequestException("down"));
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_NoCandidate_DoesNothing()
|
|
{
|
|
// Arrange
|
|
StubCandidates();
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_UnexpectedRejection_ImportsNothing()
|
|
{
|
|
// Arrange: a rejection outside the safe list is a human's call
|
|
StubCandidates(BuildCandidate(SafeReason, "Episode 1x02 was not found in the grabbed release"));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_MappingMismatch_ImportsNothing()
|
|
{
|
|
// Arrange: the file belongs to other content than the queue item
|
|
_arrClient.MapCandidate(Arg.Any<QueueRecord>(), Arg.Any<ManualImportCandidate>()).Returns((ManualImportFile?)null);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_OneFileOfManyMapsElsewhere_ImportsNothing()
|
|
{
|
|
// Arrange
|
|
ManualImportCandidate mapped = BuildCandidate(SafeReason);
|
|
ManualImportCandidate unmapped = BuildCandidate(SafeReason);
|
|
_arrClient.MapCandidate(Arg.Any<QueueRecord>(), unmapped).Returns((ManualImportFile?)null);
|
|
StubCandidates(mapped, unmapped);
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheArrWasNotReached_Defers()
|
|
{
|
|
// Arrange: an unreachable arr is transient, so the download waits instead of collecting a strike
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>())
|
|
.Returns(Task.FromException<bool>(new HttpRequestException("boom")));
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheArrWasNotReached_CostsNoTry()
|
|
{
|
|
// Arrange
|
|
SetConfig(maxTries: 1);
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>())
|
|
.Returns(Task.FromException<bool>(new HttpRequestException("down")));
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
|
|
// Act
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: a request that never landed keeps the tries intact
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(3).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheArrRefusedTheImport_SpendsTheTry()
|
|
{
|
|
// Arrange: an arr that keeps refusing must not hold the download out of the strike path forever
|
|
SetConfig(maxTries: 1);
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>())
|
|
.Returns(Task.FromException<bool>(new HttpRequestException("bad request", null, HttpStatusCode.BadRequest)));
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
|
|
// Act
|
|
ForceImportOutcome first = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome second = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
first.ShouldBe(ForceImportOutcome.Deferred);
|
|
second.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheArrRefusedALaterTry_StillReportsTheAcceptedOne()
|
|
{
|
|
// Arrange: try 1 lands, try 2 is refused
|
|
SetConfig(maxTries: 2);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>())
|
|
.Returns(Task.FromException<bool>(new HttpRequestException("bad request", null, HttpStatusCode.BadRequest)));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert: the refused try leaves try 1's file count untouched
|
|
await _eventPublisher.Received(1).PublishForceImported(record.Title, record.DownloadId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheDownloadStaysBlocked_AsksAgainUpToTheLimit()
|
|
{
|
|
// Arrange
|
|
SetConfig(maxTries: 2);
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome first = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome second = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome third = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: the strike path takes over once the tries run out
|
|
first.ShouldBe(ForceImportOutcome.Deferred);
|
|
second.ShouldBe(ForceImportOutcome.Deferred);
|
|
third.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.Received(2).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_OutOfTries_StaysOutOfTheWay()
|
|
{
|
|
// Arrange
|
|
SetConfig(maxTries: 1);
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_ADeferredRunCostsNoTry()
|
|
{
|
|
// Arrange: the arr is busy, so nothing is asked of it
|
|
SetConfig(maxTries: 1);
|
|
_arrClient.GetCommandsAsync(_instance).Returns([
|
|
new ArrCommandStatus(1, ArrCommandState.Started, null, "ManualImport"),
|
|
]);
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_arrClient.GetCommandsAsync(_instance).Returns([]);
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_DryRunTurnsOnMidRun_RegistersNoPendingAndSpendsNoTry()
|
|
{
|
|
// Arrange: dry run turns on after the service reads it
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>()).Returns(false);
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: no try spent, no pending import
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
_cache.TryGetValue(CacheKeys.ForceImportTries(record.DownloadId, _instance.Url), out int _).ShouldBeFalse();
|
|
_cache.TryGetValue(CacheKeys.ForceImportPending(_instance.Url), out ConcurrentDictionary<string, PendingForceImport>? pending);
|
|
(pending is null || pending.IsEmpty).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_DryRun_SpendsADryTry()
|
|
{
|
|
// Arrange
|
|
EnableDryRun();
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: dry run spends its own budget, live budget stays empty
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(1).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
_cache.TryGetValue(CacheKeys.ForceImportTries(record.DownloadId, _instance.Url, true), out int dryTries).ShouldBeTrue();
|
|
dryTries.ShouldBe(1);
|
|
_cache.TryGetValue(CacheKeys.ForceImportTries(record.DownloadId, _instance.Url), out int _).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_DryRun_LeavesTheFullBudgetForALiveRun()
|
|
{
|
|
// Arrange
|
|
SetConfig(maxTries: 2);
|
|
EnableDryRun();
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_dryRunInterceptor.IsDryRunEnabled().Returns(false);
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>()).Returns(true);
|
|
|
|
// Act
|
|
ForceImportOutcome first = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome second = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome third = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: the dry run leaves the live try budget untouched
|
|
first.ShouldBe(ForceImportOutcome.Deferred);
|
|
second.ShouldBe(ForceImportOutcome.Deferred);
|
|
third.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_DryRun_AtTheTryLimit_SetsTheDryGiveUpMarker()
|
|
{
|
|
// Arrange: the dry run spends its only try
|
|
SetConfig(maxTries: 1);
|
|
EnableDryRun();
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act: a second dry run hits the try limit and gives up
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: dry run gives up and sets its own give-up marker
|
|
outcome.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
_cache.TryGetValue(CacheKeys.ForceImportGaveUp(record.DownloadId, _instance.Url, true), out DateTimeOffset _)
|
|
.ShouldBeTrue();
|
|
|
|
// Dry run leaves the live marker alone
|
|
_cache.TryGetValue(CacheKeys.ForceImportGaveUp(record.DownloadId, _instance.Url), out DateTimeOffset _)
|
|
.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_DryRun_ForgetDryRun_ClearsGiveUpMarkerAndTriesAgain()
|
|
{
|
|
// Arrange: the dry run gives up
|
|
SetConfig(maxTries: 1);
|
|
EnableDryRun();
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
ForceImportOutcome gaveUp = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
gaveUp.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
|
|
// Act: turning dry run off drops the dry budget and its give-up marker
|
|
ForceImportService.ForgetDryRun();
|
|
|
|
// Assert: marker gone, a fresh dry run tries again
|
|
_cache.TryGetValue(CacheKeys.ForceImportGaveUp(record.DownloadId, _instance.Url, true), out DateTimeOffset _)
|
|
.ShouldBeFalse();
|
|
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_DryRun_RegistersNoPendingImport()
|
|
{
|
|
// Arrange
|
|
EnableDryRun();
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act: the download leaves the queue and the arr's history has grown
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert: no arr call means no import gets claimed
|
|
await _striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_ForgetDryRunRacesTheAttempt_EvictsTheDryTryWrite()
|
|
{
|
|
// Arrange: a purge fires mid-attempt
|
|
EnableDryRun();
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns(_ =>
|
|
{
|
|
ForceImportService.ForgetDryRun();
|
|
return _importedByTheArr;
|
|
});
|
|
|
|
// Act
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: the purge evicted the dry try
|
|
_cache.TryGetValue(CacheKeys.ForceImportTries(record.DownloadId, _instance.Url, true), out int _).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_ForgetDryRunRacesTheDryRunCheck_EvictsTheDryTryWrite()
|
|
{
|
|
// Arrange: a purge fires while awaiting the dry-run check itself
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
_dryRunInterceptor.IsDryRunEnabled().Returns(_ =>
|
|
{
|
|
ForceImportService.ForgetDryRun();
|
|
return Task.FromResult(true);
|
|
});
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>()).Returns(_importedByTheArr);
|
|
|
|
// Act
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: the purge evicted the dry try and give-up marker
|
|
_cache.TryGetValue(CacheKeys.ForceImportTries(record.DownloadId, _instance.Url, true), out int _).ShouldBeFalse();
|
|
_cache.TryGetValue(CacheKeys.ForceImportGaveUp(record.DownloadId, _instance.Url, true), out DateTimeOffset _).ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheGaveUpWindowPassed_TriesAgain()
|
|
{
|
|
// Arrange: whatever stopped the arr may be fixed by now
|
|
SetConfig(maxTries: 1);
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
(await _sut.TryImportAsync(_arrClient, _instance, record)).ShouldBe(ForceImportOutcome.NotApplicable);
|
|
|
|
_timeProvider.Advance(TimeSpan.FromHours(6));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.Received(2).ForceImportAsync(_instance, Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheSightingWindowPassed_WaitsForASecondSightingAgain()
|
|
{
|
|
// Arrange: a sighting that old says nothing about the block in front of us
|
|
QueueRecord record = BuildRecord(state: "importPending");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_timeProvider.Advance(TimeSpan.FromHours(6));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_TheDownloadLeftTheQueue_ReportsTheImport()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _striker.Received(1).ResetStrikeAsync(record.DownloadId, record.Title, StrikeType.FailedImport);
|
|
await _eventPublisher.Received(1).PublishForceImported(record.Title, record.DownloadId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_TheDownloadIsStillQueued_ReportsNothing()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string> { record.DownloadId });
|
|
|
|
// Assert
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_ReportsAnImportOnlyOnce()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _eventPublisher.Received(1).PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_ThePublishFailed_ReportsItOnTheNextRun()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_eventPublisher
|
|
.PublishForceImported(record.Title, record.DownloadId)
|
|
.Returns(Task.FromException(new Exception("the event went nowhere")));
|
|
|
|
// Act
|
|
await Should.ThrowAsync<Exception>(() => _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>()));
|
|
|
|
_eventPublisher.ClearSubstitute(ClearOptions.ReturnValues);
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _eventPublisher.Received(2).PublishForceImported(record.Title, record.DownloadId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_TheLastTryLandedAfterGivingUp_ReportsTheImport()
|
|
{
|
|
// Arrange: the tries run out while the arr is still importing what it was last asked for
|
|
SetConfig(maxTries: 1);
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
_importedByTheArr = 0;
|
|
|
|
ForceImportOutcome gaveUp = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
_importedByTheArr = 1;
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
gaveUp.ShouldBe(ForceImportOutcome.NotApplicable);
|
|
await _striker.Received(1).ResetStrikeAsync(record.DownloadId, record.Title, StrikeType.FailedImport);
|
|
await _eventPublisher.Received(1).PublishForceImported(record.Title, record.DownloadId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_AsksAgain_KeepsTheFirstBaseline()
|
|
{
|
|
// Arrange: the first request lands while the arr still reports the download as blocked
|
|
SetConfig(maxTries: 3);
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// The arr imports nothing more, because the first request already covered the files.
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>())
|
|
.Returns(Task.FromResult(true));
|
|
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert: a baseline taken on the retry would have swallowed the first import
|
|
await _eventPublisher.Received(1).PublishForceImported(record.Title, record.DownloadId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_TheArrRecordedNoImport_ReportsNothing()
|
|
{
|
|
// Arrange: the download left the queue without the arr importing it
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
_importedByTheArr = 0;
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_AnOlderImportOfTheSameDownload_ReportsNothing()
|
|
{
|
|
// Arrange: the arr imported this download once before, and its history keeps that row
|
|
_importedByTheArr = 1;
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
_importedByTheArr = 1;
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_TheHistoryIsUnavailable_ReportsItOnALaterRun()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns<int>(_ => throw new HttpRequestException("the arr is down"));
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>()).Returns(_ => _importedByTheArr);
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _eventPublisher.Received(1).PublishForceImported(record.Title, record.DownloadId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_TheImportNeverLanded_GivesUpOnIt()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
_importedByTheArr = 0;
|
|
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
_timeProvider.Advance(TimeSpan.FromHours(6));
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert: the entry is gone, so an import recorded later is not read as this one
|
|
_importedByTheArr = 1;
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_TheHistoryIsUnavailable_AsksForNothing()
|
|
{
|
|
// Arrange
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns<int>(_ => throw new HttpRequestException("the arr is down"));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
// Assert: a try is worth spending only on a request the arr received
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
[Fact]
|
|
public void Forget_NothingIsPending_DoesNothing()
|
|
{
|
|
Should.NotThrow(() => _sut.Forget(_instance, "HASH"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_CleanuparrRemovedTheDownloadMidRun_ReportsNothing()
|
|
{
|
|
// Arrange: another job takes the download out while the history call is in flight
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
|
|
_arrClient.GetImportedCountAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns(_ =>
|
|
{
|
|
_sut.Forget(_instance, record.DownloadId);
|
|
return _importedByTheArr;
|
|
});
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_CleanuparrRemovedTheDownload_ReportsNothing()
|
|
{
|
|
// Arrange: the download left the queue because Cleanuparr took it out
|
|
QueueRecord record = BuildRecord(state: "importBlocked");
|
|
StubCandidates(BuildCandidate(SafeReason));
|
|
await _sut.TryImportAsync(_arrClient, _instance, record);
|
|
_sut.Forget(_instance, record.DownloadId);
|
|
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReconcileAsync_NothingPending_ReportsNothing()
|
|
{
|
|
// Act
|
|
await _sut.ReconcileAsync(_arrClient, _instance, new HashSet<string>());
|
|
|
|
// Assert
|
|
await _eventPublisher.DidNotReceive().PublishForceImported(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TryImportAsync_CandidatesUnreachable_Defers()
|
|
{
|
|
// Arrange: the arr may answer on the next run
|
|
_arrClient.GetManualImportCandidatesAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns<List<ManualImportCandidate>>(_ => throw new HttpRequestException("down"));
|
|
|
|
// Act
|
|
ForceImportOutcome outcome = await _sut.TryImportAsync(_arrClient, _instance, BuildRecord(state: "importBlocked"));
|
|
|
|
// Assert
|
|
outcome.ShouldBe(ForceImportOutcome.Deferred);
|
|
await _arrClient.DidNotReceive().ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>());
|
|
}
|
|
|
|
// The real ArrClient skips the send under dry run.
|
|
private void EnableDryRun()
|
|
{
|
|
_dryRunInterceptor.IsDryRunEnabled().Returns(true);
|
|
_arrClient.ForceImportAsync(Arg.Any<ArrInstance>(), Arg.Any<List<ManualImportFile>>()).Returns(false);
|
|
}
|
|
|
|
private static void SetConfig(bool forceImport = true, ushort maxTries = 3)
|
|
{
|
|
ContextProvider.Set(new QueueCleanerConfig
|
|
{
|
|
FailedImport = new FailedImportConfig { ForceImport = forceImport, ForceImportMaxTries = maxTries },
|
|
});
|
|
}
|
|
|
|
private void StubCandidates(params ManualImportCandidate[] candidates)
|
|
{
|
|
_arrClient.GetManualImportCandidatesAsync(Arg.Any<ArrInstance>(), Arg.Any<string>())
|
|
.Returns(candidates.ToList());
|
|
}
|
|
|
|
private static ManualImportCandidate BuildCandidate(params string[] rejections) => new()
|
|
{
|
|
Path = "/downloads/show.mkv",
|
|
RelativePath = "show.mkv",
|
|
DownloadId = "HASH",
|
|
Rejections = rejections.Select(reason => new ManualImportRejection { Reason = reason, Type = "permanent" }).ToList(),
|
|
};
|
|
|
|
private static QueueRecord BuildRecord(
|
|
string state,
|
|
string status = "warning",
|
|
List<string>? messages = null,
|
|
bool withStatusMessage = true
|
|
) => new()
|
|
{
|
|
Id = 1,
|
|
Title = "Show.S01E01",
|
|
DownloadId = "HASH",
|
|
SeriesId = 7,
|
|
EpisodeId = 9,
|
|
Protocol = "torrent",
|
|
Status = "completed",
|
|
TrackedDownloadStatus = status,
|
|
TrackedDownloadState = state,
|
|
StatusMessages = withStatusMessage
|
|
?
|
|
[
|
|
new TrackedDownloadStatusMessage { Title = "show.mkv", Messages = messages ?? [SafeReason] },
|
|
]
|
|
: [],
|
|
};
|
|
}
|