442 lines
18 KiB
C#
442 lines
18 KiB
C#
using System.Diagnostics;
|
|
using System.IO.Abstractions.TestingHelpers;
|
|
using System.Runtime.InteropServices;
|
|
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.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);
|
|
|
|
var category = torrent.Category!;
|
|
var torrentName = torrent.RdName!;
|
|
var fileName = downloads[0].FileName!;
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
settings.DownloadClient.DownloadPath = @"C:\Downloads";
|
|
}
|
|
|
|
var downloadPath = Path.Combine(settings.DownloadClient.DownloadPath, category);
|
|
var torrentPath = Path.Combine(downloadPath, torrentName);
|
|
var 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!,
|
|
new TestSettings(),
|
|
new TorrentRunnerState());
|
|
|
|
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 = Path.Combine(settings.DownloadClient.DownloadPath, torrent.Category ?? "");
|
|
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "");
|
|
var filePath = Path.Combine(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!,
|
|
new TestSettings(),
|
|
new TorrentRunnerState());
|
|
|
|
//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 = Path.Combine(settings.DownloadClient.DownloadPath, torrent.Category ?? "");
|
|
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "");
|
|
var filePath = Path.Combine(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!,
|
|
new TestSettings(),
|
|
new TorrentRunnerState());
|
|
|
|
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 = Path.Combine(settings.DownloadClient.DownloadPath, torrent.Category ?? "");
|
|
var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "");
|
|
var filePath = Path.Combine(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!,
|
|
new TestSettings(),
|
|
new TorrentRunnerState());
|
|
|
|
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!,
|
|
new TestSettings(),
|
|
new TorrentRunnerState());
|
|
|
|
// 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!,
|
|
new TestSettings(),
|
|
new TorrentRunnerState());
|
|
|
|
// 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);
|
|
}
|
|
}
|