rdt-client/server/RdtClient.Service.Test/Services/EnricherTest.cs
MentalBlank 98042f8040 Improved enrichment validation, logging, and test coverage.
Strengthened URL Validation.
Improved HTTP Client Configuration.
Logged total rejected tracker count at the information level.
Logged individual rejected tracker details at the debug level.
Debug logs for bytes and enriched bytes are now conditional.
Converted some tests from Fact to Theory.
Used Assert.Collection in EnricherTest.cs.
Standardized URL Encoding in Enricher.cs.
2025-05-25 21:50:26 +10:00

459 lines
No EOL
16 KiB
C#

using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Service.Services;
using MonoTorrent.BEncoding;
namespace RdtClient.Service.Test.Services;
public class EnricherTest : IDisposable
{
private readonly MockRepository _mockRepository;
private readonly Mock<ILogger<Enricher>> _loggerMock;
private readonly Mock<ITrackerListGrabber> _trackerListGrabberMock;
public EnricherTest()
{
_mockRepository = new MockRepository(MockBehavior.Strict);
_loggerMock = _mockRepository.Create<ILogger<Enricher>>(MockBehavior.Loose);
_trackerListGrabberMock = _mockRepository.Create<ITrackerListGrabber>();
}
public void Dispose()
{
_mockRepository.VerifyAll();
}
private const String TestMagnetLink =
"magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567&dn=TestFile&tr=http%3A%2F%2Ftracker1.com%2Fannounce&tr=http%3A%2F%2Ftracker2.com%2Fannounce";
[Fact]
public async Task EnrichMagnetLink_AddsNoTrackers_WhenNoTrackersFromTrackerGrabber()
{
// Arrange
SetupTrackerListGrabber([]);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enriched = await enricher.EnrichMagnetLink(TestMagnetLink);
// Assert
Assert.Equal(TestMagnetLink, enriched);
}
[Theory]
[InlineData("magnet:?xt=urn:btih:HASH",
new[]
{
"http://new-tracker.com/announce"
},
"magnet:?xt=urn:btih:HASH&tr=http%3A%2F%2Fnew-tracker.com%2Fannounce")]
[InlineData("magnet:?xt=urn:btih:HASH",
new[]
{
"http://tracker1.com/announce", "http://tracker2.com/announce"
},
"magnet:?xt=urn:btih:HASH&tr=http%3A%2F%2Ftracker1.com%2Fannounce&tr=http%3A%2F%2Ftracker2.com%2Fannounce")]
public async Task EnrichMagnetLink_AddsTrackers_WhenTrackersFromTrackerGrabber_Theory(String magnetLink, String[] newTrackers, String expectedResult)
{
// Arrange
SetupTrackerListGrabber(newTrackers);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enriched = await enricher.EnrichMagnetLink(magnetLink);
// Assert
Assert.Equal(expectedResult, enriched);
}
[Fact]
public async Task EnrichMagnetLink_DoesNotAddDuplicateTrackers_WhenTrackersFromTrackerGrabberAlreadyPresent()
{
// Arrange
SetupTrackerListGrabber(["http://new-tracker.com/announce", "http://tracker1.com/announce"]);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enriched = await enricher.EnrichMagnetLink(TestMagnetLink);
// Assert
Assert.Equal(TestMagnetLink + $"&tr={Uri.EscapeDataString("http://new-tracker.com/announce")}", enriched);
}
[Fact]
public async Task EnrichMagnetLink_Throws_WhenTrackerGrabberThrows()
{
// Arrange
_trackerListGrabberMock
.Setup(t => t.GetTrackers())
.ThrowsAsync(new InvalidOperationException("Unable to fetch tracker list for enrichment."));
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => enricher.EnrichMagnetLink(TestMagnetLink));
}
[Fact]
public async Task EnrichTorrentBytes_AddsTrackers_WhenTrackersFromTrackerGrabber()
{
// Arrange
var originalTracker = "http://tracker1.com/announce";
var newTracker = "http://new-tracker.com/announce";
var torrentDict = new BEncodedDictionary
{
["announce"] = new BEncodedString(originalTracker),
["announce-list"] = new BEncodedList
{
new BEncodedList
{
new BEncodedString(originalTracker)
}
}
};
var originalTorrentBytes = torrentDict.Encode();
SetupTrackerListGrabber([newTracker]);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enrichedBytes = await enricher.EnrichTorrentBytes(originalTorrentBytes);
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
// Assert
Assert.True(enrichedDict.ContainsKey("announce"));
Assert.True(enrichedDict.ContainsKey("announce-list"));
var announceList = (BEncodedList)enrichedDict["announce-list"];
var flattened = announceList.Cast<BEncodedList>().SelectMany(l => l.Cast<BEncodedString>().Select(s => s.Text)).ToList();
Assert.Contains(originalTracker, flattened);
Assert.Contains(newTracker, flattened);
var announce = ((BEncodedString)enrichedDict["announce"]).Text;
Assert.Equal(flattened.First(), announce);
}
private void SetupTrackerListGrabber(String[] trackerList)
{
_trackerListGrabberMock
.Setup(t => t.GetTrackers())
.ReturnsAsync(trackerList)
.Verifiable();
}
[Fact]
public async Task EnrichTorrentBytes_DoesNotAddDuplicateTrackers()
{
// Arrange
var originalTracker = "http://tracker1.com/announce";
var duplicateTracker = "http://tracker1.com/announce";
var torrentDict = new BEncodedDictionary
{
["announce"] = new BEncodedString(originalTracker),
["announce-list"] = new BEncodedList
{
new BEncodedList
{
new BEncodedString(originalTracker)
}
}
};
var originalTorrentBytes = torrentDict.Encode();
SetupTrackerListGrabber([duplicateTracker]);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enrichedBytes = await enricher.EnrichTorrentBytes(originalTorrentBytes);
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
// Assert
var announceList = (BEncodedList)enrichedDict["announce-list"];
var flattened = announceList.Cast<BEncodedList>().SelectMany(l => l.Cast<BEncodedString>().Select(s => s.Text)).ToList();
Assert.Single(flattened);
Assert.Contains(originalTracker, flattened);
}
[Fact]
public async Task EnrichTorrentBytes_AddsTrackers_WhenNoAnnounceListPresent()
{
// Arrange
var originalTracker = "http://tracker1.com/announce";
var newTracker = "http://new-tracker.com/announce";
var torrentDict = new BEncodedDictionary
{
["announce"] = new BEncodedString(originalTracker)
// No "announce-list"
};
var originalTorrentBytes = torrentDict.Encode();
SetupTrackerListGrabber([newTracker]);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enrichedBytes = await enricher.EnrichTorrentBytes(originalTorrentBytes);
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
// Assert
Assert.True(enrichedDict.ContainsKey("announce-list"));
var announceList = (BEncodedList)enrichedDict["announce-list"];
var flattened = announceList.Cast<BEncodedList>().SelectMany(l => l.Cast<BEncodedString>().Select(s => s.Text)).ToList();
Assert.Contains(originalTracker, flattened);
Assert.Contains(newTracker, flattened);
}
[Fact]
public async Task EnrichTorrentBytes_Throws_WhenTrackerGrabberThrows()
{
// Arrange
_trackerListGrabberMock
.Setup(t => t.GetTrackers())
.ThrowsAsync(new InvalidOperationException("Unable to fetch tracker list for enrichment."));
var torrentDict = new BEncodedDictionary
{
["announce"] = new BEncodedString("http://tracker1.com/announce"),
["announce-list"] = new BEncodedList
{
new BEncodedList
{
new BEncodedString("http://tracker1.com/announce")
}
}
};
var originalTorrentBytes = torrentDict.Encode();
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => enricher.EnrichTorrentBytes(originalTorrentBytes));
}
[Fact]
public async Task EnrichTorrentBytes_ThrowsArgumentException_OnNullOrEmptyInput()
{
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
await Assert.ThrowsAsync<ArgumentException>(() => enricher.EnrichTorrentBytes(null!));
await Assert.ThrowsAsync<ArgumentException>(() => enricher.EnrichTorrentBytes([]));
}
[Fact]
public async Task EnrichTorrentBytes_ThrowsInvalidOperationException_OnNonTorrentBytes()
{
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
var notTorrent = new Byte[]
{
1, 2, 3, 4, 5
};
await Assert.ThrowsAsync<InvalidOperationException>(() => enricher.EnrichTorrentBytes(notTorrent));
}
[Fact]
public async Task EnrichMagnetLink_WithExistingTrackersAndOtherParameters()
{
// Arrange
var magnetLink = "magnet:?xt=urn:btih:HASH&dn=MyFile&tr=udp%3A%2F%2Fexisting";
var newTrackers = new[]
{
"udp://new1", "udp://existing"
};
SetupTrackerListGrabber(newTrackers);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var result = await enricher.EnrichMagnetLink(magnetLink);
// Assert
var queryParams = System.Web.HttpUtility.ParseQueryString(new Uri(result).Query);
var trValues = queryParams.GetValues("tr")?.ToList() ?? new List<String>();
Assert.Equal("urn:btih:HASH", queryParams["xt"]);
Assert.Equal("MyFile", queryParams["dn"]);
Assert.Contains("udp://existing", trValues);
Assert.Contains("udp://new1", trValues);
Assert.Equal(2, trValues.Count);
}
[Theory]
[InlineData("magnet:?xt=urn:btih:HASH",
new[]
{
"udp://tracker1"
},
"magnet:?xt=urn:btih:HASH&tr=udp%3A%2F%2Ftracker1")]
[InlineData("magnet:?xt=urn:btih:HASH",
new[]
{
"udp://tracker1", "udp://tracker2"
},
"magnet:?xt=urn:btih:HASH&tr=udp%3A%2F%2Ftracker1&tr=udp%3A%2F%2Ftracker2")]
[InlineData("magnet:?xt=urn:btih:HASH",
new String[]
{
},
"magnet:?xt=urn:btih:HASH")]
public async Task EnrichMagnetLink_BasicMagnetWithHashOnly_Theory(String magnetLink, String[] newTrackers, String expectedResult)
{
// Arrange
SetupTrackerListGrabber(newTrackers);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var result = await enricher.EnrichMagnetLink(magnetLink);
// Assert
Assert.Equal(expectedResult, result);
}
[Theory]
[InlineData("magnet:?",
new[]
{
"udp://tracker1"
},
"magnet:?tr=udp%3A%2F%2Ftracker1")]
[InlineData("magnet:?",
new[]
{
"udp://tracker1", "udp://tracker2"
},
"magnet:?tr=udp%3A%2F%2Ftracker1&tr=udp%3A%2F%2Ftracker2")]
[InlineData("magnet:?",
new String[]
{
},
"magnet:?")]
public async Task EnrichMagnetLink_MagnetEndsWithQuestionMarkOnly_Theory(String magnetLink, String[] newTrackers, String expectedResult)
{
// Arrange
SetupTrackerListGrabber(newTrackers);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var result = await enricher.EnrichMagnetLink(magnetLink);
// Assert
Assert.Equal(expectedResult, result);
}
[Fact]
public async Task EnrichMagnetLink_GetTrackersReturnsEmptyArray_Unchanged()
{
// Arrange
var magnetLink = "magnet:?xt=urn:btih:HASH&tr=udp%3A%2F%2Fexisting";
SetupTrackerListGrabber([]);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var result = await enricher.EnrichMagnetLink(magnetLink);
// Assert
Assert.Equal(magnetLink, result);
}
[Fact]
public async Task EnrichTorrentBytes_NoExistingAnnounceOrAnnounceList()
{
// Arrange
var torrentDict = new BEncodedDictionary
{
["info"] = new BEncodedDictionary()
};
var originalBytes = torrentDict.Encode();
var newTrackers = new[]
{
"udp://tracker1", "udp://tracker2"
};
SetupTrackerListGrabber(newTrackers);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enrichedBytes = await enricher.EnrichTorrentBytes(originalBytes);
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
// Assert
Assert.Equal(newTrackers[0], ((BEncodedString)enrichedDict["announce"]).Text);
var announceList = (BEncodedList)enrichedDict["announce-list"];
Assert.Equal(2, announceList.Count);
Assert.Collection(announceList,
tier => Assert.Equal(newTrackers[0], ((BEncodedString)((BEncodedList)tier)[0]).Text),
tier => Assert.Equal(newTrackers[1], ((BEncodedString)((BEncodedList)tier)[0]).Text));
}
[Fact]
public async Task EnrichTorrentBytes_WithExistingAnnounceAndAnnounceList()
{
// Arrange
var existingTrackers = new List<String>
{
"http://tier1.com",
"http://existing.com/announce"
};
var torrentDict = new BEncodedDictionary
{
["announce"] = new BEncodedString(existingTrackers[1]),
["announce-list"] = new BEncodedList
{
new BEncodedList
{
new BEncodedString(existingTrackers[0])
},
new BEncodedList
{
new BEncodedString(existingTrackers[1])
}
},
["info"] = new BEncodedDictionary()
};
var originalBytes = torrentDict.Encode();
var newTrackersFromGrabber = new[]
{
"udp://new1", "http://existing.com/announce"
};
SetupTrackerListGrabber(newTrackersFromGrabber);
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
// Act
var enrichedBytes = await enricher.EnrichTorrentBytes(originalBytes);
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
// Assert
var finalAnnounceList = ((BEncodedList)enrichedDict["announce-list"])
.Cast<BEncodedList>()
.SelectMany(tier => tier.Cast<BEncodedString>().Select(t => t.Text))
.ToList();
Assert.Contains("http://tier1.com", finalAnnounceList);
Assert.Contains("http://existing.com/announce", finalAnnounceList);
Assert.Contains("udp://new1", finalAnnounceList);
Assert.Equal(3, finalAnnounceList.Count);
Assert.Contains(((BEncodedString)enrichedDict["announce"]).Text, finalAnnounceList);
}
}