From 1ca935b62bb7c55b29962ca7fe08b1c47c39118d Mon Sep 17 00:00:00 2001 From: Flaminel Date: Fri, 15 May 2026 03:53:05 +0300 Subject: [PATCH] Fix Deluge crashing when removing malware (#308) --- .../DownloadClient/DelugeServiceTests.cs | 114 ++++++++++++++++++ .../DownloadClient/Deluge/DelugeServiceCB.cs | 3 +- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs index 4d6488cf..3247dd79 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceTests.cs @@ -1,7 +1,11 @@ +using System.Collections.Concurrent; +using System.Text.RegularExpressions; using Cleanuparr.Domain.Entities.Deluge.Response; using Cleanuparr.Domain.Enums; +using Cleanuparr.Infrastructure.Features.Context; using Cleanuparr.Infrastructure.Features.DownloadClient; using Cleanuparr.Infrastructure.Features.DownloadClient.Deluge; +using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker; using NSubstitute; using Shouldly; using Xunit; @@ -542,4 +546,114 @@ public class DelugeServiceTests : IClassFixture result.ChangeCategory.ShouldBeTrue(); } } + + public class BlockUnwantedFilesAsyncScenarios : DelugeServiceTests + { + public BlockUnwantedFilesAsyncScenarios(DelugeServiceFixture fixture) : base(fixture) + { + } + + private void SetMalwareBlockerContext() + { + ContextProvider.Set(new ContentBlockerConfig()); + ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr); + + _fixture.BlocklistProvider + .GetBlocklistType(Arg.Any()) + .Returns(BlocklistType.Blacklist); + _fixture.BlocklistProvider + .GetPatterns(Arg.Any()) + .Returns(new ConcurrentBag()); + _fixture.BlocklistProvider + .GetRegexes(Arg.Any()) + .Returns(new ConcurrentBag()); + } + + private static DownloadStatus MakeDownloadStatus(string hash) => new() + { + Hash = hash, + Name = "Malware Torrent", + State = DelugeState.Downloading, + Private = false, + DownloadSpeed = 1000, + Trackers = new List(), + DownloadLocation = "/downloads", + }; + + [Fact] + public async Task AllFilesAreMalware_DoesNotCallChangeFilesPriority_AndMarksForRemoval() + { + const string hash = "all-malware-hash"; + var sut = _fixture.CreateSut(); + SetMalwareBlockerContext(); + + _fixture.ClientWrapper + .GetTorrentStatus(hash) + .Returns(MakeDownloadStatus(hash)); + + _fixture.ClientWrapper + .GetTorrentFiles(hash) + .Returns(new DelugeContents + { + Contents = new Dictionary + { + { "malware.exe", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "malware.exe" } }, + }, + }); + + _fixture.FilenameEvaluator + .IsValid(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any>()) + .Returns(false); + + var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty()); + + result.Found.ShouldBeTrue(); + result.ShouldRemove.ShouldBeTrue(); + result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked); + + await _fixture.ClientWrapper + .DidNotReceive() + .ChangeFilesPriority(Arg.Any(), Arg.Any>()); + } + + [Fact] + public async Task PartialMalware_CallsChangeFilesPriority_AndDoesNotMarkForRemoval() + { + const string hash = "partial-malware-hash"; + var sut = _fixture.CreateSut(); + SetMalwareBlockerContext(); + + _fixture.ClientWrapper + .GetTorrentStatus(hash) + .Returns(MakeDownloadStatus(hash)); + + _fixture.ClientWrapper + .GetTorrentFiles(hash) + .Returns(new DelugeContents + { + Contents = new Dictionary + { + { "movie.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "movie.mkv" } }, + { "malware.exe", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 1, Path = "malware.exe" } }, + }, + }); + + _fixture.FilenameEvaluator + .IsValid(Arg.Is(name => name.EndsWith("malware.exe")), Arg.Any(), Arg.Any>(), Arg.Any>()) + .Returns(false); + _fixture.FilenameEvaluator + .IsValid(Arg.Is(name => name.EndsWith("movie.mkv")), Arg.Any(), Arg.Any>(), Arg.Any>()) + .Returns(true); + + var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty()); + + result.Found.ShouldBeTrue(); + result.ShouldRemove.ShouldBeFalse(); + result.DeleteReason.ShouldBe(DeleteReason.None); + + await _fixture.ClientWrapper + .Received(1) + .ChangeFilesPriority(hash, Arg.Is>(p => p.Count == 2 && p[0] == 1 && p[1] == 0)); + } + } } diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceCB.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceCB.cs index 7776f414..51b11e0b 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceCB.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceCB.cs @@ -120,8 +120,9 @@ public partial class DelugeService _logger.LogDebug("All files are blocked for {name}", download.Name); result.ShouldRemove = true; result.DeleteReason = DeleteReason.AllFilesBlocked; + return result; } - + _logger.LogDebug("Marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name); await _dryRunInterceptor.InterceptAsync(ChangeFilesPriority, hash, sortedPriorities);