411 lines
17 KiB
C#
411 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.Wrappers;
|
|
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 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>())).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<Data.Enums.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<Data.Enums.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<Data.Enums.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<Data.Enums.DownloadClient>(),
|
|
It.IsAny<Torrent>()), Times.Once);
|
|
}
|
|
}
|