using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission; using Shouldly; using Transmission.API.RPC.Entity; using Xunit; namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient; public class TransmissionItemWrapperTests { [Fact] public void Constructor_WithNullTorrentInfo_ThrowsArgumentNullException() { // Act & Assert Should.Throw(() => new TransmissionItemWrapper(null!)); } [Fact] public void Hash_ReturnsCorrectValue() { // Arrange var expectedHash = "test-hash-123"; var torrentInfo = new TorrentInfo { HashString = expectedHash }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Hash; // Assert result.ShouldBe(expectedHash); } [Fact] public void Hash_WithNullValue_ReturnsEmptyString() { // Arrange var torrentInfo = new TorrentInfo { HashString = null }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Hash; // Assert result.ShouldBe(string.Empty); } [Fact] public void Name_ReturnsCorrectValue() { // Arrange var expectedName = "Test Torrent"; var torrentInfo = new TorrentInfo { Name = expectedName }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Name; // Assert result.ShouldBe(expectedName); } [Fact] public void Name_WithNullValue_ReturnsEmptyString() { // Arrange var torrentInfo = new TorrentInfo { Name = null }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Name; // Assert result.ShouldBe(string.Empty); } [Theory] [InlineData(true, true)] [InlineData(false, false)] [InlineData(null, false)] public void IsPrivate_ReturnsCorrectValue(bool? isPrivate, bool expected) { // Arrange var torrentInfo = new TorrentInfo { IsPrivate = isPrivate }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.IsPrivate; // Assert result.ShouldBe(expected); } [Theory] [InlineData(1024L * 1024 * 1024, 1024L * 1024 * 1024)] // 1GB [InlineData(0L, 0L)] [InlineData(null, 0L)] public void Size_ReturnsCorrectValue(long? totalSize, long expected) { // Arrange var torrentInfo = new TorrentInfo { TotalSize = totalSize }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Size; // Assert result.ShouldBe(expected); } [Theory] [InlineData(0L, 1024L, 0.0)] [InlineData(512L, 1024L, 50.0)] [InlineData(768L, 1024L, 75.0)] [InlineData(1024L, 1024L, 100.0)] [InlineData(0L, 0L, 0.0)] // Edge case: zero size [InlineData(null, 1024L, 0.0)] // Edge case: null downloaded [InlineData(512L, null, 0.0)] // Edge case: null total size public void CompletionPercentage_ReturnsCorrectValue(long? downloadedEver, long? totalSize, double expectedPercentage) { // Arrange var torrentInfo = new TorrentInfo { DownloadedEver = downloadedEver, TotalSize = totalSize }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.CompletionPercentage; // Assert result.ShouldBe(expectedPercentage); } [Theory] [InlineData(1024L * 1024 * 100, 1024L * 1024 * 100)] // 100MB [InlineData(0L, 0L)] [InlineData(null, 0L)] public void DownloadedBytes_ReturnsCorrectValue(long? downloadedEver, long expected) { // Arrange var torrentInfo = new TorrentInfo { DownloadedEver = downloadedEver }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.DownloadedBytes; // Assert result.ShouldBe(expected); } [Theory] [InlineData(2.0, 2.0)] [InlineData(0.5, 0.5)] [InlineData(1.0, 1.0)] [InlineData(0.0, 0.0)] [InlineData(null, 0.0)] public void Ratio_ReturnsCorrectValue(double? uploadRatio, double expected) { // Arrange var torrentInfo = new TorrentInfo { uploadRatio = uploadRatio }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Ratio; // Assert result.ShouldBe(expected); } [Theory] [InlineData(3600L, 3600L)] // 1 hour [InlineData(0L, 0L)] [InlineData(-1L, -1L)] // Unknown/infinite [InlineData(null, 0L)] public void Eta_ReturnsCorrectValue(long? eta, long expected) { // Arrange var torrentInfo = new TorrentInfo { Eta = eta }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Eta; // Assert result.ShouldBe(expected); } [Theory] [InlineData(86400L, 86400L)] // 1 day [InlineData(0L, 0L)] [InlineData(null, 0L)] public void SeedingTimeSeconds_ReturnsCorrectValue(long? secondsSeeding, long expected) { // Arrange var torrentInfo = new TorrentInfo { SecondsSeeding = secondsSeeding }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.SeedingTimeSeconds; // Assert result.ShouldBe(expected); } [Fact] public void SeederCount_WithTrackerStats_ReturnsMaxSeederCount() { // Arrange var torrentInfo = new TorrentInfo { TrackerStats = new TransmissionTorrentTrackerStats[] { new() { SeederCount = 3L }, new() { SeederCount = 7L }, new() { SeederCount = 5L }, } }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.SeederCount; // Assert result.ShouldBe(7); } [Fact] public void SeederCount_WithNullTrackerStats_ReturnsNull() { // Arrange var torrentInfo = new TorrentInfo { TrackerStats = null }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.SeederCount; // Assert result.ShouldBeNull(); } [Fact] public void SeederCount_WithEmptyTrackerStats_ReturnsNull() { // Arrange var torrentInfo = new TorrentInfo { TrackerStats = Array.Empty() }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.SeederCount; // Assert result.ShouldBeNull(); } [Fact] public void SeederCount_WhenAllTrackerStatsAreUnscraped_ReturnsNull() { // Transmission RPC reports seederCount = -1 for trackers that have not been scraped yet // (libtransmission/announcer.cc: view.seederCount = tracker.seeder_count().value_or(-1)). var torrentInfo = new TorrentInfo { TrackerStats = new TransmissionTorrentTrackerStats[] { new() { SeederCount = -1L }, new() { SeederCount = -1L }, } }; var wrapper = new TransmissionItemWrapper(torrentInfo); var result = wrapper.SeederCount; result.ShouldBeNull(); } [Fact] public void SeederCount_WithMixedScrapedAndUnscrapedTrackerStats_ReturnsMaxScrapedValue() { var torrentInfo = new TorrentInfo { TrackerStats = new TransmissionTorrentTrackerStats[] { new() { SeederCount = -1L }, new() { SeederCount = 4L }, } }; var wrapper = new TransmissionItemWrapper(torrentInfo); var result = wrapper.SeederCount; result.ShouldBe(4); } // TrackerDomains property tests [Fact] public void TrackerDomains_WithMultipleTrackers_ReturnsExtractedDomains() { // Arrange var torrentInfo = new TorrentInfo { Trackers = new TransmissionTorrentTrackers[] { new() { Announce = "http://tracker.example.com/announce" }, new() { Announce = "udp://open.stealth.si:80/announce" } } }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.TrackerDomains; // Assert result.Count.ShouldBe(2); result.ShouldContain("tracker.example.com"); result.ShouldContain("open.stealth.si"); } [Fact] public void TrackerDomains_WithNullTrackers_ReturnsEmptyList() { // Arrange var torrentInfo = new TorrentInfo { Trackers = null }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.TrackerDomains; // Assert result.ShouldBeEmpty(); } [Fact] public void TrackerDomains_WithEmptyTrackers_ReturnsEmptyList() { // Arrange var torrentInfo = new TorrentInfo { Trackers = Array.Empty() }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.TrackerDomains; // Assert result.ShouldBeEmpty(); } [Fact] public void TrackerDomains_WithNullAnnounceUrls_FiltersThemOut() { // Arrange var torrentInfo = new TorrentInfo { Trackers = new TransmissionTorrentTrackers[] { new() { Announce = "http://tracker.example.com/announce" }, new() { Announce = null }, new() { Announce = "" } } }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.TrackerDomains; // Assert result.Count.ShouldBe(1); result.ShouldContain("tracker.example.com"); } // Tags property tests [Fact] public void Tags_ReturnsLabels() { // Arrange var torrentInfo = new TorrentInfo { Labels = new[] { "tag1", "tag2", "tag3" } }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Tags; // Assert result.Count.ShouldBe(3); result.ShouldContain("tag1"); result.ShouldContain("tag2"); result.ShouldContain("tag3"); } [Fact] public void Tags_WithNullLabels_ReturnsEmptyList() { // Arrange var torrentInfo = new TorrentInfo { Labels = null }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Tags; // Assert result.ShouldBeEmpty(); } [Fact] public void Tags_WithEmptyLabels_ReturnsEmptyList() { // Arrange var torrentInfo = new TorrentInfo { Labels = Array.Empty() }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.Tags; // Assert result.ShouldBeEmpty(); } [Fact] public void IsIgnored_WithEmptyList_ReturnsFalse() { // Arrange var torrentInfo = new TorrentInfo { HashString = "abc123", Name = "Test Torrent" }; var wrapper = new TransmissionItemWrapper(torrentInfo); // Act var result = wrapper.IsIgnored(Array.Empty()); // Assert result.ShouldBeFalse(); } [Fact] public void IsIgnored_MatchingHash_ReturnsTrue() { // Arrange var torrentInfo = new TorrentInfo { HashString = "abc123", Name = "Test Torrent" }; var wrapper = new TransmissionItemWrapper(torrentInfo); var ignoredDownloads = new[] { "abc123" }; // Act var result = wrapper.IsIgnored(ignoredDownloads); // Assert result.ShouldBeTrue(); } [Fact] public void IsIgnored_MatchingCategory_ReturnsTrue() { // Arrange var torrentInfo = new TorrentInfo { HashString = "abc123", Name = "Test Torrent", DownloadDir = "/downloads/test-category" }; var wrapper = new TransmissionItemWrapper(torrentInfo); var ignoredDownloads = new[] { "test-category" }; // Act var result = wrapper.IsIgnored(ignoredDownloads); // Assert result.ShouldBeTrue(); } [Fact] public void IsIgnored_MatchingTracker_ReturnsTrue() { // Arrange var torrentInfo = new TorrentInfo { HashString = "abc123", Name = "Test Torrent", Trackers = new TransmissionTorrentTrackers[] { new() { Announce = "http://tracker.example.com/announce" } } }; var wrapper = new TransmissionItemWrapper(torrentInfo); var ignoredDownloads = new[] { "tracker.example.com" }; // Act var result = wrapper.IsIgnored(ignoredDownloads); // Assert result.ShouldBeTrue(); } [Fact] public void IsIgnored_NotMatching_ReturnsFalse() { // Arrange var torrentInfo = new TorrentInfo { HashString = "abc123", Name = "Test Torrent", Labels = new[] { "some-category" }, Trackers = new TransmissionTorrentTrackers[] { new() { Announce = "http://tracker.example.com/announce" } } }; var wrapper = new TransmissionItemWrapper(torrentInfo); var ignoredDownloads = new[] { "notmatching" }; // Act var result = wrapper.IsIgnored(ignoredDownloads); // Assert result.ShouldBeFalse(); } }