Files
Cleanuparr/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/MalwareBlockerTests.cs
2026-01-03 23:34:21 +02:00

610 lines
20 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.MalwareBlocker;
using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.Extensions.Logging;
using Moq;
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 Mock<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.Object,
_fixture.DataContext,
_fixture.Cache,
_fixture.MessageBus.Object,
_fixture.ArrClientFactory.Object,
_fixture.ArrQueueIterator.Object,
_fixture.DownloadServiceFactory.Object,
_fixture.BlocklistProvider.Object,
_fixture.EventPublisher.Object
);
}
#region ExecuteInternalAsync Tests
[Fact]
public async Task ExecuteInternalAsync_WhenNoDownloadClientsConfigured_LogsWarningAndReturns()
{
// Arrange
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_logger.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("No download clients configured")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task ExecuteInternalAsync_WhenNoBlocklistsEnabled_LogsWarningAndReturns()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_logger.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("No blocklists are enabled")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task ExecuteInternalAsync_WhenBlocklistEnabled_LoadsBlocklists()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
EnableSonarrBlocklist();
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
_fixture.ArrClientFactory
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
.Returns(mockArrClient.Object);
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(Task.CompletedTask);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_fixture.BlocklistProvider.Verify(x => x.LoadBlocklistsAsync(), Times.Once);
}
[Fact]
public async Task ExecuteInternalAsync_WhenSonarrEnabled_ProcessesSonarrInstances()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
EnableSonarrBlocklist();
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(Task.CompletedTask);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()), Times.Once);
}
[Fact]
public async Task ExecuteInternalAsync_WhenDeleteKnownMalwareEnabled_ProcessesAllArrs()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
contentBlockerConfig.DeleteKnownMalware = true;
// Need at least one blocklist enabled for processing to occur
contentBlockerConfig.Sonarr = new BlocklistSettings { Enabled = true };
_fixture.DataContext.SaveChanges();
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
_fixture.ArrClientFactory
.Setup(x => x.GetClient(It.IsAny<InstanceType>(), It.IsAny<float>()))
.Returns(mockArrClient.Object);
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(Task.CompletedTask);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert - Sonarr and Radarr processed because DeleteKnownMalware is true
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()), Times.Once);
_fixture.ArrClientFactory.Verify(x => x.GetClient(InstanceType.Radarr, It.IsAny<float>()), Times.Once);
}
#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 = new Mock<IArrClient>();
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
var queueRecord = new QueueRecord
{
Id = 1,
DownloadId = "ignored-download-id",
Title = "Ignored Download",
Protocol = "torrent"
};
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
{
await callback([queueRecord]);
});
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_logger.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("ignored")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
[Fact]
public async Task ProcessInstanceAsync_ChecksTorrentClientsForBlockedFiles()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
EnableSonarrBlocklist();
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
var queueRecord = new QueueRecord
{
Id = 1,
DownloadId = "torrent-download-id",
Title = "Torrent Download",
Protocol = "torrent"
};
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
{
await callback([queueRecord]);
});
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService
.Setup(x => x.BlockUnwantedFilesAsync(
It.IsAny<string>(),
It.IsAny<List<string>>()
))
.ReturnsAsync(new BlockFilesResult { Found = true, ShouldRemove = false });
_fixture.DownloadServiceFactory
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
.Returns(mockDownloadService.Object);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
mockDownloadService.Verify(
x => x.BlockUnwantedFilesAsync("torrent-download-id", It.IsAny<List<string>>()),
Times.Once
);
}
[Fact]
public async Task ProcessInstanceAsync_WhenShouldRemove_PublishesRemoveRequest()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
EnableSonarrBlocklist();
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
var queueRecord = new QueueRecord
{
Id = 1,
DownloadId = "malware-download-id",
Title = "Malware Download",
Protocol = "torrent",
SeriesId = 1,
EpisodeId = 1
};
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
{
await callback([queueRecord]);
});
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService
.Setup(x => x.BlockUnwantedFilesAsync(
It.IsAny<string>(),
It.IsAny<List<string>>()
))
.ReturnsAsync(new BlockFilesResult
{
Found = true,
ShouldRemove = true,
IsPrivate = false,
DeleteReason = DeleteReason.AllFilesBlocked
});
_fixture.DownloadServiceFactory
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
.Returns(mockDownloadService.Object);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_fixture.MessageBus.Verify(
x => x.Publish(
It.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
r.DeleteReason == DeleteReason.AllFilesBlocked
),
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[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 = new Mock<IArrClient>();
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
var queueRecord = new QueueRecord
{
Id = 1,
DownloadId = "private-malware-id",
Title = "Private Malware",
Protocol = "torrent",
SeriesId = 1,
EpisodeId = 1
};
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
{
await callback([queueRecord]);
});
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService
.Setup(x => x.BlockUnwantedFilesAsync(
It.IsAny<string>(),
It.IsAny<List<string>>()
))
.ReturnsAsync(new BlockFilesResult
{
Found = true,
ShouldRemove = true,
IsPrivate = true,
DeleteReason = DeleteReason.AllFilesBlocked
});
_fixture.DownloadServiceFactory
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
.Returns(mockDownloadService.Object);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert - RemoveFromClient should be false
_fixture.MessageBus.Verify(
x => x.Publish(
It.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
r.RemoveFromClient == false
),
It.IsAny<CancellationToken>()
),
Times.Once
);
}
[Fact]
public async Task ProcessInstanceAsync_WhenDownloadNotFoundInTorrentClient_LogsWarning()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
EnableSonarrBlocklist();
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
var queueRecord = new QueueRecord
{
Id = 1,
DownloadId = "missing-download-id",
Title = "Missing Download",
Protocol = "torrent"
};
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
{
await callback([queueRecord]);
});
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService
.Setup(x => x.BlockUnwantedFilesAsync(
It.IsAny<string>(),
It.IsAny<List<string>>()
))
.ReturnsAsync(new BlockFilesResult { Found = false });
_fixture.DownloadServiceFactory
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
.Returns(mockDownloadService.Object);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_logger.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Download not found in any torrent client")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
#endregion
#region Error Handling Tests
[Fact]
public async Task ProcessInstanceAsync_WhenDownloadServiceThrows_LogsErrorAndContinues()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
EnableSonarrBlocklist();
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockArrClient = new Mock<IArrClient>();
mockArrClient.Setup(x => x.IsRecordValid(It.IsAny<QueueRecord>())).Returns(true);
_fixture.ArrClientFactory
.Setup(x => x.GetClient(InstanceType.Sonarr, It.IsAny<float>()))
.Returns(mockArrClient.Object);
var queueRecord = new QueueRecord
{
Id = 1,
DownloadId = "error-download-id",
Title = "Error Download",
Protocol = "torrent"
};
_fixture.ArrQueueIterator
.Setup(x => x.Iterate(
It.IsAny<IArrClient>(),
It.IsAny<ArrInstance>(),
It.IsAny<Func<IReadOnlyList<QueueRecord>, Task>>()
))
.Returns(async (IArrClient client, ArrInstance instance, Func<IReadOnlyList<QueueRecord>, Task> callback) =>
{
await callback([queueRecord]);
});
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService
.Setup(x => x.BlockUnwantedFilesAsync(
It.IsAny<string>(),
It.IsAny<List<string>>()
))
.ThrowsAsync(new Exception("Connection failed"));
_fixture.DownloadServiceFactory
.Setup(x => x.GetDownloadService(It.IsAny<DownloadClientConfig>()))
.Returns(mockDownloadService.Object);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert
_logger.Verify(
x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Error checking download")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()
),
Times.Once
);
}
#endregion
#region Helper Methods
private void EnableSonarrBlocklist()
{
var contentBlockerConfig = _fixture.DataContext.ContentBlockerConfigs.First();
contentBlockerConfig.Sonarr = new BlocklistSettings { Enabled = true };
_fixture.DataContext.SaveChanges();
}
#endregion
}