mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-06-09 14:25:43 -04:00
Add option to remove malware if any file is blocked (#621)
This commit is contained in:
@@ -18,6 +18,8 @@ public sealed record UpdateMalwareBlockerConfigRequest
|
||||
|
||||
public bool ProcessNoContentId { get; init; }
|
||||
|
||||
public bool DeleteIfAnyFileBlocked { get; init; }
|
||||
|
||||
public BlocklistSettings Sonarr { get; init; } = new();
|
||||
|
||||
public BlocklistSettings Radarr { get; init; } = new();
|
||||
@@ -38,6 +40,7 @@ public sealed record UpdateMalwareBlockerConfigRequest
|
||||
config.IgnorePrivate = IgnorePrivate;
|
||||
config.DeletePrivate = DeletePrivate;
|
||||
config.ProcessNoContentId = ProcessNoContentId;
|
||||
config.DeleteIfAnyFileBlocked = DeleteIfAnyFileBlocked;
|
||||
config.Sonarr = Sonarr;
|
||||
config.Radarr = Radarr;
|
||||
config.Lidarr = Lidarr;
|
||||
|
||||
@@ -11,4 +11,5 @@ public enum DeleteReason
|
||||
AllFilesSkipped,
|
||||
AllFilesSkippedByQBit,
|
||||
AllFilesBlocked,
|
||||
AtLeastOneFileBlocked,
|
||||
}
|
||||
@@ -553,9 +553,9 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
{
|
||||
}
|
||||
|
||||
private void SetMalwareBlockerContext()
|
||||
private void SetMalwareBlockerContext(ContentBlockerConfig? config = null)
|
||||
{
|
||||
ContextProvider.Set(new ContentBlockerConfig());
|
||||
ContextProvider.Set(config ?? new ContentBlockerConfig());
|
||||
ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr);
|
||||
|
||||
_fixture.BlocklistProvider
|
||||
@@ -655,5 +655,45 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
.Received(1)
|
||||
.ChangeFilesPriority(hash, Arg.Is<List<int>>(p => p.Count == 2 && p[0] == 1 && p[1] == 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_WithDeleteIfAnyFileBlocked_MarksForRemoval_AndSkipsChangeFilesPriority()
|
||||
{
|
||||
const string hash = "partial-malware-any-hash";
|
||||
DelugeService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "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<string>(name => name.EndsWith("malware.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AtLeastOneFileBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using NSubstitute;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -1128,4 +1131,218 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockUnwantedFilesAsyncScenarios : QBitServiceTests
|
||||
{
|
||||
public BlockUnwantedFilesAsyncScenarios(QBitServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
private void SetMalwareBlockerContext(ContentBlockerConfig? config = null)
|
||||
{
|
||||
ContextProvider.Set(config ?? new ContentBlockerConfig());
|
||||
ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr);
|
||||
|
||||
_fixture.BlocklistProvider
|
||||
.GetBlocklistType(Arg.Any<InstanceType>())
|
||||
.Returns(BlocklistType.Blacklist);
|
||||
_fixture.BlocklistProvider
|
||||
.GetPatterns(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<string>());
|
||||
_fixture.BlocklistProvider
|
||||
.GetRegexes(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<Regex>());
|
||||
}
|
||||
|
||||
private static TorrentInfo MakeTorrentInfo(string hash) => new()
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Malware Torrent",
|
||||
State = TorrentState.Downloading,
|
||||
DownloadSpeed = 1000,
|
||||
};
|
||||
|
||||
private static TorrentProperties MakeTorrentProperties(bool isPrivate = false) => new()
|
||||
{
|
||||
AdditionalData = new Dictionary<string, JToken>
|
||||
{
|
||||
{ "is_private", JToken.FromObject(isPrivate) },
|
||||
},
|
||||
};
|
||||
|
||||
private void StubClient(string hash, IReadOnlyList<TorrentContent> files, bool isPrivate = false)
|
||||
{
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { MakeTorrentInfo(hash) });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(MakeTorrentProperties(isPrivate));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(files);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_MarksForRemoval_WithAllFilesBlockedReason()
|
||||
{
|
||||
const string hash = "all-malware-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "malware.exe", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_CallsSetFilePriority_AndDoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "partial-malware-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "movie.mkv", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
new TorrentContent { Name = "installer.exe", Index = 1, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 1, TorrentContentPriority.Skip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_WithDeleteIfAnyFileBlocked_MarksForRemoval_AndSkipsSetFilePriority()
|
||||
{
|
||||
const string hash = "partial-malware-any-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "movie.mkv", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
new TorrentContent { Name = "installer.exe", Index = 1, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AtLeastOneFileBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<TorrentContentPriority>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoUnwantedFiles_DoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "clean-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "movie.mkv", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<TorrentContentPriority>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlreadySkippedFile_DoesNotTriggerEarlyReturn_WhenDeleteIfAnyFileBlocked()
|
||||
{
|
||||
const string hash = "already-skipped-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "movie.mkv", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
new TorrentContent { Name = "installer.exe", Index = 1, Priority = TorrentContentPriority.Skip },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAlreadySkipped_NoNewMalware_DoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "all-skipped-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "movie.mkv", Index = 0, Priority = TorrentContentPriority.Skip },
|
||||
new TorrentContent { Name = "installer.exe", Index = 1, Priority = TorrentContentPriority.Skip },
|
||||
]);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities.RTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Shouldly;
|
||||
@@ -768,4 +773,176 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockUnwantedFilesAsyncScenarios : RTorrentServiceTests
|
||||
{
|
||||
public BlockUnwantedFilesAsyncScenarios(RTorrentServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
private void SetMalwareBlockerContext(ContentBlockerConfig? config = null)
|
||||
{
|
||||
ContextProvider.Set(config ?? new ContentBlockerConfig());
|
||||
ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr);
|
||||
|
||||
_fixture.BlocklistProvider
|
||||
.GetBlocklistType(Arg.Any<InstanceType>())
|
||||
.Returns(BlocklistType.Blacklist);
|
||||
_fixture.BlocklistProvider
|
||||
.GetPatterns(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<string>());
|
||||
_fixture.BlocklistProvider
|
||||
.GetRegexes(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<Regex>());
|
||||
}
|
||||
|
||||
private static RTorrentTorrent MakeDownload(string hash, bool isPrivate = false) => new()
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Malware Torrent",
|
||||
IsPrivate = isPrivate ? 1 : 0,
|
||||
State = 1,
|
||||
Complete = 0,
|
||||
DownRate = 1000,
|
||||
SizeBytes = 1000,
|
||||
CompletedBytes = 500,
|
||||
};
|
||||
|
||||
private void StubClient(string hash, IReadOnlyList<RTorrentFile> files, bool isPrivate = false)
|
||||
{
|
||||
_fixture.ClientWrapper.GetTorrentAsync(hash).Returns(MakeDownload(hash, isPrivate));
|
||||
_fixture.ClientWrapper.GetTrackersAsync(hash).Returns(new List<string>());
|
||||
_fixture.ClientWrapper.GetTorrentFilesAsync(hash).Returns(files.ToList());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_MarksForRemoval_WithAllFilesBlockedReason()
|
||||
{
|
||||
const string hash = "ALL-MALWARE-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new RTorrentFile { Index = 0, Path = "malware.exe", Priority = 1 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_CallsSetFilePriority_AndDoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "PARTIAL-MALWARE-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 },
|
||||
new RTorrentFile { Index = 1, Path = "installer.exe", Priority = 1 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 1, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_WithDeleteIfAnyFileBlocked_MarksForRemoval_AndSkipsSetFilePriority()
|
||||
{
|
||||
const string hash = "PARTIAL-MALWARE-ANY-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 },
|
||||
new RTorrentFile { Index = 1, Path = "installer.exe", Priority = 1 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AtLeastOneFileBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoUnwantedFiles_DoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "CLEAN-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlreadySkippedFile_DoesNotTriggerEarlyReturn_WhenDeleteIfAnyFileBlocked()
|
||||
{
|
||||
const string hash = "ALREADY-SKIPPED-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 },
|
||||
new RTorrentFile { Index = 1, Path = "installer.exe", Priority = 0 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using NSubstitute;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -528,4 +533,163 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockUnwantedFilesAsyncScenarios : TransmissionServiceTests
|
||||
{
|
||||
public BlockUnwantedFilesAsyncScenarios(TransmissionServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
private void SetMalwareBlockerContext(ContentBlockerConfig? config = null)
|
||||
{
|
||||
ContextProvider.Set(config ?? new ContentBlockerConfig());
|
||||
ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr);
|
||||
|
||||
_fixture.BlocklistProvider
|
||||
.GetBlocklistType(Arg.Any<InstanceType>())
|
||||
.Returns(BlocklistType.Blacklist);
|
||||
_fixture.BlocklistProvider
|
||||
.GetPatterns(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<string>());
|
||||
_fixture.BlocklistProvider
|
||||
.GetRegexes(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<Regex>());
|
||||
}
|
||||
|
||||
private void StubClient(string hash, (string Name, bool Wanted)[] files, bool isPrivate = false)
|
||||
{
|
||||
TorrentInfo torrentInfo = new()
|
||||
{
|
||||
Id = 42,
|
||||
HashString = hash,
|
||||
Name = "Malware Torrent",
|
||||
Status = 4,
|
||||
IsPrivate = isPrivate,
|
||||
Files = files.Select(f => new TransmissionTorrentFiles { Name = f.Name }).ToArray(),
|
||||
FileStats = files.Select(f => new TransmissionTorrentFileStats { Wanted = f.Wanted }).ToArray(),
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(new TransmissionTorrents { Torrents = new[] { torrentInfo } });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_MarksForRemoval_WithAllFilesBlockedReason()
|
||||
{
|
||||
const string hash = "all-malware-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [("malware.exe", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_CallsTorrentSet_AndDoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "partial-malware-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [("movie.mkv", true), ("installer.exe", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_WithDeleteIfAnyFileBlocked_MarksForRemoval_AndSkipsTorrentSet()
|
||||
{
|
||||
const string hash = "partial-malware-any-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash, [("movie.mkv", true), ("installer.exe", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AtLeastOneFileBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoUnwantedFiles_DoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "clean-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [("movie.mkv", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlreadyUnwantedFile_DoesNotTriggerEarlyReturn_WhenDeleteIfAnyFileBlocked()
|
||||
{
|
||||
const string hash = "already-skipped-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash, [("movie.mkv", true), ("installer.exe", false)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Shouldly;
|
||||
@@ -711,4 +716,178 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockUnwantedFilesAsyncScenarios : UTorrentServiceTests
|
||||
{
|
||||
public BlockUnwantedFilesAsyncScenarios(UTorrentServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
private void SetMalwareBlockerContext(ContentBlockerConfig? config = null)
|
||||
{
|
||||
ContextProvider.Set(config ?? new ContentBlockerConfig());
|
||||
ContextProvider.Set(nameof(InstanceType), (object)InstanceType.Sonarr);
|
||||
|
||||
_fixture.BlocklistProvider
|
||||
.GetBlocklistType(Arg.Any<InstanceType>())
|
||||
.Returns(BlocklistType.Blacklist);
|
||||
_fixture.BlocklistProvider
|
||||
.GetPatterns(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<string>());
|
||||
_fixture.BlocklistProvider
|
||||
.GetRegexes(Arg.Any<InstanceType>())
|
||||
.Returns(new ConcurrentBag<Regex>());
|
||||
}
|
||||
|
||||
private void StubClient(string hash, IReadOnlyList<UTorrentFile> files, bool isPrivate = false)
|
||||
{
|
||||
UTorrentItem item = new()
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Malware Torrent",
|
||||
Status = 9,
|
||||
DownloadSpeed = 1000,
|
||||
};
|
||||
UTorrentProperties properties = new()
|
||||
{
|
||||
Hash = hash,
|
||||
Pex = isPrivate ? -1 : 0,
|
||||
Trackers = string.Empty,
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper.GetTorrentAsync(hash).Returns(item);
|
||||
_fixture.ClientWrapper.GetTorrentPropertiesAsync(hash).Returns(properties);
|
||||
_fixture.ClientWrapper.GetTorrentFilesAsync(hash).Returns(files.ToList());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_MarksForRemoval_WithAllFilesBlockedReason()
|
||||
{
|
||||
const string hash = "all-malware-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new UTorrentFile { Name = "malware.exe", Index = 0, Priority = 2, Size = 1024, Downloaded = 1024 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_CallsSetFilesPriority_AndDoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "partial-malware-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new UTorrentFile { Name = "movie.mkv", Index = 0, Priority = 2, Size = 32_768, Downloaded = 32_768 },
|
||||
new UTorrentFile { Name = "installer.exe", Index = 1, Priority = 2, Size = 1024, Downloaded = 1024 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilesPriorityAsync(hash, Arg.Is<List<int>>(idx => idx.Count == 1 && idx[0] == 1), 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_WithDeleteIfAnyFileBlocked_MarksForRemoval_AndSkipsSetFilesPriority()
|
||||
{
|
||||
const string hash = "partial-malware-any-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new UTorrentFile { Name = "movie.mkv", Index = 0, Priority = 2, Size = 32_768, Downloaded = 32_768 },
|
||||
new UTorrentFile { Name = "installer.exe", Index = 1, Priority = 2, Size = 1024, Downloaded = 1024 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AtLeastOneFileBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilesPriorityAsync(Arg.Any<string>(), Arg.Any<List<int>>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoUnwantedFiles_DoesNotMarkForRemoval()
|
||||
{
|
||||
const string hash = "clean-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new UTorrentFile { Name = "movie.mkv", Index = 0, Priority = 2, Size = 32_768, Downloaded = 32_768 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilesPriorityAsync(Arg.Any<string>(), Arg.Any<List<int>>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlreadySkippedFile_DoesNotTriggerEarlyReturn_WhenDeleteIfAnyFileBlocked()
|
||||
{
|
||||
const string hash = "already-skipped-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new UTorrentFile { Name = "movie.mkv", Index = 0, Priority = 2, Size = 32_768, Downloaded = 32_768 },
|
||||
new UTorrentFile { Name = "installer.exe", Index = 1, Priority = 0, Size = 1024, Downloaded = 1024 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +362,76 @@ public class MalwareBlockerTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[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.ArrQueueIterator
|
||||
.Iterate(
|
||||
Arg.Any<IArrClient>(),
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
)
|
||||
.Returns(ci =>
|
||||
{
|
||||
var callback = ci.ArgAt<Func<IReadOnlyList<QueueRecord>, Task>>(2);
|
||||
return callback([queueRecord]);
|
||||
});
|
||||
|
||||
IDownloadService 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
|
||||
await _fixture.MessageBus.Received(1).Publish(
|
||||
Arg.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.DeleteReason == DeleteReason.AtLeastOneFileBlocked
|
||||
),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_WhenPrivateAndDeletePrivateFalse_DoesNotRemoveFromClient()
|
||||
{
|
||||
|
||||
@@ -92,6 +92,14 @@ public partial class DelugeService
|
||||
priority = 0;
|
||||
hasPriorityUpdates = true;
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogTrace("File is valid | {file}", file.Path);
|
||||
|
||||
@@ -99,6 +99,15 @@ public partial class QBitService
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Name);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
unwantedFiles.Add(file.Index.Value);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
|
||||
@@ -98,6 +98,15 @@ public partial class RTorrentService
|
||||
hasPriorityUpdates = true;
|
||||
priorityUpdates.Add((file.Index, 0));
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,15 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", download.Files[i].Name);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
unwantedFiles.Add(i);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,14 @@ public partial class UTorrentService
|
||||
totalUnwantedFiles++;
|
||||
fileIndexes.Add(i);
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Name);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2200
code/backend/Cleanuparr.Persistence/Migrations/Data/20260528201320_AddDeleteIfAnyFileBlocked.Designer.cs
generated
Normal file
2200
code/backend/Cleanuparr.Persistence/Migrations/Data/20260528201320_AddDeleteIfAnyFileBlocked.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDeleteIfAnyFileBlocked : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "delete_if_any_file_blocked",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "delete_if_any_file_blocked",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -748,6 +748,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeleteIfAnyFileBlocked")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_if_any_file_blocked");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
@@ -22,6 +22,8 @@ public sealed record ContentBlockerConfig : IJobConfig
|
||||
|
||||
public bool ProcessNoContentId { get; set; }
|
||||
|
||||
public bool DeleteIfAnyFileBlocked { get; set; }
|
||||
|
||||
public BlocklistSettings Sonarr { get; set; } = new();
|
||||
|
||||
public BlocklistSettings Radarr { get; set; } = new();
|
||||
|
||||
@@ -105,6 +105,7 @@ export class DocumentationService {
|
||||
'ignorePrivate': 'ignore-private',
|
||||
'deletePrivate': 'delete-private',
|
||||
'processNoContentId': 'process-downloads-with-no-content-id',
|
||||
'deleteIfAnyFileBlocked': 'delete-if-any-file-is-blocked',
|
||||
'sonarr.enabled': 'enable-blocklist',
|
||||
'sonarr.blocklistPath': 'blocklist-path',
|
||||
'sonarr.blocklistType': 'blocklist-type',
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
<app-toggle label="Process downloads with no content ID" [(checked)]="processNoContentId"
|
||||
hint="Process downloads from the queue that are not linked to any content in the arr app. Cleanuparr will not be able to trigger a search for a replacement when this happens."
|
||||
helpKey="malware-blocker:processNoContentId" />
|
||||
<app-toggle label="Delete if any file is blocked" [(checked)]="deleteIfAnyFileBlocked"
|
||||
hint="When enabled, the entire download will be removed if any file in it matches the blocklist. When disabled, the download is only removed when all of its files match."
|
||||
helpKey="malware-blocker:deleteIfAnyFileBlocked" />
|
||||
|
||||
<div class="form-divider"></div>
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
readonly ignorePrivate = signal(false);
|
||||
readonly deletePrivate = signal(false);
|
||||
readonly processNoContentId = signal(false);
|
||||
readonly deleteIfAnyFileBlocked = signal(false);
|
||||
readonly arrExpanded = signal(false);
|
||||
|
||||
readonly scheduleIntervalOptions = computed(() => {
|
||||
@@ -165,6 +166,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
this.ignorePrivate.set(config.ignorePrivate);
|
||||
this.deletePrivate.set(config.deletePrivate);
|
||||
this.processNoContentId.set(config.processNoContentId);
|
||||
this.deleteIfAnyFileBlocked.set(config.deleteIfAnyFileBlocked);
|
||||
|
||||
const blocklists: Record<string, any> = {};
|
||||
for (const name of ARR_NAMES) {
|
||||
@@ -221,6 +223,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
ignorePrivate: this.ignorePrivate(),
|
||||
deletePrivate: this.deletePrivate(),
|
||||
processNoContentId: this.processNoContentId(),
|
||||
deleteIfAnyFileBlocked: this.deleteIfAnyFileBlocked(),
|
||||
sonarr: { enabled: blocklists['sonarr'].enabled, blocklistPath: blocklists['sonarr'].blocklistPath, blocklistType: blocklists['sonarr'].blocklistType as BlocklistType },
|
||||
radarr: { enabled: blocklists['radarr'].enabled, blocklistPath: blocklists['radarr'].blocklistPath, blocklistType: blocklists['radarr'].blocklistType as BlocklistType },
|
||||
lidarr: { enabled: blocklists['lidarr'].enabled, blocklistPath: blocklists['lidarr'].blocklistPath, blocklistType: blocklists['lidarr'].blocklistType as BlocklistType },
|
||||
@@ -257,6 +260,7 @@ export class MalwareBlockerComponent implements OnInit, HasPendingChanges {
|
||||
ignorePrivate: this.ignorePrivate(),
|
||||
deletePrivate: this.deletePrivate(),
|
||||
processNoContentId: this.processNoContentId(),
|
||||
deleteIfAnyFileBlocked: this.deleteIfAnyFileBlocked(),
|
||||
arrBlocklists: this.arrBlocklists(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface MalwareBlockerConfig {
|
||||
ignorePrivate: boolean;
|
||||
deletePrivate: boolean;
|
||||
processNoContentId: boolean;
|
||||
deleteIfAnyFileBlocked: boolean;
|
||||
sonarr: BlocklistSettings;
|
||||
radarr: BlocklistSettings;
|
||||
lidarr: BlocklistSettings;
|
||||
|
||||
Reference in New Issue
Block a user