From 3c52d49ea63ded2c94183de96a6da0fdbafefcdb Mon Sep 17 00:00:00 2001 From: Flaminel Date: Sun, 23 Aug 2026 12:37:53 +0300 Subject: [PATCH] added unit tests --- .../Jobs/DownloadCleanerOrphanedFilesTests.cs | 160 +++++++++++++++++- .../Features/Jobs/DownloadCleanerTests.cs | 33 ++++ 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerOrphanedFilesTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerOrphanedFilesTests.cs index 6b1f0925..f48388b3 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerOrphanedFilesTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerOrphanedFilesTests.cs @@ -497,17 +497,17 @@ public sealed class DownloadCleanerOrphanedFilesTests : IDisposable File.Exists(fileThatWouldBeMoved).ShouldBeTrue(); (Directory.Exists(orphanedDir) && Directory.GetFiles(orphanedDir).Length > 0).ShouldBeFalse(); _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Error, "Failed to get torrents").ShouldBeTrue(); - _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Warning, "torrents are unavailable or empty").ShouldBeTrue(); + _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Warning, "torrents are unavailable").ShouldBeTrue(); } [Fact] - public async Task OrphanedFiles_DownloadClientReturnsZeroTorrents_ScanIsSkipped() + public async Task OrphanedFiles_DownloadClientReturnsZeroTorrents_ScanStillRuns() { string scanDir = Path.Combine(_tempRoot, "downloads"); string orphanedDir = Path.Combine(_tempRoot, "orphaned"); Directory.CreateDirectory(scanDir); - string fileThatWouldBeMoved = Path.Combine(scanDir, "would-be-orphan.mkv"); - File.WriteAllText(fileThatWouldBeMoved, "x"); + string orphan = Path.Combine(scanDir, "orphan.mkv"); + File.WriteAllText(orphan, "x"); TestDataContextFactory.AddDownloadClient(_fixture.DataContext); DownloadClientConfig dbClient = _fixture.DataContext.DownloadClients.First(); @@ -521,10 +521,154 @@ public sealed class DownloadCleanerOrphanedFilesTests : IDisposable DownloadCleaner sut = CreateSut(); await ExecuteWithTimeAdvance(sut); - File.Exists(fileThatWouldBeMoved).ShouldBeTrue(); - (Directory.Exists(orphanedDir) && Directory.GetFiles(orphanedDir).Length > 0).ShouldBeFalse(); - _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Debug, "No torrents found").ShouldBeTrue(); - _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Warning, "torrents are unavailable or empty").ShouldBeTrue(); + File.Exists(orphan).ShouldBeFalse(); + File.Exists(Path.Combine(orphanedDir, "orphan.mkv")).ShouldBeTrue(); + _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Warning, "no torrents reported").ShouldBeTrue(); + _fixture.OrphanedFilesLogger.HasNoLogContaining(LogLevel.Warning, "torrents are unavailable").ShouldBeTrue(); + } + + [Fact] + public async Task OrphanedFiles_ZeroTorrents_PurgeStillRuns() + { + _fixture.TimeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero)); + + string scanDir = Path.Combine(_tempRoot, "downloads"); + string orphanedDir = Path.Combine(_tempRoot, "orphaned"); + Directory.CreateDirectory(scanDir); + Directory.CreateDirectory(orphanedDir); + + string agedOrphan = Path.Combine(orphanedDir, "aged.bin"); + File.WriteAllText(agedOrphan, "old"); + File.SetLastWriteTimeUtc(agedOrphan, new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + + TestDataContextFactory.AddDownloadClient(_fixture.DataContext); + DownloadClientConfig dbClient = _fixture.DataContext.DownloadClients.First(); + TestDataContextFactory.AddOrphanedFilesConfig( + _fixture.DataContext, dbClient, + scanDirectories: [scanDir], + orphanedDirectory: orphanedDir, + purgeAfterHours: 24); + + SetupDownloadService(dbClient, []); + + DownloadCleaner sut = CreateSut(); + await ExecuteWithTimeAdvance(sut); + + File.Exists(agedOrphan).ShouldBeFalse(); + } + + [Fact] + public async Task OrphanedFiles_ZeroTorrents_ExcludePatternsAndMinFileAgeStillApply() + { + _fixture.TimeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero)); + + string scanDir = Path.Combine(_tempRoot, "downloads"); + string orphanedDir = Path.Combine(_tempRoot, "orphaned"); + Directory.CreateDirectory(scanDir); + + DateTime aged = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + string agedOrphan = Path.Combine(scanDir, "aged.bin"); + File.WriteAllText(agedOrphan, "x"); + File.SetLastWriteTimeUtc(agedOrphan, aged); + File.SetCreationTimeUtc(agedOrphan, aged); + + string agedExcluded = Path.Combine(scanDir, "metadata.nfo"); + File.WriteAllText(agedExcluded, "x"); + File.SetLastWriteTimeUtc(agedExcluded, aged); + File.SetCreationTimeUtc(agedExcluded, aged); + + string freshOrphan = Path.Combine(scanDir, "fresh.bin"); + File.WriteAllText(freshOrphan, "x"); + File.SetLastWriteTimeUtc(freshOrphan, new DateTime(2026, 1, 1, 11, 45, 0, DateTimeKind.Utc)); + File.SetCreationTimeUtc(freshOrphan, new DateTime(2026, 1, 1, 11, 45, 0, DateTimeKind.Utc)); + + TestDataContextFactory.AddDownloadClient(_fixture.DataContext); + DownloadClientConfig dbClient = _fixture.DataContext.DownloadClients.First(); + TestDataContextFactory.AddOrphanedFilesConfig( + _fixture.DataContext, dbClient, + scanDirectories: [scanDir], + orphanedDirectory: orphanedDir, + excludePatterns: ["*.nfo"], + minFileAgeHours: 1); + + SetupDownloadService(dbClient, []); + + DownloadCleaner sut = CreateSut(); + await ExecuteWithTimeAdvance(sut); + + File.Exists(agedOrphan).ShouldBeFalse(); + File.Exists(Path.Combine(orphanedDir, "aged.bin")).ShouldBeTrue(); + File.Exists(agedExcluded).ShouldBeTrue(); + File.Exists(freshOrphan).ShouldBeTrue(); + } + + [Fact] + public async Task OrphanedFiles_ZeroTorrents_OrphanedDirectoryItselfIsNotMoved() + { + string scanDir = Path.Combine(_tempRoot, "downloads"); + string orphanedDir = Path.Combine(scanDir, "orphaned"); + Directory.CreateDirectory(orphanedDir); + string alreadyQuarantined = Path.Combine(orphanedDir, "previous.bin"); + File.WriteAllText(alreadyQuarantined, "x"); + + TestDataContextFactory.AddDownloadClient(_fixture.DataContext); + DownloadClientConfig dbClient = _fixture.DataContext.DownloadClients.First(); + TestDataContextFactory.AddOrphanedFilesConfig( + _fixture.DataContext, dbClient, + scanDirectories: [scanDir], + orphanedDirectory: orphanedDir); + + SetupDownloadService(dbClient, []); + + DownloadCleaner sut = CreateSut(); + await ExecuteWithTimeAdvance(sut); + + Directory.Exists(orphanedDir).ShouldBeTrue(); + File.Exists(alreadyQuarantined).ShouldBeTrue(); + Directory.Exists(Path.Combine(orphanedDir, "orphaned")).ShouldBeFalse(); + } + + [Fact] + public async Task OrphanedFiles_ThrowingClientAndEmptyClient_OnlyEmptyClientIsScanned() + { + string scanDirA = Path.Combine(_tempRoot, "downloads-a"); + string orphanedDirA = Path.Combine(_tempRoot, "orphaned-a"); + string scanDirB = Path.Combine(_tempRoot, "downloads-b"); + string orphanedDirB = Path.Combine(_tempRoot, "orphaned-b"); + Directory.CreateDirectory(scanDirA); + Directory.CreateDirectory(scanDirB); + string fileInA = Path.Combine(scanDirA, "a-orphan.mkv"); + string fileInB = Path.Combine(scanDirB, "b-orphan.mkv"); + File.WriteAllText(fileInA, "x"); + File.WriteAllText(fileInB, "x"); + + DownloadClientConfig clientA = TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "Client A"); + DownloadClientConfig clientB = TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "Client B"); + TestDataContextFactory.AddOrphanedFilesConfig( + _fixture.DataContext, clientA, + scanDirectories: [scanDirA], + orphanedDirectory: orphanedDirA); + TestDataContextFactory.AddOrphanedFilesConfig( + _fixture.DataContext, clientB, + scanDirectories: [scanDirB], + orphanedDirectory: orphanedDirB); + + IDownloadService svcA = Substitute.For(); + svcA.ClientConfig.Returns(clientA); + svcA.LoginAsync().Returns(Task.CompletedTask); + svcA.GetSeedingDownloads().Returns([]); + svcA.GetAllTorrentsLite().ThrowsAsync(new HttpRequestException("connection refused")); + _fixture.DownloadServiceFactory.GetDownloadService(clientA).Returns(svcA); + + SetupDownloadService(clientB, []); + + DownloadCleaner sut = CreateSut(); + await ExecuteWithTimeAdvance(sut); + + File.Exists(fileInA).ShouldBeTrue(); + File.Exists(fileInB).ShouldBeFalse(); + Directory.GetFiles(orphanedDirB).ShouldContain(f => Path.GetFileName(f) == "b-orphan.mkv"); } [Fact] diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs index 416abac7..ad7db30d 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs @@ -153,6 +153,39 @@ public class DownloadCleanerTests : IDisposable _logger.HasLogContaining(LogLevel.Information, "No seeding downloads found").ShouldBeTrue(); } + [Fact] + public async Task ExecuteInternalAsync_WhenNoSeedingDownloadsFound_OrphanedFilesStillRuns() + { + // Arrange + DownloadClientConfig client = TestDataContextFactory.AddDownloadClient(_fixture.DataContext); + TestDataContextFactory.AddOrphanedFilesConfig( + _fixture.DataContext, client, + scanDirectories: [Path.Combine(Path.GetTempPath(), "cleanuparr-tests", Guid.NewGuid().ToString("N"))], + orphanedDirectory: Path.Combine(Path.GetTempPath(), "cleanuparr-tests", Guid.NewGuid().ToString("N"))); + + // Bound to the persisted client, because the orphaned scan matches configs by client id + IDownloadService mockDownloadService = Substitute.For(); + mockDownloadService.ClientConfig.Returns(client); + mockDownloadService.LoginAsync().Returns(Task.CompletedTask); + mockDownloadService.GetSeedingDownloads().Returns([]); + mockDownloadService.GetAllTorrentsLite().Returns([]); + mockDownloadService.GetClaimedPathsAsync(Arg.Any>()) + .Returns(Task.FromResult>([])); + + _fixture.DownloadServiceFactory + .GetDownloadService(Arg.Any()) + .Returns(mockDownloadService); + + DownloadCleaner sut = CreateSut(); + + // Act + await sut.ExecuteAsync(); + + // Assert - the scan reached its directory loop, so the orphaned pass is not gated on seeding downloads + _logger.HasLogContaining(LogLevel.Information, "No seeding downloads found").ShouldBeTrue(); + _fixture.OrphanedFilesLogger.HasLogContainingAtLeastOnce(LogLevel.Warning, "Scan directory does not exist").ShouldBeTrue(); + } + [Fact] public async Task ExecuteInternalAsync_FiltersOutIgnoredDownloads() {