rdt-client/server/RdtClient.Service.Test/Services/TorrentsTest.cs
2026-04-20 22:11:42 -04:00

429 lines
17 KiB
C#

using System.Diagnostics;
using System.IO.Abstractions.TestingHelpers;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services;
using RdtClient.Service.Test.Helpers;
using RdtClient.Service.Wrappers;
using DownloadClient = RdtClient.Data.Enums.DownloadClient;
using TorrentsService = RdtClient.Service.Services.Torrents;
namespace RdtClient.Service.Test.Services;
internal class Mocks
{
public readonly Mock<IDownloads> DownloadsMock;
public readonly Mock<IEnricher> EnricherMock;
public readonly Mock<IProcessFactory> ProcessFactoryMock;
public readonly Mock<IProcess> ProcessMock;
public readonly Mock<ITorrentData> TorrentDataMock;
public readonly Mock<ILogger<TorrentsService>> TorrentsLoggerMock;
public Mocks()
{
TorrentDataMock = new();
DownloadsMock = new();
EnricherMock = new();
TorrentsLoggerMock = new();
ProcessMock = new();
ProcessStartInfo startInfo = new();
ProcessMock.SetupProperty(p => p.StartInfo, startInfo);
ProcessFactoryMock = new();
ProcessFactoryMock.Setup(p => p.NewProcess()).Returns(ProcessMock.Object);
}
}
public class TorrentsTest
{
public static TheoryData<Torrent, List<Download>> TorrentAndDownload()
{
var torrent = new Torrent
{
RdName = "TestTorrent",
Hash = "123ABC",
Category = "Movies",
RdSize = 100,
TorrentId = Guid.Empty
};
List<Download> downloads =
[
new()
{
FileName = "file.txt",
TorrentId = torrent.TorrentId
}
];
return new()
{
{
torrent, downloads
}
};
}
[Theory]
[MemberData(nameof(TorrentAndDownload))]
public async Task RunTorrentComplete_WhenCommandSet_ShouldRunCommand(Torrent torrent, List<Download> downloads)
{
// Arrange
var settings = new DbSettings
{
General = new()
{
RunOnTorrentCompleteFileName = "/bin/echo",
RunOnTorrentCompleteArguments = "%N %L %F %R %D %C %Z %I"
}
};
var mocks = new Mocks();
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
String downloadPath;
String torrentPath;
String filePath;
var category = torrent.Category!;
var torrentName = torrent.RdName!;
var fileName = downloads[0].FileName!;
downloadPath = Path.Combine(settings.DownloadClient.DownloadPath, category);
torrentPath = Path.Combine(downloadPath, torrentName);
filePath = Path.Combine(torrentPath, fileName);
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
{
{
filePath, new("Test file")
}
});
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
fileSystemMock,
mocks.EnricherMock.Object,
null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine
null!,
null!,
null!,
null!);
mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny<Int32>())).Returns(true);
// Act
await torrents.RunTorrentComplete(torrent.TorrentId, settings);
// Assert
Assert.Equal("/bin/echo", mocks.ProcessMock.Object.StartInfo.FileName);
var expectedArgumentsSb = new StringBuilder();
expectedArgumentsSb.Append($"\"{torrent.RdName}\"");
expectedArgumentsSb.Append($" \"{torrent.Category}\"");
expectedArgumentsSb.Append($" \"{filePath}\"");
expectedArgumentsSb.Append($" \"{downloadPath}\"");
expectedArgumentsSb.Append($" \"{torrentPath}\"");
expectedArgumentsSb.Append($" {downloads.Count.ToString()}");
expectedArgumentsSb.Append($" {torrent.RdSize.ToString()}");
expectedArgumentsSb.Append($" {torrent.Hash}");
var expectedArguments = expectedArgumentsSb.ToString();
Assert.Equal(expectedArguments, mocks.ProcessMock.Object.StartInfo.Arguments);
mocks.ProcessMock.Verify(p => p.Start(), Times.Once);
}
[Theory]
[MemberData(nameof(TorrentAndDownload))]
public async Task RunTorrentComplete_WhenCommandNotSet_ShouldNotRunCommand(Torrent torrent, List<Download> downloads)
{
// Arrange
var settings = new DbSettings
{
General = new()
{
RunOnTorrentCompleteFileName = null
}
};
var mocks = new Mocks();
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
var downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
var torrentPath = $"{downloadPath}/{torrent.RdName}";
var filePath = $"{torrentPath}/{downloads[0].FileName}";
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
{
{
filePath, new("Test file")
}
});
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
fileSystemMock,
mocks.EnricherMock.Object,
null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine
null!,
null!,
null!,
null!);
//Act
await torrents.RunTorrentComplete(torrent.TorrentId, settings);
//Assert
mocks.ProcessFactoryMock.VerifyNoOtherCalls();
}
[Theory]
[MemberData(nameof(TorrentAndDownload))]
public async Task RunTorrentComplete_WhenStdOut_Logs(Torrent torrent, List<Download> downloads)
{
// Arrange
var settings = new DbSettings
{
General = new()
{
RunOnTorrentCompleteFileName = "/bin/echo"
}
};
var mocks = new Mocks();
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
var downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
var torrentPath = $"{downloadPath}/{torrent.RdName}";
var filePath = $"{torrentPath}/{downloads[0].FileName}";
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
{
{
filePath, new("Test file")
}
});
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
fileSystemMock,
mocks.EnricherMock.Object,
null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine
null!,
null!,
null!,
null!);
mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny<Int32>()))
.Callback(() =>
{
mocks.ProcessMock.Raise(m => m.OutputDataReceived += null, this, "output-line 1");
mocks.ProcessMock.Raise(m => m.OutputDataReceived += null, this, "output-line 2");
mocks.ProcessMock.Raise(m => m.OutputDataReceived += null, this, "output-line 3");
})
.Returns(true);
// Act
await torrents.RunTorrentComplete(torrent.TorrentId, settings);
// Assert
mocks.ProcessMock.Verify(p => p.BeginOutputReadLine(), Times.Once);
var messages = mocks.TorrentsLoggerMock.Invocations.Where(i => i.Method.Name == "Log").Select(i => i.Arguments[2].ToString()).Where(m => m != null).ToList();
var exitedWithOutputMessages = messages.Where(m => Regex.IsMatch(m!, "exited with output")).ToList();
Assert.NotNull(exitedWithOutputMessages);
Assert.Single(exitedWithOutputMessages);
var exitedWithOutputMessage = exitedWithOutputMessages.First();
Assert.NotNull(exitedWithOutputMessage);
Assert.Matches("output-line 1", exitedWithOutputMessage);
Assert.Matches("output-line 2", exitedWithOutputMessage);
Assert.Matches("output-line 3", exitedWithOutputMessage);
}
[Theory]
[MemberData(nameof(TorrentAndDownload))]
public async Task RunTorrentComplete_WhenStdErr_Logs(Torrent torrent, List<Download> downloads)
{
// Arrange
var settings = new DbSettings
{
General = new()
{
RunOnTorrentCompleteFileName = "/bin/echo"
}
};
var mocks = new Mocks();
mocks.TorrentDataMock.Setup(t => t.GetById(torrent.TorrentId)).Returns(Task.FromResult<Torrent?>(torrent));
mocks.DownloadsMock.Setup(d => d.GetForTorrent(torrent.TorrentId)).ReturnsAsync(downloads);
var downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
var torrentPath = $"{downloadPath}/{torrent.RdName}";
var filePath = $"{torrentPath}/{downloads[0].FileName}";
var fileSystemMock = new MockFileSystem(new Dictionary<String, MockFileData>
{
{
filePath, new("Test file")
}
});
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
fileSystemMock,
mocks.EnricherMock.Object,
null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine
null!,
null!,
null!,
null!);
mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny<Int32>()))
.Callback(() =>
{
mocks.ProcessMock.Raise(m => m.ErrorDataReceived += null, this, "error-line 1");
mocks.ProcessMock.Raise(m => m.ErrorDataReceived += null, this, "error-line 2");
mocks.ProcessMock.Raise(m => m.ErrorDataReceived += null, this, "error-line 3");
})
.Returns(true);
// Act
await torrents.RunTorrentComplete(torrent.TorrentId, settings);
// Assert
mocks.ProcessMock.Verify(p => p.BeginErrorReadLine(), Times.Once);
var messages = mocks.TorrentsLoggerMock.Invocations.Where(i => i.Method.Name == "Log").Select(i => i.Arguments[2].ToString()).Where(m => m != null).ToList();
var exitedWithOutputMessages = messages.Where(m => Regex.IsMatch(m!, "exited with errors")).ToList();
Assert.NotNull(exitedWithOutputMessages);
Assert.Single(exitedWithOutputMessages);
var exitedWithOutputMessage = exitedWithOutputMessages.First();
Assert.NotNull(exitedWithOutputMessage);
Assert.Matches("error-line 1", exitedWithOutputMessage);
Assert.Matches("error-line 2", exitedWithOutputMessage);
Assert.Matches("error-line 3", exitedWithOutputMessage);
}
[Fact]
public async Task AddNzbFileToDebridQueue_ShouldSetDownloadTypeNzb()
{
// Arrange
var mocks = new Mocks();
var torrent = new Torrent
{
TorrentId = Guid.NewGuid()
};
var nzbContent =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var bytes = Encoding.UTF8.GetBytes(nzbContent);
mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny<String>(),
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
new MockFileSystem(),
mocks.EnricherMock.Object,
null!,
null!,
null!,
null!,
null!);
// Act
await torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent);
// Assert
mocks.TorrentDataMock.Verify(t => t.Add(null,
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()),
Times.Once);
}
[Fact]
public async Task AddNzbLinkToDebridQueue_ShouldSetDownloadTypeNzb()
{
// Arrange
var mocks = new Mocks();
var torrent = new Torrent
{
TorrentId = Guid.NewGuid()
};
var link = "http://example.com/test.nzb";
mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny<String>(),
It.IsAny<String>(),
It.IsAny<String>(),
false,
DownloadType.Nzb,
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
mocks.TorrentDataMock.Object,
mocks.DownloadsMock.Object,
mocks.ProcessFactoryMock.Object,
new MockFileSystem(),
mocks.EnricherMock.Object,
null!,
null!,
null!,
null!,
null!);
// Act
await torrents.AddNzbLinkToDebridQueue(link, torrent);
// Assert
mocks.TorrentDataMock.Verify(t => t.Add(null,
It.IsAny<String>(),
link,
false,
DownloadType.Nzb,
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()),
Times.Once);
}
}