mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
1011 lines
34 KiB
C#
1011 lines
34 KiB
C#
using Cleanuparr.Domain.Entities;
|
|
using Cleanuparr.Domain.Entities.RTorrent.Response;
|
|
using Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent;
|
|
using Cleanuparr.Persistence.Models.Configuration;
|
|
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
|
using NSubstitute;
|
|
using NSubstitute.ExceptionExtensions;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
|
|
|
public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
|
{
|
|
private readonly RTorrentServiceFixture _fixture;
|
|
|
|
public RTorrentServiceDCTests(RTorrentServiceFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
_fixture.ResetMocks();
|
|
}
|
|
|
|
public class GetSeedingDownloads_Tests : RTorrentServiceDCTests
|
|
{
|
|
public GetSeedingDownloads_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FiltersSeedingState()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<RTorrentTorrent>
|
|
{
|
|
new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", State = 1, Complete = 1, IsPrivate = 0, Label = "" },
|
|
new RTorrentTorrent { Hash = "HASH2", Name = "Torrent 2", State = 1, Complete = 0, IsPrivate = 0, Label = "" }, // Downloading, not seeding
|
|
new RTorrentTorrent { Hash = "HASH3", Name = "Torrent 3", State = 1, Complete = 1, IsPrivate = 0, Label = "" },
|
|
new RTorrentTorrent { Hash = "HASH4", Name = "Torrent 4", State = 0, Complete = 1, IsPrivate = 0, Label = "" } // Stopped, not seeding
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetAllTorrentsAsync()
|
|
.Returns(downloads);
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTrackersAsync("HASH1")
|
|
.Returns(new List<string>());
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTrackersAsync("HASH3")
|
|
.Returns(new List<string>());
|
|
|
|
// Act
|
|
var result = await sut.GetSeedingDownloads();
|
|
|
|
// Assert - only torrents with State=1 AND Complete=1 should be returned
|
|
result.Count.ShouldBe(2);
|
|
foreach (var item in result) { item.Hash.ShouldNotBeNull(); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReturnsEmptyList_WhenNoTorrents()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
_fixture.ClientWrapper
|
|
.GetAllTorrentsAsync()
|
|
.Returns(new List<RTorrentTorrent>());
|
|
|
|
// Act
|
|
var result = await sut.GetSeedingDownloads();
|
|
|
|
// Assert
|
|
result.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SkipsTorrentsWithEmptyHash()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<RTorrentTorrent>
|
|
{
|
|
new RTorrentTorrent { Hash = "", Name = "No Hash", State = 1, Complete = 1, IsPrivate = 0, Label = "" },
|
|
new RTorrentTorrent { Hash = "HASH1", Name = "Valid Hash", State = 1, Complete = 1, IsPrivate = 0, Label = "" }
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetAllTorrentsAsync()
|
|
.Returns(downloads);
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTrackersAsync("HASH1")
|
|
.Returns(new List<string>());
|
|
|
|
// Act
|
|
var result = await sut.GetSeedingDownloads();
|
|
|
|
// Assert
|
|
result.ShouldHaveSingleItem();
|
|
result[0].Hash.ShouldBe("HASH1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FetchesTrackerDataForSeedingTorrents()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<RTorrentTorrent>
|
|
{
|
|
new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", State = 1, Complete = 1, IsPrivate = 0, Label = "" }
|
|
};
|
|
|
|
var trackers = new List<string>
|
|
{
|
|
"http://tracker.example.com:6969/announce",
|
|
"http://tracker.other.org:8080/announce"
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetAllTorrentsAsync()
|
|
.Returns(downloads);
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTrackersAsync("HASH1")
|
|
.Returns(trackers);
|
|
|
|
// Act
|
|
var result = await sut.GetSeedingDownloads();
|
|
|
|
// Assert
|
|
result.ShouldHaveSingleItem();
|
|
var wrapper = result[0];
|
|
wrapper.TrackerDomains.ShouldContain("tracker.example.com");
|
|
wrapper.TrackerDomains.ShouldContain("tracker.other.org");
|
|
|
|
await _fixture.ClientWrapper.Received(1)
|
|
.GetTrackersAsync("HASH1");
|
|
}
|
|
}
|
|
|
|
public class FilterDownloadsToBeCleanedAsync_Tests : RTorrentServiceDCTests
|
|
{
|
|
public FilterDownloadsToBeCleanedAsync_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public void MatchesCategories()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", Label = "movies" }, null, TimeProvider.System),
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH2", Name = "Torrent 2", Label = "tv" }, null, TimeProvider.System),
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH3", Name = "Torrent 3", Label = "music" }, null, TimeProvider.System)
|
|
};
|
|
|
|
var categories = new List<ISeedingRule>
|
|
{
|
|
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
|
new RTorrentSeedingRule { Name = "tv", Categories = ["tv"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
|
};
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToBeCleanedAsync(downloads, categories);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.Count.ShouldBe(2);
|
|
result.ShouldContain(x => x.Category == "movies");
|
|
result.ShouldContain(x => x.Category == "tv");
|
|
}
|
|
|
|
[Fact]
|
|
public void IsCaseInsensitive()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", Label = "Movies" }, null, TimeProvider.System)
|
|
};
|
|
|
|
var categories = new List<ISeedingRule>
|
|
{
|
|
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
|
};
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToBeCleanedAsync(downloads, categories);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.ShouldHaveSingleItem();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsEmptyList_WhenNoMatches()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", Label = "music" }, null, TimeProvider.System)
|
|
};
|
|
|
|
var categories = new List<ISeedingRule>
|
|
{
|
|
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
|
};
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToBeCleanedAsync(downloads, categories);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsNull_WhenDownloadsNull()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var categories = new List<ISeedingRule>
|
|
{
|
|
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
|
};
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToBeCleanedAsync(null, categories);
|
|
|
|
// Assert
|
|
result.ShouldBeNull();
|
|
}
|
|
}
|
|
|
|
public class FilterDownloadsToChangeCategoryAsync_Tests : RTorrentServiceDCTests
|
|
{
|
|
public FilterDownloadsToChangeCategoryAsync_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public void MatchesCategories()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", Label = "movies" }, null, TimeProvider.System),
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH2", Name = "Torrent 2", Label = "tv" }, null, TimeProvider.System),
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH3", Name = "Torrent 3", Label = "music" }, null, TimeProvider.System)
|
|
};
|
|
|
|
var unlinkedConfig = new UnlinkedConfig { Categories = ["movies", "tv"] };
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToChangeCategoryAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.Count.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void SkipsEmptyHashes()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "", Name = "No Hash", Label = "movies" }, null, TimeProvider.System),
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Valid Hash", Label = "movies" }, null, TimeProvider.System)
|
|
};
|
|
|
|
var unlinkedConfig = new UnlinkedConfig { Categories = ["movies"] };
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToChangeCategoryAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.ShouldHaveSingleItem();
|
|
result[0].Hash.ShouldBe("HASH1");
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsEmpty_WhenNoCategoriesMatch()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", Label = "tv" }, null, TimeProvider.System)
|
|
};
|
|
|
|
var unlinkedConfig = new UnlinkedConfig { Categories = ["movies"] };
|
|
|
|
// Act
|
|
var result = sut.FilterDownloadsToChangeCategoryAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
result.ShouldNotBeNull();
|
|
result.ShouldBeEmpty();
|
|
}
|
|
}
|
|
|
|
public class DeleteDownload_Tests : RTorrentServiceDCTests
|
|
{
|
|
public DeleteDownload_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
|
|
[Fact]
|
|
public async Task NormalizesHashToUppercase()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
var hash = "lowercase";
|
|
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
|
mockTorrent.Hash.Returns(hash);
|
|
mockTorrent.SavePath.Returns("/test/path");
|
|
|
|
_fixture.ClientWrapper
|
|
.DeleteTorrentAsync("LOWERCASE")
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Act
|
|
await sut.DeleteDownload(mockTorrent, deleteSourceFiles: false);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.Received(1)
|
|
.DeleteTorrentAsync("LOWERCASE");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemapsSavePathBeforeDeletingFiles()
|
|
{
|
|
// Arrange
|
|
string targetRoot = Directory.CreateTempSubdirectory().FullName;
|
|
string targetPath = Path.Combine(targetRoot, "seed-rule-tracker");
|
|
Directory.CreateDirectory(targetPath);
|
|
File.WriteAllText(Path.Combine(targetPath, "file.txt"), "content");
|
|
|
|
DownloadClientConfig config = new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Name = "Test rTorrent Client",
|
|
TypeName = Domain.Enums.DownloadClientTypeName.rTorrent,
|
|
Type = Domain.Enums.DownloadClientType.Torrent,
|
|
Enabled = true,
|
|
Host = new Uri("http://localhost/RPC2"),
|
|
Username = "admin",
|
|
Password = "admin",
|
|
UrlBase = "",
|
|
DownloadDirectorySource = "/downloads",
|
|
DownloadDirectoryTarget = targetRoot,
|
|
};
|
|
var sut = _fixture.CreateSut(config);
|
|
|
|
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
|
mockTorrent.Hash.Returns("hash");
|
|
mockTorrent.SavePath.Returns("/downloads/seed-rule-tracker");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
await sut.DeleteDownload(mockTorrent, deleteSourceFiles: true);
|
|
|
|
// Assert
|
|
Directory.Exists(targetPath).ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(targetRoot))
|
|
{
|
|
Directory.Delete(targetRoot, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public class StopDownload_Tests : RTorrentServiceDCTests
|
|
{
|
|
public StopDownload_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NormalizesHashToUppercase()
|
|
{
|
|
// Arrange
|
|
RTorrentService sut = _fixture.CreateSut();
|
|
ITorrentItemWrapper mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
|
mockTorrent.Hash.Returns("lowercase");
|
|
mockTorrent.SavePath.Returns("/test/path");
|
|
|
|
_fixture.ClientWrapper
|
|
.StopTorrentAsync("LOWERCASE")
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Act
|
|
await sut.StopDownload(mockTorrent);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.Received(1)
|
|
.StopTorrentAsync("LOWERCASE");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DoesNotDeleteTheTorrent()
|
|
{
|
|
// Arrange
|
|
RTorrentService sut = _fixture.CreateSut();
|
|
ITorrentItemWrapper mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
|
mockTorrent.Hash.Returns("LOWERCASE");
|
|
mockTorrent.SavePath.Returns("/test/path");
|
|
|
|
_fixture.ClientWrapper
|
|
.StopTorrentAsync(Arg.Any<string>())
|
|
.Returns(Task.CompletedTask);
|
|
|
|
// Act
|
|
await sut.StopDownload(mockTorrent);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.DeleteTorrentAsync(Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LeavesTheSourceFilesOnDisk()
|
|
{
|
|
// Arrange
|
|
RTorrentService sut = _fixture.CreateSut();
|
|
string savePath = Path.Combine(Path.GetTempPath(), "cleanuparr-tests", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(savePath);
|
|
await File.WriteAllTextAsync(Path.Combine(savePath, "file.mkv"), "content");
|
|
|
|
ITorrentItemWrapper mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
|
mockTorrent.Hash.Returns("HASH1");
|
|
mockTorrent.SavePath.Returns(savePath);
|
|
|
|
_fixture.ClientWrapper
|
|
.StopTorrentAsync(Arg.Any<string>())
|
|
.Returns(Task.CompletedTask);
|
|
|
|
try
|
|
{
|
|
// Act
|
|
await sut.StopDownload(mockTorrent);
|
|
|
|
// Assert
|
|
Directory.Exists(savePath).ShouldBeTrue();
|
|
File.Exists(Path.Combine(savePath, "file.mkv")).ShouldBeTrue();
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(savePath, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class IsStopped_Tests : RTorrentServiceDCTests
|
|
{
|
|
public IsStopped_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsTrueWhenStateIsStopped()
|
|
{
|
|
// Arrange
|
|
RTorrentItemWrapper wrapper = new RTorrentItemWrapper(
|
|
new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", State = 0 }, null, TimeProvider.System);
|
|
|
|
// Act & Assert
|
|
wrapper.IsStopped.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void ReturnsFalseWhenStateIsStarted()
|
|
{
|
|
// Arrange
|
|
RTorrentItemWrapper wrapper = new RTorrentItemWrapper(
|
|
new RTorrentTorrent { Hash = "HASH1", Name = "Torrent 1", State = 1 }, null, TimeProvider.System);
|
|
|
|
// Act & Assert
|
|
wrapper.IsStopped.ShouldBeFalse();
|
|
}
|
|
}
|
|
|
|
public class CreateCategoryAsync_Tests : RTorrentServiceDCTests
|
|
{
|
|
public CreateCategoryAsync_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IsNoOp_BecauseRTorrentDoesNotSupportCategories()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
// Act
|
|
await sut.CreateCategoryAsync("test-category");
|
|
|
|
// Assert - no client calls should be made
|
|
// (NSubstitute has no direct equivalent of VerifyNoOtherCalls, but since no setups
|
|
// were made, any unexpected call would return default values - the test passes by
|
|
// verifying no specific interactions occurred)
|
|
}
|
|
}
|
|
|
|
public class ChangeCategoryForNoHardLinksAsync_Tests : RTorrentServiceDCTests
|
|
{
|
|
public ChangeCategoryForNoHardLinksAsync_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NullDownloads_DoesNothing()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(null, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EmptyDownloads_DoesNothing()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(new List<ITorrentItemWrapper>(), unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MissingHash_SkipsTorrent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MissingName_SkipsTorrent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MissingCategory_SkipsTorrent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetFilesThrows_SkipsTorrent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Throws(new Exception("XML-RPC error"));
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SkippedFiles_IgnoredInCheck()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 0 }, // Skipped
|
|
new RTorrentFile { Index = 1, Path = "file2.mkv", Priority = 1 } // Active
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(0);
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert - only called for file2.mkv (the active file)
|
|
_fixture.HardLinkFileService.Received(1)
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoHardlinks_ChangesLabel()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(0);
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert - rTorrent uses SetLabelAsync (not SetTorrentCategoryAsync)
|
|
await _fixture.ClientWrapper.Received(1)
|
|
.SetLabelAsync("HASH1", "unlinked");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasHardlinks_SkipsTorrent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(2); // Has hardlinks
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FileNotFound_SkipsTorrent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(-1); // Error / file not found
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
await _fixture.ClientWrapper.DidNotReceive()
|
|
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PublishesCategoryChangedEvent()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(0);
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
_fixture.EventPublisher.Received(1)
|
|
.PublishCategoryChanged("movies", "unlinked", false);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UsesDirectoryOverBasePathForFilePath()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
// Single-file torrent: BasePath is the full file path, Directory is the containing dir
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent
|
|
{
|
|
Hash = "HASH1", Name = "movie.mkv", Label = "movies",
|
|
BasePath = "/downloads/movie.mkv",
|
|
Directory = "/downloads"
|
|
}, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(0);
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert - path should use Directory (/downloads), not BasePath (/downloads/movie.mkv)
|
|
var expectedPath = string.Join(Path.DirectorySeparatorChar,
|
|
Path.Combine("/downloads", "movie.mkv").Split('\\', '/'));
|
|
_fixture.HardLinkFileService.Received(1)
|
|
.GetHardLinkCount(expectedPath, false);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FallsBackToBasePathWhenDirectoryNull()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var downloads = new List<ITorrentItemWrapper>
|
|
{
|
|
new RTorrentItemWrapper(new RTorrentTorrent
|
|
{
|
|
Hash = "HASH1", Name = "Test", Label = "movies",
|
|
BasePath = "/downloads",
|
|
Directory = null
|
|
}, null, TimeProvider.System)
|
|
};
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(0);
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert - path should fall back to BasePath
|
|
var expectedPath = string.Join(Path.DirectorySeparatorChar,
|
|
Path.Combine("/downloads", "file1.mkv").Split('\\', '/'));
|
|
_fixture.HardLinkFileService.Received(1)
|
|
.GetHardLinkCount(expectedPath, false);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdatesCategoryOnWrapper()
|
|
{
|
|
// Arrange
|
|
var sut = _fixture.CreateSut();
|
|
|
|
var unlinkedConfig = new UnlinkedConfig
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TargetCategory = "unlinked"
|
|
};
|
|
|
|
var wrapper = new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System);
|
|
var downloads = new List<ITorrentItemWrapper> { wrapper };
|
|
|
|
_fixture.ClientWrapper
|
|
.GetTorrentFilesAsync("HASH1")
|
|
.Returns(new List<RTorrentFile>
|
|
{
|
|
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
|
});
|
|
|
|
_fixture.HardLinkFileService
|
|
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
|
.Returns(0);
|
|
|
|
// Act
|
|
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
|
|
|
// Assert
|
|
wrapper.Category.ShouldBe("unlinked");
|
|
}
|
|
}
|
|
|
|
public class GetClaimedPaths_Tests : RTorrentServiceDCTests
|
|
{
|
|
public GetClaimedPaths_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
|
{
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClaimsBasePathAndDirectory()
|
|
{
|
|
// rTorrent resolves base_path (content root) and directory (its parent) itself;
|
|
// no file lookup, and the display name is never involved.
|
|
var sut = _fixture.CreateSut();
|
|
var wrapper = new RTorrentItemWrapper(new RTorrentTorrent
|
|
{
|
|
Hash = "HASH1",
|
|
Name = "Renamed Display",
|
|
BasePath = "/downloads/show",
|
|
Directory = "/downloads"
|
|
}, null, TimeProvider.System);
|
|
|
|
IReadOnlyList<string> claimed = await sut.GetClaimedPathsAsync(new Domain.Entities.ITorrentItemWrapper[] { wrapper });
|
|
|
|
claimed.ShouldContain("/downloads/show");
|
|
claimed.ShouldContain("/downloads");
|
|
claimed.ShouldNotContain("/downloads/Renamed Display");
|
|
}
|
|
}
|
|
}
|