mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 18:58:59 -04:00
1983 lines
70 KiB
C#
1983 lines
70 KiB
C#
using Cleanuparr.Domain.Entities.Arr;
|
|
using System.Threading.Channels;
|
|
using Cleanuparr.Infrastructure.Features.Arr.ForceImport;
|
|
using Cleanuparr.Infrastructure.Features.LazyLibrarian;
|
|
using Cleanuparr.Domain.Entities.LazyLibrarian;
|
|
using Cleanuparr.Domain.Entities;
|
|
using Cleanuparr.Domain.Entities.Arr.Queue;
|
|
using Cleanuparr.Domain.Enums;
|
|
using Cleanuparr.Infrastructure.Features.Arr;
|
|
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
|
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
|
|
using Cleanuparr.Infrastructure.Helpers;
|
|
using Cleanuparr.Infrastructure.Services.Interfaces;
|
|
using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
|
|
using Cleanuparr.Infrastructure.Tests.TestHelpers;
|
|
using Cleanuparr.Persistence.Models.Configuration;
|
|
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
|
using Cleanuparr.Persistence.Models.Configuration.General;
|
|
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using QueueCleanerJob = Cleanuparr.Infrastructure.Features.Jobs.QueueCleaner;
|
|
|
|
namespace Cleanuparr.Infrastructure.Tests.Features.Jobs;
|
|
|
|
[Collection(JobHandlerCollection.Name)]
|
|
public class QueueCleanerTests : IDisposable
|
|
{
|
|
private readonly JobHandlerFixture _fixture;
|
|
private readonly ILogger<QueueCleanerJob> _logger;
|
|
private readonly IConnectivityChecker _connectivityChecker;
|
|
|
|
public QueueCleanerTests(JobHandlerFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
_fixture.RecreateDataContext();
|
|
_fixture.ResetMocks();
|
|
_logger = _fixture.CreateLogger<QueueCleanerJob>();
|
|
_connectivityChecker = Substitute.For<IConnectivityChecker>();
|
|
_connectivityChecker.IsOnlineAsync(Arg.Any<GeneralConfig>(), Arg.Any<CancellationToken>()).Returns(true);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private QueueCleanerJob CreateSut()
|
|
{
|
|
return new QueueCleanerJob(
|
|
_logger,
|
|
_fixture.DataContext,
|
|
_fixture.Cache,
|
|
_fixture.RemovalQueue.Writer,
|
|
_fixture.ArrClientFactory,
|
|
_fixture.ArrQueueReader,
|
|
_fixture.DownloadServiceFactory,
|
|
_fixture.EventPublisher,
|
|
_fixture.DryRunInterceptor,
|
|
_connectivityChecker,
|
|
_fixture.ForceImportService,
|
|
_fixture.LazyLibrarianServiceQC
|
|
);
|
|
}
|
|
|
|
#region ExecuteInternalAsync Tests
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenOffline_SkipsRun()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddStallRule(_fixture.DataContext, enabled: true);
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
_connectivityChecker.IsOnlineAsync(Arg.Any<GeneralConfig>(), Arg.Any<CancellationToken>()).Returns(false);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Warning, "no internet connectivity").ShouldBeTrue();
|
|
await _fixture.ArrQueueReader
|
|
.DidNotReceive()
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_LoadsStallRulesFromDatabase()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddStallRule(_fixture.DataContext, enabled: true);
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
_fixture.ArrClientFactory
|
|
.GetClient(Arg.Any<InstanceType>(), Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - no debug message about no active stall rules
|
|
_logger.HasNoLogContaining(LogLevel.Debug, "No active stall rules found").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenNoStallRules_LogsDebugMessage()
|
|
{
|
|
// Arrange
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Debug, "No active stall rules found").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_LoadsSlowRulesFromDatabase()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSlowRule(_fixture.DataContext, enabled: true);
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
_fixture.ArrClientFactory
|
|
.GetClient(Arg.Any<InstanceType>(), Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - no debug message about no active slow rules
|
|
_logger.HasNoLogContaining(LogLevel.Debug, "No active slow rules found").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenNoSlowRules_LogsDebugMessage()
|
|
{
|
|
// Arrange
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Debug, "No active slow rules found").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_ProcessesAllArrConfigs()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
_fixture.ArrClientFactory
|
|
.GetClient(Arg.Any<InstanceType>(), Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_fixture.ArrClientFactory.Received(1).GetClient(InstanceType.Sonarr, Arg.Any<float>());
|
|
_fixture.ArrClientFactory.Received(1).GetClient(InstanceType.Radarr, Arg.Any<float>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ProcessInstanceAsync Tests
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_SkipsIgnoredDownloads()
|
|
{
|
|
// Arrange
|
|
var generalConfig = _fixture.DataContext.GeneralConfigs.First();
|
|
generalConfig.IgnoredDownloads = ["ignored-download-id"];
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "ignored-download-id",
|
|
Title = "Ignored Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Information, "download is ignored").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_SkipsDownloadsIgnoredByClientName()
|
|
{
|
|
// Arrange
|
|
var generalConfig = _fixture.DataContext.GeneralConfigs.First();
|
|
generalConfig.IgnoredDownloads = ["myDownloadClient"];
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "not-a-valid-hash",
|
|
DownloadClient = "MYDOWNLOADCLIENT",
|
|
Title = "Ignored By Client",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Information, "download is ignored").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_SkipsAlreadyCachedDownloads()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
// Pre-cache the download using the correct cache key format
|
|
var cacheKey = CacheKeys.DownloadMarkedForRemoval("cached-download-id", sonarrInstance.Url);
|
|
_fixture.Cache.Set(cacheKey, true);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "cached-download-id",
|
|
Title = "Cached Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Debug, "already marked for removal").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_ChecksTorrentClientsForDownloadInfo()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "torrent-download-id",
|
|
Title = "Torrent Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await mockDownloadService.Received(1)
|
|
.ShouldRemoveFromArrQueueAsync("torrent-download-id", Arg.Any<List<string>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenShouldRemove_PublishesRemoveRequest()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "stalled-download-id",
|
|
Title = "Stalled Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_fixture.RemovalQueue.Reader.Count.ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenShouldRemove_MarksDownloadInCache()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "stalled-download-id",
|
|
Title = "Stalled Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
var cacheKey = CacheKeys.DownloadMarkedForRemoval("stalled-download-id", sonarrInstance.Url);
|
|
_fixture.Cache.TryGetValue(cacheKey, out bool marked).ShouldBeTrue();
|
|
marked.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenDownloadNotFound_LogsWarning()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "missing-download-id",
|
|
Title = "Missing Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult { Found = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Warning, "Download not found in any torrent client").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_ChecksFailedImportsWhenDownloadCheckPasses()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "download-id",
|
|
Title = "Test Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - verify failed import check was called
|
|
await mockArrClient.Received(1).ShouldRemoveFromQueue(
|
|
InstanceType.Sonarr,
|
|
queueRecord,
|
|
false,
|
|
Arg.Any<short>()
|
|
);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true, 0)]
|
|
[InlineData(false, 1)]
|
|
public async Task ProcessInstanceAsync_WhenUsenetDownloadNotFound_HonoursSkipIfNotFoundInClient(bool skipIfNotFound, int expectedChecks)
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, "Test SABnzbd", DownloadClientTypeName.Sabnzbd);
|
|
|
|
QueueCleanerConfig queueCleanerConfig = _fixture.DataContext.QueueCleanerConfigs.First();
|
|
queueCleanerConfig.FailedImport = queueCleanerConfig.FailedImport with { SkipIfNotFoundInClient = skipIfNotFound };
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
QueueRecord queueRecord = new()
|
|
{
|
|
Id = 1,
|
|
DownloadId = "SABnzbd_nzo_missing",
|
|
Title = "Missing Usenet Download",
|
|
Protocol = "usenet",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
IMockDownloadService mockDownloadService = _fixture.CreateMockDownloadService("Test SABnzbd", DownloadClientTypeName.Sabnzbd);
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult { Found = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
QueueCleanerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Warning, "Download not found in any usenet client").ShouldBeTrue();
|
|
await mockArrClient.Received(expectedChecks).ShouldRemoveFromQueue(
|
|
InstanceType.Sonarr,
|
|
queueRecord,
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenForceImportDefers_SkipsTheFailedImportCheck()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
_fixture.ForceImportService
|
|
.TryImportAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>())
|
|
.Returns(ForceImportOutcome.Deferred);
|
|
|
|
QueueRecord queueRecord = new()
|
|
{
|
|
Id = 1,
|
|
DownloadId = "download-id",
|
|
Title = "Test Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
QueueCleanerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await mockArrClient.DidNotReceive().ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_ReconcilesForceImportsWithEveryQueuedDownload()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
QueueRecord[] page = [
|
|
new() { Id = 1, DownloadId = "first", Title = "First", Protocol = "torrent", SeriesId = 1, EpisodeId = 1 },
|
|
new() { Id = 2, DownloadId = "second", Title = "Second", Protocol = "torrent", SeriesId = 1, EpisodeId = 2 },
|
|
];
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns(page.ToList());
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
QueueCleanerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await _fixture.ForceImportService
|
|
.Received(1)
|
|
.ReconcileAsync(
|
|
Arg.Any<IArrClient>(),
|
|
Arg.Any<ArrInstance>(),
|
|
Arg.Is<IReadOnlySet<string>>(ids => ids.Contains("first") && ids.Contains("second"))
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenPackRecordsAreNotAdjacent_ChecksDownloadServiceOnlyOnce()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
QueueRecord packEp2 = new() { Id = 1, DownloadId = "pack-download-id", Title = "Pack S01E02", Protocol = "torrent", SeriesId = 1, EpisodeId = 2 };
|
|
QueueRecord otherDownload = new() { Id = 2, DownloadId = "other-download-id", Title = "Other Download", Protocol = "torrent", SeriesId = 1, EpisodeId = 20 };
|
|
QueueRecord packEp3 = new() { Id = 3, DownloadId = "pack-download-id", Title = "Pack S01E03", Protocol = "torrent", SeriesId = 1, EpisodeId = 3 };
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([packEp2, otherDownload, packEp3]);
|
|
|
|
IMockDownloadService mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
QueueCleanerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await mockDownloadService.Received(1)
|
|
.ShouldRemoveFromArrQueueAsync("pack-download-id", Arg.Any<List<string>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenPackRecordsAreNotAdjacent_ChecksFailedImportOnlyOnce()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
QueueRecord packEp2 = new() { Id = 1, DownloadId = "pack-download-id", Title = "Pack S01E02", Protocol = "torrent", SeriesId = 1, EpisodeId = 2 };
|
|
QueueRecord otherDownload = new() { Id = 2, DownloadId = "other-download-id", Title = "Other Download", Protocol = "torrent", SeriesId = 1, EpisodeId = 20 };
|
|
QueueRecord packEp3 = new() { Id = 3, DownloadId = "pack-download-id", Title = "Pack S01E03", Protocol = "torrent", SeriesId = 1, EpisodeId = 3 };
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([packEp2, otherDownload, packEp3]);
|
|
|
|
IMockDownloadService mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
// The unrelated download is removed outright, so it never reaches the failed-import check.
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync("other-download-id", Arg.Any<List<string>>())
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = true, DeleteReason = DeleteReason.Stalled });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
QueueCleanerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await mockArrClient.Received(1).ShouldRemoveFromQueue(
|
|
InstanceType.Sonarr,
|
|
Arg.Any<QueueRecord>(),
|
|
false,
|
|
Arg.Any<short>()
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenPackRecordsAreNotAdjacentAndShouldRemove_PublishesOneRequestWithPackFlag()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
QueueRecord packEp2 = new() { Id = 1, DownloadId = "pack-download-id", Title = "Pack S01E02", Protocol = "torrent", SeriesId = 1, EpisodeId = 2, SeasonNumber = 1 };
|
|
QueueRecord otherDownload = new() { Id = 2, DownloadId = "other-download-id", Title = "Other Download", Protocol = "torrent", SeriesId = 1, EpisodeId = 20, SeasonNumber = 1 };
|
|
QueueRecord packEp3 = new() { Id = 3, DownloadId = "pack-download-id", Title = "Pack S01E03", Protocol = "torrent", SeriesId = 1, EpisodeId = 3, SeasonNumber = 1 };
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([packEp2, otherDownload, packEp3]);
|
|
|
|
IMockDownloadService mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
QueueCleanerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - the pack flag true branch builds a season SearchItem instead of an episode one
|
|
List<QueueItemRemoveRequest> requests = [];
|
|
|
|
while (_fixture.RemovalQueue.Reader.TryRead(out QueueItemRemoveRequest? request))
|
|
{
|
|
requests.Add(request);
|
|
}
|
|
|
|
requests.ShouldContain(r =>
|
|
r.SeriesItem().SearchType == SeriesSearchType.Season &&
|
|
r.SeriesItem().Id == 1
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenFailedImport_PublishesRemoveRequest()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "failed-import-id",
|
|
Title = "Failed Import",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.DeleteReason.ShouldBe(DeleteReason.FailedImport);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_SkipsItem_WhenMissingContentId_AndProcessNoContentIdIsFalse()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "no-content-id-download",
|
|
Title = "No Content ID Download",
|
|
Protocol = "torrent"
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Information, "skip | item is missing the content id").ShouldBeTrue();
|
|
|
|
_fixture.RemovalQueue.Reader.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenMissingContentId_AndProcessNoContentIdIsTrue_PublishesRemoveRequestWithSkipSearch()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var queueCleanerConfig = _fixture.DataContext.QueueCleanerConfigs.First();
|
|
queueCleanerConfig.ProcessNoContentId = true;
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(false);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "no-content-id-download",
|
|
Title = "No Content ID Download",
|
|
Protocol = "torrent"
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - SkipSearch must be true because the item has no content ID
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.SkipSearch.ShouldBeTrue();
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling Tests
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenDownloadServiceThrows_LogsErrorAndContinues()
|
|
{
|
|
// Arrange
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(false);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "error-download-id",
|
|
Title = "Error Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.ThrowsAsync(new Exception("Connection failed"));
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Error, "Error checking download").ShouldBeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GenericHandler PublishQueueItemRemoveRequest Tests
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_WhenCacheHasKey_SkipsRemovalRequest()
|
|
{
|
|
// Arrange - test the cache skip in GenericHandler.PublishQueueItemRemoveRequest
|
|
// This simulates a race condition where the key is added between QueueCleaner's check
|
|
// and calling PublishQueueItemRemoveRequest
|
|
var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "race-condition-download",
|
|
Title = "Race Condition Download",
|
|
Protocol = "torrent",
|
|
MovieId = 1
|
|
};
|
|
|
|
// Simulate race condition: add to cache when ShouldRemoveFromArrQueueAsync is called
|
|
// (after QueueCleaner's cache check but before PublishQueueItemRemoveRequest)
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(ci =>
|
|
{
|
|
// Add to cache here - simulating another thread/process adding this
|
|
var cacheKey = CacheKeys.DownloadMarkedForRemoval(queueRecord.DownloadId, radarrInstance.Url);
|
|
_fixture.Cache.Set(cacheKey, true);
|
|
|
|
return new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
};
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should log "skip removal request | already marked for removal" from GenericHandler
|
|
_logger.HasLogContaining(LogLevel.Debug, "skip removal request").ShouldBeTrue();
|
|
|
|
// Verify no publish was made
|
|
_fixture.RemovalQueue.Reader.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_ForRadarr_PublishesSearchItemRequest()
|
|
{
|
|
// Arrange - test the SearchItem branch for Radarr (not SeriesSearchItem)
|
|
var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "radarr-download-id",
|
|
Title = "Radarr Download",
|
|
Protocol = "torrent",
|
|
MovieId = 42
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should publish QueueItemRemoveRequest (not SeriesSearchItem)
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.Instance.ArrConfig.Type.ShouldBe(InstanceType.Radarr);
|
|
r.ArrTarget().SearchItem.Id.ShouldBe(42);
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_ForLidarr_PublishesSearchItemRequest()
|
|
{
|
|
// Arrange - test the SearchItem branch for Lidarr
|
|
var lidarrInstance = TestDataContextFactory.AddLidarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Lidarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "lidarr-download-id",
|
|
Title = "Lidarr Download",
|
|
Protocol = "torrent",
|
|
AlbumId = 123
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.SlowSpeed
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should publish QueueItemRemoveRequest with AlbumId
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.Instance.ArrConfig.Type.ShouldBe(InstanceType.Lidarr);
|
|
r.ArrTarget().SearchItem.Id.ShouldBe(123);
|
|
r.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_ForReadarr_PublishesSearchItemRequest()
|
|
{
|
|
// Arrange - test the SearchItem branch for Readarr
|
|
var readarrInstance = TestDataContextFactory.AddReadarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Readarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "readarr-download-id",
|
|
Title = "Readarr Download",
|
|
Protocol = "torrent",
|
|
BookId = 456
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should publish QueueItemRemoveRequest with BookId
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.Instance.ArrConfig.Type.ShouldBe(InstanceType.Readarr);
|
|
r.ArrTarget().SearchItem.Id.ShouldBe(456);
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_ForWhisparrV2_PublishesSeriesSearchItemRequest()
|
|
{
|
|
// Arrange - test that Whisparr v2 uses SeriesSearchItem
|
|
var whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext, version: 2);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Whisparr, 2f)
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "whisparr-v2-download-id",
|
|
Title = "Whisparr V2 Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 10,
|
|
EpisodeId = 100
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should publish QueueItemRemoveRequest
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.Instance.ArrConfig.Type.ShouldBe(InstanceType.Whisparr);
|
|
r.ArrTarget().SearchItem.Id.ShouldBe(100); // EpisodeId
|
|
r.SeriesItem().SeriesId.ShouldBe(10);
|
|
r.SeriesItem().SearchType.ShouldBe(SeriesSearchType.Episode);
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_ForWhisparrV3_PublishesSearchItemRequest()
|
|
{
|
|
// Arrange - test that Whisparr v3 uses SearchItem
|
|
var whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext, version: 3);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Whisparr, 3f)
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "whisparr-v3-download-id",
|
|
Title = "Whisparr V3 Download",
|
|
Protocol = "torrent",
|
|
MovieId = 42
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should publish QueueItemRemoveRequest with MovieId
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.Instance.ArrConfig.Type.ShouldBe(InstanceType.Whisparr);
|
|
r.ArrTarget().SearchItem.Id.ShouldBe(42); // MovieId
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishQueueItemRemoveRequest_ForWhisparrV2Pack_PublishesSeasonSearchItemRequest()
|
|
{
|
|
// Arrange - test that Whisparr v2 pack (multiple records with same download ID) uses SeriesSearchItem with Season search type
|
|
var whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext, version: 2);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Whisparr, 2f)
|
|
.Returns(mockArrClient);
|
|
|
|
// Create multiple records with same download ID to simulate a pack (season pack)
|
|
var record1 = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "whisparr-v2-pack-download-id",
|
|
Title = "Whisparr V2 Season Pack - Episode 1",
|
|
Protocol = "torrent",
|
|
SeriesId = 10,
|
|
EpisodeId = 100,
|
|
SeasonNumber = 3
|
|
};
|
|
var record2 = new QueueRecord
|
|
{
|
|
Id = 2,
|
|
DownloadId = "whisparr-v2-pack-download-id",
|
|
Title = "Whisparr V2 Season Pack - Episode 2",
|
|
Protocol = "torrent",
|
|
SeriesId = 10,
|
|
EpisodeId = 101,
|
|
SeasonNumber = 3
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([record1, record2]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteFromClient = true,
|
|
DeleteReason = DeleteReason.Stalled
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - should publish QueueItemRemoveRequest with Season search type
|
|
// because multiple records with the same download ID indicate a pack
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.Instance.ArrConfig.Type.ShouldBe(InstanceType.Whisparr);
|
|
r.ArrTarget().SearchItem.Id.ShouldBe(3); // SeasonNumber
|
|
r.SeriesItem().SeriesId.ShouldBe(10);
|
|
r.SeriesItem().SearchType.ShouldBe(SeriesSearchType.Season);
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ChangeCategory Tests
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenFailedImportWithChangeCategory_PublishesRequestWithChangeCategoryAndRemoveFromClientFalse()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var queueCleanerConfig = _fixture.DataContext.QueueCleanerConfigs.First();
|
|
// Set DeletePrivate = true so RemoveFromClient would be true without the ChangeCategory override.
|
|
// This makes the RemoveFromClient == false assertion below conclusive.
|
|
queueCleanerConfig.FailedImport = queueCleanerConfig.FailedImport with { ChangeCategory = true, DeletePrivate = false };
|
|
// Validate gate prevents both flags being true at once; we keep DeletePrivate=false here, but rely on
|
|
// IsPrivate=false from the mock so removeFromClient resolves to !changeCategory.
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "failed-import-change-category",
|
|
Title = "Failed Import Change Category",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
// IsPrivate=false ensures the failed-import path computes
|
|
// removeFromClient = !changeCategory && (!IsPrivate || DeletePrivate) = !changeCategory && true.
|
|
// So RemoveFromClient == false in the assertion is only satisfiable due to changeCategory=true.
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false, IsPrivate = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.DeleteReason.ShouldBe(DeleteReason.FailedImport);
|
|
r.ArrTarget().ChangeCategory.ShouldBeTrue();
|
|
r.ArrTarget().RemoveFromClient.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenFailedImportWithChangeCategoryOnUsenet_PublishesRequestWithoutChangeCategoryAndRemoveFromClientTrue()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, "Test SABnzbd", DownloadClientTypeName.Sabnzbd);
|
|
|
|
var queueCleanerConfig = _fixture.DataContext.QueueCleanerConfigs.First();
|
|
queueCleanerConfig.FailedImport = queueCleanerConfig.FailedImport with { ChangeCategory = true, DeletePrivate = false };
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.ShouldRemoveFromQueue(
|
|
Arg.Any<InstanceType>(),
|
|
Arg.Any<QueueRecord>(),
|
|
Arg.Any<bool>(),
|
|
Arg.Any<short>()
|
|
).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "failed-import-change-category-usenet",
|
|
Title = "Failed Import Change Category Usenet",
|
|
Protocol = "usenet",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService("Test SABnzbd", DownloadClientTypeName.Sabnzbd);
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false, IsPrivate = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.DeleteReason.ShouldBe(DeleteReason.FailedImport);
|
|
r.ArrTarget().ChangeCategory.ShouldBeFalse();
|
|
r.ArrTarget().RemoveFromClient.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenStallRuleHasChangeCategory_PublishesRequestWithChangeCategoryAndRemoveFromClientFalse()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
|
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
|
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
|
.Returns(mockArrClient);
|
|
|
|
var queueRecord = new QueueRecord
|
|
{
|
|
Id = 1,
|
|
DownloadId = "stall-change-category",
|
|
Title = "Stall Change Category",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.ShouldRemoveFromArrQueueAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new DownloadCheckResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = true,
|
|
DeleteFromClient = true,
|
|
ChangeCategory = true,
|
|
DeleteReason = DeleteReason.Stalled,
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
r.ArrTarget().ChangeCategory.ShouldBeTrue();
|
|
r.ArrTarget().RemoveFromClient.ShouldBeFalse();
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region LazyLibrarian
|
|
|
|
private static LazyLibrarianQueueItem CreateBookItem(LazyLibrarianOrigin origin = LazyLibrarianOrigin.New) => new()
|
|
{
|
|
DownloadId = "torrent-hash",
|
|
Title = "Book",
|
|
Books = [new LazyLibrarianBookRef { BookId = "OL7353617M", Library = BookLibrary.EBook }],
|
|
Source = LazyLibrarianSource.QBittorrent,
|
|
Origin = origin,
|
|
};
|
|
|
|
private (IDownloadService Service, ITorrentItemWrapper Torrent) StubLazyLibrarianDecision(
|
|
LazyLibrarianOrigin origin = LazyLibrarianOrigin.New,
|
|
bool removeFromClient = true
|
|
)
|
|
{
|
|
TestDataContextFactory.AddLazyLibrarianInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
ITorrentItemWrapper torrent = Substitute.For<ITorrentItemWrapper>();
|
|
torrent.DownloadId.Returns("torrent-hash");
|
|
|
|
var downloadService = _fixture.CreateMockDownloadService();
|
|
DownloadClientConfig clientConfig = downloadService.ClientConfig;
|
|
|
|
LazyLibrarianRemovalDecision decision = new()
|
|
{
|
|
Item = CreateBookItem(origin),
|
|
DeleteReason = DeleteReason.Stalled,
|
|
RemoveFromClient = removeFromClient,
|
|
DownloadClient = clientConfig,
|
|
DownloadService = downloadService,
|
|
Download = torrent,
|
|
};
|
|
|
|
IReadOnlyList<LazyLibrarianRemovalDecision> decisions = [decision];
|
|
|
|
_fixture.LazyLibrarianServiceQC
|
|
.EvaluateAsync(Arg.Any<ArrInstance>(), Arg.Any<IReadOnlyList<IDownloadService>>(), Arg.Any<IReadOnlyList<string>>())
|
|
.Returns(decisions);
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(downloadService);
|
|
|
|
_fixture.DryRunInterceptor
|
|
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
|
.Returns(ci => ((Func<Task>)ci[0])());
|
|
|
|
return (downloadService, torrent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_LazyLibrarian_DeletesTheTorrentThroughTheDryRunInterceptor()
|
|
{
|
|
// Arrange
|
|
(IDownloadService downloadService, ITorrentItemWrapper torrent) = StubLazyLibrarianDecision();
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await downloadService.Received(1).DeleteDownload(torrent, true);
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.LazyTarget().RemovedFromClient.ShouldBeTrue();
|
|
r.LazyTarget().Item.Books.Single().BookId.ShouldBe("OL7353617M");
|
|
r.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_LazyLibrarian_KeepsATorrentLazyLibrarianAdopted()
|
|
{
|
|
// Arrange: LazyLibrarian refuses to remove an adopted task, and the files back another seed.
|
|
(IDownloadService downloadService, ITorrentItemWrapper torrent) =
|
|
StubLazyLibrarianDecision(origin: LazyLibrarianOrigin.Adopted);
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await downloadService.DidNotReceive().DeleteDownload(Arg.Any<ITorrentItemWrapper>(), Arg.Any<bool>());
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.LazyTarget().RemovedFromClient.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_LazyLibrarian_DoesNotPublishWhenTheDeleteFails()
|
|
{
|
|
// Arrange
|
|
(IDownloadService downloadService, ITorrentItemWrapper torrent) = StubLazyLibrarianDecision();
|
|
downloadService
|
|
.DeleteDownload(Arg.Any<ITorrentItemWrapper>(), Arg.Any<bool>())
|
|
.ThrowsAsync(new Exception("client unreachable"));
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_fixture.RemovalQueue.Reader.Count.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_LazyLibrarian_DeletesAFailedSabnzbdDownloadFromTheClient()
|
|
{
|
|
// Arrange: a SABnzbd Failed job is a removal decision like any torrent one, but Download is a plain usenet item.
|
|
TestDataContextFactory.AddLazyLibrarianInstance(_fixture.DataContext);
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
IDownloadItem nzbItem = Substitute.For<IDownloadItem>();
|
|
nzbItem.DownloadId.Returns("nzo-hash");
|
|
|
|
var downloadService = _fixture.CreateMockDownloadService(typeName: DownloadClientTypeName.Sabnzbd);
|
|
DownloadClientConfig clientConfig = downloadService.ClientConfig;
|
|
|
|
LazyLibrarianRemovalDecision decision = new()
|
|
{
|
|
Item = CreateBookItem() with { DownloadId = "nzo-hash", Source = LazyLibrarianSource.Sabnzbd },
|
|
DeleteReason = DeleteReason.DownloadFailed,
|
|
RemoveFromClient = true,
|
|
DownloadClient = clientConfig,
|
|
DownloadService = downloadService,
|
|
Download = nzbItem,
|
|
};
|
|
|
|
_fixture.LazyLibrarianServiceQC
|
|
.EvaluateAsync(Arg.Any<ArrInstance>(), Arg.Any<IReadOnlyList<IDownloadService>>(), Arg.Any<IReadOnlyList<string>>())
|
|
.Returns([decision]);
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(downloadService);
|
|
|
|
_fixture.DryRunInterceptor
|
|
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
|
.Returns(ci => ((Func<Task>)ci[0])());
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await downloadService.Received(1).DeleteDownload(nzbItem, true);
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.LazyTarget().RemovedFromClient.ShouldBeTrue();
|
|
r.DeleteReason.ShouldBe(DeleteReason.DownloadFailed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_LazyLibrarian_DoesNotUseTheArrQueueReader()
|
|
{
|
|
// Arrange
|
|
StubLazyLibrarianDecision();
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await _fixture.ArrQueueReader.DidNotReceive().ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>());
|
|
}
|
|
|
|
#endregion
|
|
}
|