Files
Cleanuparr/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/Integration/MalwareBlockerIntegrationTests.cs
2026-04-06 09:59:31 +03:00

269 lines
11 KiB
C#

using System.Text.Json;
using Cleanuparr.Domain.Entities.Arr.Queue;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
using Cleanuparr.Infrastructure.Features.DownloadClient;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Shouldly;
using Xunit;
using MalwareBlockerJob = Cleanuparr.Infrastructure.Features.Jobs.MalwareBlocker;
namespace Cleanuparr.Infrastructure.Tests.Features.Jobs.Integration;
[Collection(IntegrationTestCollection.Name)]
public class MalwareBlockerIntegrationTests : IDisposable
{
private readonly IntegrationTestFixture _fixture;
public MalwareBlockerIntegrationTests(IntegrationTestFixture fixture)
{
_fixture = fixture;
_fixture.Reset();
}
public void Dispose()
{
Striker.RecurringHashes.Clear();
}
private MalwareBlockerJob CreateSut()
{
return new MalwareBlockerJob(
Substitute.For<ILogger<MalwareBlockerJob>>(),
_fixture.DataContext,
_fixture.Cache,
_fixture.MessageBus,
_fixture.ArrClientFactory,
_fixture.ArrQueueIterator,
_fixture.DownloadServiceFactory,
_fixture.BlocklistProvider,
_fixture.EventPublisher);
}
[Fact]
public async Task MalwareDetected_RemovesFromArr_SavesEvent_SendsNotification()
{
// Arrange
var instance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
var downloadClient = TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
// Enable Radarr blocklist
var contentBlockerConfig = await _fixture.DataContext.ContentBlockerConfigs.FirstAsync();
contentBlockerConfig.Radarr = new BlocklistSettings { Enabled = true };
await _fixture.DataContext.SaveChangesAsync();
var record = CreateQueueRecord(movieId: 77);
_fixture.SetupArrQueueIterator(record);
_fixture.ArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
_fixture.ArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<IReadOnlyList<string>>())
.Returns(new BlockFilesResult
{
Found = true,
ShouldRemove = true,
DeleteReason = DeleteReason.AllFilesBlocked,
IsPrivate = false
});
_fixture.DownloadServiceFactory.GetDownloadService(Arg.Any<Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig>())
.Returns(mockDownloadService);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert Phase 1: Remove request was published
var removeRequests = _fixture.GetCapturedRemoveRequests();
removeRequests.Count.ShouldBe(1);
// Process through real QueueItemRemover
_fixture.ArrClient.DeleteQueueItemAsync(
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
.Returns(Task.CompletedTask);
await _fixture.ProcessCapturedRemoveRequestsAsync();
// Assert: Arr client was told to delete with AllFilesBlocked reason
await _fixture.ArrClient.Received(1).DeleteQueueItemAsync(
Arg.Is<ArrInstance>(i => i.Id == instance.Id),
Arg.Is<QueueRecord>(r => r.DownloadId == record.DownloadId),
true,
DeleteReason.AllFilesBlocked);
// Assert: Full event property verification
var events = await _fixture.EventsContext.Events.ToListAsync();
events.Count.ShouldBe(2);
// DownloadMarkedForDeletion event
var markedEvent = events.First(e => e.EventType == EventType.DownloadMarkedForDeletion);
markedEvent.Message.ShouldBe("Download marked for deletion");
markedEvent.Severity.ShouldBe(EventSeverity.Important);
markedEvent.JobRunId.ShouldBe(_fixture.JobRunId);
markedEvent.ArrInstanceId.ShouldBe(instance.Id);
markedEvent.DownloadClientId.ShouldBe(mockDownloadService.ClientConfig.Id);
markedEvent.IsDryRun.ShouldBe(false);
markedEvent.StrikeId.ShouldBeNull();
markedEvent.TrackingId.ShouldBeNull();
markedEvent.SearchStatus.ShouldBeNull();
markedEvent.CompletedAt.ShouldBeNull();
markedEvent.CycleId.ShouldBeNull();
markedEvent.Data.ShouldNotBeNull();
using (var markedData = JsonDocument.Parse(markedEvent.Data!))
{
markedData.RootElement.GetProperty("itemName").GetString().ShouldBe("Suspicious.Movie.2024.1080p");
markedData.RootElement.GetProperty("hash").GetString().ShouldBe("MALWARE_HASH_789");
}
// QueueItemDeleted event
var deletedEvent = events.First(e => e.EventType == EventType.QueueItemDeleted);
deletedEvent.Message.ShouldBe("Deleting item from queue with reason: AllFilesBlocked");
deletedEvent.Severity.ShouldBe(EventSeverity.Important);
deletedEvent.JobRunId.ShouldBe(_fixture.JobRunId);
deletedEvent.ArrInstanceId.ShouldBe(instance.Id);
deletedEvent.DownloadClientId.ShouldBe(mockDownloadService.ClientConfig.Id);
deletedEvent.IsDryRun.ShouldBe(false);
deletedEvent.StrikeId.ShouldBeNull();
deletedEvent.TrackingId.ShouldBeNull();
deletedEvent.SearchStatus.ShouldBeNull();
deletedEvent.CompletedAt.ShouldBeNull();
deletedEvent.CycleId.ShouldBeNull();
deletedEvent.Data.ShouldNotBeNull();
using (var deletedData = JsonDocument.Parse(deletedEvent.Data!))
{
deletedData.RootElement.GetProperty("itemName").GetString().ShouldBe("Suspicious.Movie.2024.1080p");
deletedData.RootElement.GetProperty("hash").GetString().ShouldBe("MALWARE_HASH_789");
deletedData.RootElement.GetProperty("removeFromClient").GetBoolean().ShouldBe(true);
deletedData.RootElement.GetProperty("deleteReason").GetString().ShouldBe("AllFilesBlocked");
}
// Assert: Notification sent
await _fixture.NotificationPublisher.Received(1)
.NotifyQueueItemDeleted(true, DeleteReason.AllFilesBlocked);
// Assert: Search queue item added for replacement search
var searchItems = await _fixture.DataContext.SearchQueue.ToListAsync();
searchItems.Count.ShouldBe(1);
searchItems[0].ItemId.ShouldBe(77);
}
[Fact]
public async Task NoBlocklistsEnabled_ExitsEarly_NothingProcessed()
{
// Arrange: Default seed data has all blocklists disabled
TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert: Blocklists were never loaded, no processing happened
await _fixture.BlocklistProvider.DidNotReceive().LoadBlocklistsAsync();
_fixture.GetCapturedRemoveRequests().ShouldBeEmpty();
var events = await _fixture.EventsContext.Events.ToListAsync();
events.ShouldBeEmpty();
}
[Fact]
public async Task PrivateTorrent_DeletePrivateFalse_RemoveFromClientIsFalse()
{
// Arrange
var instance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
// Enable Radarr blocklist, but keep DeletePrivate = false (default)
var contentBlockerConfig = await _fixture.DataContext.ContentBlockerConfigs.FirstAsync();
contentBlockerConfig.Radarr = new BlocklistSettings { Enabled = true };
contentBlockerConfig.DeletePrivate = false;
await _fixture.DataContext.SaveChangesAsync();
var record = CreateQueueRecord(movieId: 88);
_fixture.SetupArrQueueIterator(record);
_fixture.ArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
_fixture.ArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService.BlockUnwantedFilesAsync(Arg.Any<string>(), Arg.Any<IReadOnlyList<string>>())
.Returns(new BlockFilesResult
{
Found = true,
ShouldRemove = true,
DeleteReason = DeleteReason.AllFilesBlocked,
IsPrivate = true
});
_fixture.DownloadServiceFactory.GetDownloadService(Arg.Any<Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig>())
.Returns(mockDownloadService);
var sut = CreateSut();
// Act
await sut.ExecuteAsync();
// Assert: Remove request has RemoveFromClient = false
_fixture.GetCapturedRemoveRequests().Count.ShouldBe(1);
_fixture.ArrClient.DeleteQueueItemAsync(
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
.Returns(Task.CompletedTask);
await _fixture.ProcessCapturedRemoveRequestsAsync();
await _fixture.ArrClient.Received(1).DeleteQueueItemAsync(
Arg.Any<ArrInstance>(),
Arg.Any<QueueRecord>(),
false,
DeleteReason.AllFilesBlocked);
// Full event property verification
var events = await _fixture.EventsContext.Events.ToListAsync();
var deletedEvent = events.First(e => e.EventType == EventType.QueueItemDeleted);
deletedEvent.Message.ShouldBe("Deleting item from queue with reason: AllFilesBlocked");
deletedEvent.Severity.ShouldBe(EventSeverity.Important);
deletedEvent.JobRunId.ShouldBe(_fixture.JobRunId);
deletedEvent.ArrInstanceId.ShouldBe(instance.Id);
deletedEvent.IsDryRun.ShouldBe(false);
deletedEvent.StrikeId.ShouldBeNull();
deletedEvent.SearchStatus.ShouldBeNull();
deletedEvent.Data.ShouldNotBeNull();
using (var data = JsonDocument.Parse(deletedEvent.Data!))
{
data.RootElement.GetProperty("itemName").GetString().ShouldBe("Suspicious.Movie.2024.1080p");
data.RootElement.GetProperty("hash").GetString().ShouldBe("MALWARE_HASH_789");
data.RootElement.GetProperty("removeFromClient").GetBoolean().ShouldBe(false);
data.RootElement.GetProperty("deleteReason").GetString().ShouldBe("AllFilesBlocked");
}
await _fixture.NotificationPublisher.Received(1)
.NotifyQueueItemDeleted(false, DeleteReason.AllFilesBlocked);
}
private static QueueRecord CreateQueueRecord(
long movieId = 1,
string downloadId = "MALWARE_HASH_789",
string title = "Suspicious.Movie.2024.1080p")
{
return new QueueRecord
{
Id = 1,
Title = title,
Protocol = "torrent",
DownloadId = downloadId,
MovieId = movieId,
Status = "warning",
StatusMessages = []
};
}
}