mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-11-04 21:33:38 -05:00
970 lines
34 KiB
C#
970 lines
34 KiB
C#
using Cleanuparr.Domain.Entities.Arr;
|
|
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.Features.LazyLibrarian;
|
|
using Cleanuparr.Domain.Entities.LazyLibrarian;
|
|
using Cleanuparr.Domain.Entities;
|
|
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
|
using Cleanuparr.Infrastructure.Helpers;
|
|
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.MalwareBlocker;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using MalwareBlockerJob = Cleanuparr.Infrastructure.Features.Jobs.MalwareBlocker;
|
|
|
|
namespace Cleanuparr.Infrastructure.Tests.Features.Jobs;
|
|
|
|
[Collection(JobHandlerCollection.Name)]
|
|
public class MalwareBlockerTests : IDisposable
|
|
{
|
|
private readonly JobHandlerFixture _fixture;
|
|
private readonly ILogger<MalwareBlockerJob> _logger;
|
|
|
|
public MalwareBlockerTests(JobHandlerFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
_fixture.RecreateDataContext();
|
|
_fixture.ResetMocks();
|
|
_logger = _fixture.CreateLogger<MalwareBlockerJob>();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private MalwareBlockerJob CreateSut()
|
|
{
|
|
return new MalwareBlockerJob(
|
|
_logger,
|
|
_fixture.DataContext,
|
|
_fixture.Cache,
|
|
_fixture.RemovalQueue.Writer,
|
|
_fixture.ArrClientFactory,
|
|
_fixture.ArrQueueReader,
|
|
_fixture.DownloadServiceFactory,
|
|
_fixture.BlocklistProvider,
|
|
_fixture.EventPublisher,
|
|
_fixture.DryRunInterceptor,
|
|
_fixture.ForceImportService,
|
|
_fixture.LazyLibrarianServiceCB
|
|
);
|
|
}
|
|
|
|
#region ExecuteInternalAsync Tests
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenNoDownloadClientsConfigured_LogsWarningAndReturns()
|
|
{
|
|
// Arrange
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Warning, "No download clients configured").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenNoBlocklistsEnabled_LogsWarningAndReturns()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Warning, "No blocklists are enabled").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenBlocklistEnabled_LoadsBlocklists()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
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
|
|
await _fixture.BlocklistProvider.Received(1).LoadBlocklistsAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteInternalAsync_WhenSonarrEnabled_ProcessesSonarrInstances()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
_fixture.ArrClientFactory
|
|
.GetClient(InstanceType.Sonarr, 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>());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(InstanceType.Radarr)]
|
|
[InlineData(InstanceType.Lidarr)]
|
|
[InlineData(InstanceType.Readarr)]
|
|
[InlineData(InstanceType.Whisparr)]
|
|
public async Task ExecuteInternalAsync_WhenArrTypeEnabled_ProcessesCorrectInstances(InstanceType instanceType)
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableBlocklist(instanceType);
|
|
AddArrInstance(instanceType);
|
|
|
|
var mockArrClient = Substitute.For<IArrClient>();
|
|
_fixture.ArrClientFactory
|
|
.GetClient(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, 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.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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, "ignored").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_SkipsDownloadsIgnoredByClientName()
|
|
{
|
|
// Arrange
|
|
var generalConfig = _fixture.DataContext.GeneralConfigs.First();
|
|
generalConfig.IgnoredDownloads = ["myDownloadClient"];
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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, "ignored").ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_ChecksTorrentClientsForBlockedFiles()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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 = "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
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await mockDownloadService.Received(1)
|
|
.BlockUnwantedFilesAsync("torrent-download-id", Arg.Any<List<string>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenShouldRemove_PublishesRemoveRequest()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_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 = "malware-download-id",
|
|
Title = "Malware Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteReason = DeleteReason.AllFilesBlocked
|
|
});
|
|
|
|
_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.AllFilesBlocked);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenShouldRemove_MarksDownloadInCache()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_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 = "malware-download-id",
|
|
Title = "Malware Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteReason = DeleteReason.AllFilesBlocked
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
var cacheKey = CacheKeys.DownloadMarkedForRemoval("malware-download-id", sonarrInstance.Url);
|
|
_fixture.Cache.TryGetValue(cacheKey, out bool marked).ShouldBeTrue();
|
|
marked.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenShouldRemoveWithAtLeastOneFileBlocked_PublishesRemoveRequest()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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 queueRecord = new()
|
|
{
|
|
Id = 1,
|
|
DownloadId = "any-file-blocked-download-id",
|
|
Title = "Mixed Malware Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteReason = DeleteReason.AtLeastOneFileBlocked
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
MalwareBlockerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.DeleteReason.ShouldBe(DeleteReason.AtLeastOneFileBlocked);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenPackRecordsAreNotAdjacent_ChecksBlockUnwantedFilesOnlyOnce()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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
|
|
.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<List<string>>())
|
|
.Returns(new BlockFilesResult { Found = true, ShouldRemove = false });
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
MalwareBlockerJob sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
await mockDownloadService.Received(1)
|
|
.BlockUnwantedFilesAsync("pack-download-id", Arg.Any<List<string>>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenPrivateAndDeletePrivateFalse_DoesNotRemoveFromClient()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
|
|
// Ensure DeletePrivate is false
|
|
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
|
|
contentBlockerConfig.DeletePrivate = false;
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
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 = "private-malware-id",
|
|
Title = "Private Malware",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
var mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = true,
|
|
DeleteReason = DeleteReason.AllFilesBlocked
|
|
});
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
|
.Returns(mockDownloadService);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert - RemoveFromClient should be false
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.ArrTarget().RemoveFromClient.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenDownloadNotFoundInTorrentClient_LogsWarning()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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 = "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
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult { 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_SkipsItem_WhenMissingContentId_AndProcessNoContentIdIsFalse()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
|
|
|
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
|
|
contentBlockerConfig.ProcessNoContentId = true;
|
|
_fixture.DataContext.SaveChanges();
|
|
|
|
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 mockDownloadService = _fixture.CreateMockDownloadService();
|
|
mockDownloadService
|
|
.BlockUnwantedFilesAsync(
|
|
Arg.Any<string>(),
|
|
Arg.Any<List<string>>()
|
|
)
|
|
.Returns(new BlockFilesResult
|
|
{
|
|
Found = true,
|
|
ShouldRemove = true,
|
|
IsPrivate = false,
|
|
DeleteReason = DeleteReason.AllFilesBlocked
|
|
});
|
|
|
|
_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.AllFilesBlocked);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling Tests
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenDownloadServiceThrows_LogsErrorAndContinues()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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 = "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
|
|
.BlockUnwantedFilesAsync(
|
|
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();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_WhenFirstClientThrows_NextClientStillFindsTheDownload()
|
|
{
|
|
// Arrange
|
|
DownloadClientConfig configA = TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "Client A");
|
|
DownloadClientConfig configB = TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "Client B");
|
|
EnableSonarrBlocklist();
|
|
TestDataContextFactory.AddSonarrInstance(_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 = "two-clients-download-id",
|
|
Title = "Two Clients Download",
|
|
Protocol = "torrent",
|
|
SeriesId = 1,
|
|
EpisodeId = 1
|
|
};
|
|
|
|
_fixture.ArrQueueReader
|
|
.ReadAllAsync(Arg.Any<IArrClient>(), Arg.Any<ArrInstance>())
|
|
.Returns([queueRecord]);
|
|
|
|
// Client load order is not fixed, so whichever client runs first throws.
|
|
int calls = 0;
|
|
Func<NSubstitute.Core.CallInfo, Task<BlockFilesResult>> firstThrows = _ => calls++ == 0
|
|
? throw new Exception("Connection failed")
|
|
: Task.FromResult(new BlockFilesResult { Found = true, ShouldRemove = true, DeleteReason = DeleteReason.AllFilesBlocked });
|
|
|
|
var serviceA = _fixture.CreateMockDownloadService("Client A");
|
|
serviceA.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<List<string>>()).Returns(firstThrows);
|
|
var serviceB = _fixture.CreateMockDownloadService("Client B");
|
|
serviceB.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<List<string>>()).Returns(firstThrows);
|
|
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Is<DownloadClientConfig>(x => x.Id == configA.Id))
|
|
.Returns(serviceA);
|
|
_fixture.DownloadServiceFactory
|
|
.GetDownloadService(Arg.Is<DownloadClientConfig>(x => x.Id == configB.Id))
|
|
.Returns(serviceB);
|
|
|
|
var sut = CreateSut();
|
|
|
|
// Act
|
|
await sut.ExecuteAsync();
|
|
|
|
// Assert
|
|
_logger.HasLogContaining(LogLevel.Error, "Error checking download").ShouldBeTrue();
|
|
QueueItemRemoveRequest request = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
request.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
|
request.DownloadClient.ShouldNotBeNull();
|
|
calls.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInstanceAsync_LazyLibrarian_PublishesTheEvaluatedDecisions()
|
|
{
|
|
// Arrange
|
|
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
|
EnableBlocklist(InstanceType.LazyLibrarian);
|
|
AddArrInstance(InstanceType.LazyLibrarian);
|
|
|
|
ITorrentItemWrapper torrent = Substitute.For<ITorrentItemWrapper>();
|
|
torrent.DownloadId.Returns("torrent-hash");
|
|
|
|
var downloadService = _fixture.CreateMockDownloadService();
|
|
|
|
LazyLibrarianRemovalDecision decision = new()
|
|
{
|
|
Item = new LazyLibrarianQueueItem
|
|
{
|
|
DownloadId = "torrent-hash",
|
|
Title = "Book",
|
|
Books = [new LazyLibrarianBookRef { BookId = "OL7353617M", Library = BookLibrary.EBook }],
|
|
Source = LazyLibrarianSource.QBittorrent,
|
|
Origin = LazyLibrarianOrigin.New,
|
|
},
|
|
DeleteReason = DeleteReason.AllFilesBlocked,
|
|
RemoveFromClient = true,
|
|
DownloadClient = downloadService.ClientConfig,
|
|
DownloadService = downloadService,
|
|
Download = torrent,
|
|
};
|
|
|
|
_fixture.LazyLibrarianServiceCB
|
|
.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(torrent, true);
|
|
QueueItemRemoveRequest r = _fixture.RemovalQueue.ShouldHaveSingle();
|
|
r.LazyTarget().RemovedFromClient.ShouldBeTrue();
|
|
r.LazyTarget().Item.Books.Single().BookId.ShouldBe("OL7353617M");
|
|
r.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
|
_fixture.ArrClientFactory.DidNotReceiveWithAnyArgs().GetClient(default, default);
|
|
await _fixture.ArrQueueReader.DidNotReceiveWithAnyArgs()
|
|
.ReadAllAsync(default!, default!);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private void EnableSonarrBlocklist()
|
|
{
|
|
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
|
|
contentBlockerConfig.Sonarr = new BlocklistSettings { Enabled = true };
|
|
_fixture.DataContext.SaveChanges();
|
|
}
|
|
|
|
private void EnableBlocklist(InstanceType instanceType)
|
|
{
|
|
var config = _fixture.DataContext.ContentBlockerConfigs.First();
|
|
var settings = new BlocklistSettings { Enabled = true };
|
|
switch (instanceType)
|
|
{
|
|
case InstanceType.Radarr: config.Radarr = settings; break;
|
|
case InstanceType.Lidarr: config.Lidarr = settings; break;
|
|
case InstanceType.Readarr: config.Readarr = settings; break;
|
|
case InstanceType.Whisparr: config.Whisparr = settings; break;
|
|
case InstanceType.LazyLibrarian: config.LazyLibrarian = settings; break;
|
|
default: throw new ArgumentOutOfRangeException(nameof(instanceType));
|
|
}
|
|
_fixture.DataContext.SaveChanges();
|
|
}
|
|
|
|
private void AddArrInstance(InstanceType instanceType)
|
|
{
|
|
switch (instanceType)
|
|
{
|
|
case InstanceType.Radarr: TestDataContextFactory.AddRadarrInstance(_fixture.DataContext); break;
|
|
case InstanceType.Lidarr: TestDataContextFactory.AddLidarrInstance(_fixture.DataContext); break;
|
|
case InstanceType.Readarr: TestDataContextFactory.AddReadarrInstance(_fixture.DataContext); break;
|
|
case InstanceType.Whisparr: TestDataContextFactory.AddWhisparrInstance(_fixture.DataContext); break;
|
|
case InstanceType.LazyLibrarian: TestDataContextFactory.AddLazyLibrarianInstance(_fixture.DataContext); break;
|
|
default: throw new ArgumentOutOfRangeException(nameof(instanceType));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|