Merge pull request #691 from Cucumberrbob/feat/unit-tests
feat: Add unit tests
This commit is contained in:
commit
ee62bc3098
22 changed files with 1748 additions and 46 deletions
28
server/RdtClient.Data/Data/ITorrentData.cs
Normal file
28
server/RdtClient.Data/Data/ITorrentData.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
using RdtClient.Data.Enums;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
|
public interface ITorrentData
|
||||||
|
{
|
||||||
|
Task<IList<Torrent>> Get();
|
||||||
|
Task<Torrent?> GetById(Guid torrentId);
|
||||||
|
Task<Torrent?> GetByHash(String hash);
|
||||||
|
|
||||||
|
Task<Torrent> Add(String rdId,
|
||||||
|
String hash,
|
||||||
|
String? fileOrMagnetContents,
|
||||||
|
Boolean isFile,
|
||||||
|
DownloadClient downloadClient,
|
||||||
|
Torrent torrent);
|
||||||
|
|
||||||
|
Task UpdateRdData(Torrent torrent);
|
||||||
|
Task Update(Torrent torrent);
|
||||||
|
Task UpdateCategory(Guid torrentId, String? category);
|
||||||
|
Task UpdateComplete(Guid torrentId, String? error, DateTimeOffset? datetime, Boolean retry);
|
||||||
|
Task UpdateFilesSelected(Guid torrentId, DateTimeOffset datetime);
|
||||||
|
Task UpdatePriority(Guid torrentId, Int32? priority);
|
||||||
|
Task UpdateRetry(Guid torrentId, DateTimeOffset? dateTime, Int32 retryCount);
|
||||||
|
Task UpdateError(Guid torrentId, String error);
|
||||||
|
Task Delete(Guid torrentId);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
namespace RdtClient.Data.Data;
|
namespace RdtClient.Data.Data;
|
||||||
|
|
||||||
public class TorrentData(DataContext dataContext)
|
public class TorrentData(DataContext dataContext) : ITorrentData
|
||||||
{
|
{
|
||||||
private static IList<Torrent>? _torrentCache;
|
private static IList<Torrent>? _torrentCache;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ public static class DiConfig
|
||||||
|
|
||||||
services.AddScoped<DownloadData>();
|
services.AddScoped<DownloadData>();
|
||||||
services.AddScoped<SettingData>();
|
services.AddScoped<SettingData>();
|
||||||
services.AddScoped<TorrentData>();
|
services.AddScoped<ITorrentData, TorrentData>();
|
||||||
services.AddScoped<UserData>();
|
services.AddScoped<UserData>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
269
server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs
Normal file
269
server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
|
using RdtClient.Service.Helpers;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Test.Helpers;
|
||||||
|
|
||||||
|
public class DownloadHelperTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithPath_WhenRdNameNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/file.txt",
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithoutPath_WhenRdNameNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/file.txt",
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath(torrent, download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithPath_WhenDownloadLinkNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = null,
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithoutPath_WhenDownloadLinkNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = null,
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath(torrent, download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithPath_WhenDownloadFileNameNull_UsesLinkToGuessFileName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/filename-from-link.txt",
|
||||||
|
FileName = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download, fileSystem);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedPath = Path.Combine("/data/downloads", torrent.RdName, "filename-from-link.txt");
|
||||||
|
Assert.Equal(expectedPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithoutPath_WhenDownloadFileNameNull_UsesLinkToGuessFileName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/filename-from-link.txt",
|
||||||
|
FileName = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath(torrent, download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedPath = Path.Combine(torrent.RdName, "filename-from-link.txt");
|
||||||
|
Assert.Equal(expectedPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithPath_WhenValid_CreatesDirectory()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/file.txt",
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download, fileSystem);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedDirectoryPath = Path.Combine("/data/downloads", torrent.RdName);
|
||||||
|
Assert.True(fileSystem.Directory.Exists(expectedDirectoryPath));
|
||||||
|
var expectedPath = Path.Combine(expectedDirectoryPath, download.FileName);
|
||||||
|
Assert.Equal(expectedPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithPath_WhenFileInSubdirectories_ReturnsPathWithSubdirectories()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/file.txt",
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileRelativePath = "inside/lots/of/subdirectories/file.txt";
|
||||||
|
|
||||||
|
IList<TorrentClientFile> files =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Path = fileRelativePath
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name",
|
||||||
|
RdFiles = JsonSerializer.Serialize(files)
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download, fileSystem);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedPath = Path.Combine("/data/downloads", torrent.RdName, fileRelativePath);
|
||||||
|
Assert.Equal(expectedPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithoutPath_WhenFileInSubdirectories_ReturnsPathWithSubdirectories()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url/file.txt",
|
||||||
|
FileName = "file.txt"
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileRelativePath = "inside/lots/of/subdirectories/file.txt";
|
||||||
|
|
||||||
|
IList<TorrentClientFile> files =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Path = fileRelativePath
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name",
|
||||||
|
RdFiles = JsonSerializer.Serialize(files)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath(torrent, download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedPath = Path.Combine(torrent.RdName, fileRelativePath);
|
||||||
|
Assert.Equal(expectedPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is probably a bug
|
||||||
|
[Fact]
|
||||||
|
public void GetDownloadPath_WithPath_WhenNoUriSegmentsOrFileName_ReturnsTorrentDirectory()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var download = new Download
|
||||||
|
{
|
||||||
|
Link = "https://fake.url", // HttpUtility.UrlDecode(new Uri("https://fake.url").Segments.Last()) == "/"
|
||||||
|
FileName = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdName = "Torrent Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download, fileSystem);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var expectedPath = Path.Combine("/data/downloads", torrent.RdName);
|
||||||
|
Assert.Equal(expectedPath, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
server/RdtClient.Service.Test/RdtClient.Service.Test.csproj
Normal file
31
server/RdtClient.Service.Test/RdtClient.Service.Test.csproj
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<AssemblyName>RdtClient.Service.Test</AssemblyName>
|
||||||
|
<RootNamespace>RdtClient.Service.Test</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AllDebrid.NET" Version="1.0.17" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="21.3.1" />
|
||||||
|
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="21.3.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.2"/>
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\RdtClient.Service\RdtClient.Service.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
using Moq;
|
||||||
|
using RdtClient.Service.Helpers;
|
||||||
|
using RdtClient.Service.Services.Downloaders;
|
||||||
|
using Synology.Api.Client;
|
||||||
|
using Synology.Api.Client.Apis.DownloadStation;
|
||||||
|
using Synology.Api.Client.Apis.DownloadStation.Task.Models;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Test.Services.Downloaders;
|
||||||
|
|
||||||
|
class Mocks
|
||||||
|
{
|
||||||
|
public readonly String Gid;
|
||||||
|
public readonly Mock<ISynologyClient> SynologyClientMock = new();
|
||||||
|
public readonly Mock<IDownloadStationTaskEndpoint> TaskEndpointMock = new();
|
||||||
|
|
||||||
|
public Mocks(String gid = "123456")
|
||||||
|
{
|
||||||
|
Gid = gid;
|
||||||
|
var downloadStationApiMock = new Mock<IDownloadStationApi>();
|
||||||
|
downloadStationApiMock.Setup(a => a.TaskEndpoint()).Returns(TaskEndpointMock.Object);
|
||||||
|
SynologyClientMock.Setup(c => c.DownloadStationApi()).Returns(downloadStationApiMock.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeDelayProvider : IDelayProvider
|
||||||
|
{
|
||||||
|
public Task Delay(Int32 milliseconds)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DownloadStationDownloaderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_WhenRemotePathEmpty_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var synologyClientMock = new Mock<ISynologyClient>();
|
||||||
|
var gid = Guid.NewGuid();
|
||||||
|
|
||||||
|
var downloadStationDownloader = new DownloadStationDownloader(gid.ToString(),
|
||||||
|
"https://fake.url/file.txt",
|
||||||
|
"",
|
||||||
|
"/path/to/file.txt",
|
||||||
|
"download-path",
|
||||||
|
synologyClientMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Exception>(downloadStationDownloader.Download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("invalid file path", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
synologyClientMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_WhenAlreadyAdded_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
mocks.TaskEndpointMock.Setup(t => t.GetInfoAsync(mocks.Gid)).ReturnsAsync(new DownloadStationTask());
|
||||||
|
|
||||||
|
var downloadStationDownloader = new DownloadStationDownloader(mocks.Gid,
|
||||||
|
"https://fake.url/file.txt",
|
||||||
|
"/path/on/remote/file.txt",
|
||||||
|
"/path/to/file.txt",
|
||||||
|
"download-path",
|
||||||
|
mocks.SynologyClientMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Exception>(downloadStationDownloader.Download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("already been added", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.GetInfoAsync(mocks.Gid), Times.Once);
|
||||||
|
mocks.TaskEndpointMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_WhenAddedSuccessfully_ReturnsGid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
mocks.TaskEndpointMock.Setup(t => t.GetInfoAsync(mocks.Gid)).ThrowsAsync(new Exception());
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.Setup(t => t.ListAsync())
|
||||||
|
.ReturnsAsync(new DownloadStationTaskListResponse
|
||||||
|
{
|
||||||
|
Total = 0,
|
||||||
|
Offset = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.Setup(t => t.CreateAsync(It.IsAny<DownloadStationTaskCreateRequest>()))
|
||||||
|
.ReturnsAsync(new DownloadStationTaskCreateResponse
|
||||||
|
{
|
||||||
|
TaskId = [mocks.Gid]
|
||||||
|
});
|
||||||
|
|
||||||
|
var downloadStationDownloader = new DownloadStationDownloader(mocks.Gid,
|
||||||
|
"https://fake.url/file.txt",
|
||||||
|
"/path/on/remote/file.txt",
|
||||||
|
"/path/to/file.txt",
|
||||||
|
"download-path",
|
||||||
|
mocks.SynologyClientMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await downloadStationDownloader.Download();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(mocks.Gid, result);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.GetInfoAsync(mocks.Gid), Times.Once);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.ListAsync(), Times.Once);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.CreateAsync(It.IsAny<DownloadStationTaskCreateRequest>()), Times.Once);
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_After5Tries_Throws()
|
||||||
|
{
|
||||||
|
var mocks = new Mocks();
|
||||||
|
mocks.TaskEndpointMock.Setup(t => t.GetInfoAsync(mocks.Gid)).ThrowsAsync(new Exception());
|
||||||
|
|
||||||
|
var emptyListResponse = new DownloadStationTaskListResponse
|
||||||
|
{
|
||||||
|
Total = 0,
|
||||||
|
Offset = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.SetupSequence(t => t.ListAsync())
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse);
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.SetupSequence(t => t.CreateAsync(It.IsAny<DownloadStationTaskCreateRequest>()))
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception());
|
||||||
|
|
||||||
|
var downloadStationDownloader = new DownloadStationDownloader(mocks.Gid,
|
||||||
|
"https://fake.url/file.txt",
|
||||||
|
"/path/on/remote/file.txt",
|
||||||
|
"/path/to/file.txt",
|
||||||
|
"download-path",
|
||||||
|
mocks.SynologyClientMock.Object,
|
||||||
|
new FakeDelayProvider());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<Exception>(downloadStationDownloader.Download);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("unable to download", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.GetInfoAsync(mocks.Gid), Times.Once);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.ListAsync(), Times.Exactly(5));
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.CreateAsync(It.IsAny<DownloadStationTaskCreateRequest>()), Times.Exactly(5));
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_WhenSuccessfulAfter4Tries_ReturnsGid()
|
||||||
|
{
|
||||||
|
var mocks = new Mocks();
|
||||||
|
mocks.TaskEndpointMock.Setup(t => t.GetInfoAsync(mocks.Gid)).ThrowsAsync(new Exception());
|
||||||
|
|
||||||
|
var emptyListResponse = new DownloadStationTaskListResponse
|
||||||
|
{
|
||||||
|
Total = 0,
|
||||||
|
Offset = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.SetupSequence(t => t.ListAsync())
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse)
|
||||||
|
.ReturnsAsync(emptyListResponse);
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.SetupSequence(t => t.CreateAsync(It.IsAny<DownloadStationTaskCreateRequest>()))
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ThrowsAsync(new Exception())
|
||||||
|
.ReturnsAsync(new DownloadStationTaskCreateResponse
|
||||||
|
{
|
||||||
|
TaskId = [mocks.Gid]
|
||||||
|
});
|
||||||
|
|
||||||
|
var downloadStationDownloader = new DownloadStationDownloader(mocks.Gid,
|
||||||
|
"https://fake.url/file.txt",
|
||||||
|
"/path/on/remote/file.txt",
|
||||||
|
"/path/to/file.txt",
|
||||||
|
"download-path",
|
||||||
|
mocks.SynologyClientMock.Object,
|
||||||
|
new FakeDelayProvider());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await downloadStationDownloader.Download();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(mocks.Gid, result);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.GetInfoAsync(mocks.Gid), Times.Once);
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.ListAsync(), Times.Exactly(5));
|
||||||
|
mocks.TaskEndpointMock.Verify(t => t.CreateAsync(It.IsAny<DownloadStationTaskCreateRequest>()), Times.Exactly(5));
|
||||||
|
|
||||||
|
mocks.TaskEndpointMock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,705 @@
|
||||||
|
using AllDebridNET;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using RdtClient.Data.Enums;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
|
using RdtClient.Service.Services.TorrentClients;
|
||||||
|
using File = AllDebridNET.File;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Test.Services.TorrentClients;
|
||||||
|
|
||||||
|
public class AllDebridTorrentClientTest
|
||||||
|
{
|
||||||
|
private static readonly Magnet Magnet1HalfDownloaded = new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Filename = "some-files",
|
||||||
|
Hash = "hash-1",
|
||||||
|
Status = "Downloading",
|
||||||
|
StatusCode = 1,
|
||||||
|
Downloaded = 50,
|
||||||
|
Size = 100,
|
||||||
|
Uploaded = 0,
|
||||||
|
Seeders = 1,
|
||||||
|
DownloadSpeed = 5,
|
||||||
|
UploadSpeed = 0,
|
||||||
|
UploadDate = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero).Second,
|
||||||
|
CompletionDate = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Magnet Magnet1Finished = new()
|
||||||
|
{
|
||||||
|
Id = Magnet1HalfDownloaded.Id,
|
||||||
|
Filename = Magnet1HalfDownloaded.Filename,
|
||||||
|
Hash = Magnet1HalfDownloaded.Hash,
|
||||||
|
Status = "Ready",
|
||||||
|
StatusCode = 4,
|
||||||
|
Size = Magnet1HalfDownloaded.Size,
|
||||||
|
UploadDate = Magnet1HalfDownloaded.UploadDate,
|
||||||
|
CompletionDate = new DateTimeOffset(2020, 1, 1, 1, 0, 0, TimeSpan.Zero).Second
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Magnet Magnet2QuarterDownloaded = new()
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Filename = "some-other-files",
|
||||||
|
Hash = "hash-2",
|
||||||
|
Status = "Downloading",
|
||||||
|
StatusCode = 1,
|
||||||
|
Downloaded = 100,
|
||||||
|
Size = 400,
|
||||||
|
Uploaded = 0,
|
||||||
|
Seeders = 1,
|
||||||
|
DownloadSpeed = 5,
|
||||||
|
UploadSpeed = 0,
|
||||||
|
UploadDate = new DateTimeOffset(2020, 1, 1, 0, 5, 0, TimeSpan.Zero).Second,
|
||||||
|
CompletionDate = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Magnet Magnet2Finished = new()
|
||||||
|
{
|
||||||
|
Id = Magnet2QuarterDownloaded.Id,
|
||||||
|
Filename = Magnet2QuarterDownloaded.Filename,
|
||||||
|
Hash = Magnet2QuarterDownloaded.Hash,
|
||||||
|
Status = "Ready",
|
||||||
|
StatusCode = 4,
|
||||||
|
Size = Magnet2QuarterDownloaded.Size,
|
||||||
|
UploadDate = Magnet2QuarterDownloaded.UploadDate,
|
||||||
|
CompletionDate = new DateTimeOffset(2020, 1, 1, 1, 5, 0, TimeSpan.Zero).Second
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly List<File> IncludeExcludeFiles =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "include.txt",
|
||||||
|
Size = 1,
|
||||||
|
DownloadLink = "https://fake.url/include.txt"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "exclude.txt",
|
||||||
|
Size = 1,
|
||||||
|
DownloadLink = "https://fake.url/exclude.txt"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "include-folder",
|
||||||
|
SubNodes =
|
||||||
|
[
|
||||||
|
new File
|
||||||
|
{
|
||||||
|
FolderOrFileName = "include-folder-exclude-file.txt",
|
||||||
|
Size = 1,
|
||||||
|
DownloadLink = "https://fake.url/include-folder-exclude-file.txt"
|
||||||
|
},
|
||||||
|
new File
|
||||||
|
{
|
||||||
|
FolderOrFileName = "include-folder-include-file.txt",
|
||||||
|
Size = 1,
|
||||||
|
DownloadLink = "https://fake.url/include-folder-include-file.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "exclude-folder",
|
||||||
|
SubNodes =
|
||||||
|
[
|
||||||
|
new File
|
||||||
|
{
|
||||||
|
FolderOrFileName = "exclude-folder-exclude-file.txt",
|
||||||
|
Size = 1,
|
||||||
|
DownloadLink = "https://fake.url/exclude-folder-exclude-file.txt"
|
||||||
|
},
|
||||||
|
new File
|
||||||
|
{
|
||||||
|
FolderOrFileName = "exclude-folder-include-file.txt",
|
||||||
|
Size = 1,
|
||||||
|
DownloadLink = "https://fake.url/exclude-folder-include-file.txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrents_WhenFullSyncNoTorrents_ReturnsEmptyList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [],
|
||||||
|
Fullsync = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetTorrents();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrents_WhenPartialSyncNoTorrents_ReturnsEmptyList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [],
|
||||||
|
Fullsync = true
|
||||||
|
})
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 2,
|
||||||
|
Magnets = [],
|
||||||
|
Fullsync = false
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetTorrents();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrents_WhenTorrentsFullSync_ReturnsOnlyTorrentsFromResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [Magnet1Finished],
|
||||||
|
Fullsync = true
|
||||||
|
})
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 2,
|
||||||
|
Magnets = [Magnet2Finished],
|
||||||
|
Fullsync = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
// `GetTorrents` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`,
|
||||||
|
// so when the second call `_cache.Add`s, it also affects `firstResult`.
|
||||||
|
// `.ToList()` clones it so it won't be changed by the second call
|
||||||
|
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList();
|
||||||
|
var secondResult = await allDebridTorrentClient.GetTorrents();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, firstResult.Count);
|
||||||
|
Assert.Equal(Magnet1Finished.Id.ToString(), firstResult.First().Id);
|
||||||
|
Assert.Equal(1, secondResult.Count);
|
||||||
|
Assert.Equal(Magnet2Finished.Id.ToString(), secondResult.First().Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrents_WhenPartialSyncResponseHasNewTorrent_ReturnsCachedAndNewTorrents()
|
||||||
|
{
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [Magnet1Finished],
|
||||||
|
Fullsync = true
|
||||||
|
})
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 2,
|
||||||
|
Magnets = [Magnet2Finished],
|
||||||
|
Fullsync = false
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
// `GetTorrents` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`,
|
||||||
|
// so when the second call `_cache.Add`s, it also affects `firstResult`.
|
||||||
|
// `.ToList()` clones it so it won't be changed by the second call
|
||||||
|
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList();
|
||||||
|
var secondResult = await allDebridTorrentClient.GetTorrents();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, firstResult.Count);
|
||||||
|
Assert.Equal(Magnet1Finished.Id.ToString(), firstResult[0].Id);
|
||||||
|
Assert.Equal(2, secondResult.Count);
|
||||||
|
Assert.Contains(secondResult, t => t.Id == Magnet1Finished.Id.ToString());
|
||||||
|
Assert.Contains(secondResult, t => t.Id == Magnet2Finished.Id.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTorrents_WhenPartialSyncResponseHasUpdate_ReturnsCachedAndUpdatedTorrents()
|
||||||
|
{
|
||||||
|
// Double check the fakes are as we expect
|
||||||
|
Assert.Equal(Magnet1Finished.Id, Magnet1HalfDownloaded.Id);
|
||||||
|
Assert.NotEqual(Magnet1Finished.Status, Magnet1HalfDownloaded.Status);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [Magnet1HalfDownloaded, Magnet2Finished],
|
||||||
|
Fullsync = true
|
||||||
|
})
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 2,
|
||||||
|
Magnets = [Magnet1Finished],
|
||||||
|
Fullsync = false
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
// `GetTorrents` returns a reference to `_cache` if the `StatusLiveAsync` has `Fullsync = false`,
|
||||||
|
// so when the second call `_cache.Add`s, it also affects `firstResult`.
|
||||||
|
// `.ToList()` clones it so it won't be changed by the second call
|
||||||
|
var firstResult = (await allDebridTorrentClient.GetTorrents()).ToList();
|
||||||
|
var secondResult = await allDebridTorrentClient.GetTorrents();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, firstResult.Count);
|
||||||
|
Assert.Contains(firstResult, t => t.Id == Magnet1HalfDownloaded.Id.ToString() && t.Status == Magnet1HalfDownloaded.Status);
|
||||||
|
Assert.Contains(firstResult, t => t.Id == Magnet2Finished.Id.ToString());
|
||||||
|
Assert.Equal(2, secondResult.Count);
|
||||||
|
Assert.Contains(secondResult, t => t.Id == Magnet1Finished.Id.ToString() && t.Status == Magnet1Finished.Status);
|
||||||
|
Assert.Contains(secondResult, t => t.Id == Magnet2Finished.Id.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<Object[]> DownloadingMagnetsWithProgress()
|
||||||
|
{
|
||||||
|
return [[Magnet1HalfDownloaded, 50], [Magnet2QuarterDownloaded, 25]];
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(DownloadingMagnetsWithProgress))]
|
||||||
|
public async Task Map_WhenTorrentDownloading_ComputesProgress(Magnet magnet, Int64 expectedProgress)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [magnet],
|
||||||
|
Fullsync = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
// We have to use `GetTorrents` since `Map` is private
|
||||||
|
var result = await allDebridTorrentClient.GetTorrents();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedProgress, result.First().Progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateData_WhenTorrentRdIdNull_ReturnsUnmodifiedTorrent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = null,
|
||||||
|
Hash = "hash",
|
||||||
|
RdName = "rdName"
|
||||||
|
};
|
||||||
|
|
||||||
|
var serializedOriginal = JsonConvert.SerializeObject(torrent);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.UpdateData(torrent, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(serializedOriginal, JsonConvert.SerializeObject(result));
|
||||||
|
mocks.AllDebridClientFactoryMock.Verify(f => f.GetClient(), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateData_WhenTorrentClientTorrentNotProvided_FetchesFromAPIAndUpdatesTorrentAccordingly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = Magnet1Finished.Id.ToString(),
|
||||||
|
Hash = Magnet1Finished.Hash!,
|
||||||
|
RdName = null,
|
||||||
|
RdSize = null,
|
||||||
|
RdStatus = null
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>())).ReturnsAsync(Magnet1Finished);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.UpdateData(torrent, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mocks.AllDebridClientMock.Verify(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
Assert.Equal(Magnet1Finished.Filename, result.RdName);
|
||||||
|
Assert.Equal(Magnet1Finished.Size, result.RdSize);
|
||||||
|
Assert.Equal(Provider.AllDebrid, result.ClientKind);
|
||||||
|
Assert.Equal(TorrentStatus.Finished, result.RdStatus);
|
||||||
|
|
||||||
|
// It mutates the original object:
|
||||||
|
Assert.Equal(torrent, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateData_WhenTorrentClientTorrentNotProvidedAndTorrentDeletedFromAD_UpdatesRdStatusRaw()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "123456",
|
||||||
|
Hash = "fake-hash-123456",
|
||||||
|
RdStatus = null,
|
||||||
|
RdStatusRaw = null
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny<CancellationToken>()))
|
||||||
|
.ThrowsAsync(new AllDebridException("Magnet not found", "MAGNET_INVALID_ID"));
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.UpdateData(torrent, null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("deleted", result.RdStatusRaw);
|
||||||
|
Assert.Null(result.RdStatus);
|
||||||
|
|
||||||
|
// It mutates the original object:
|
||||||
|
Assert.Equal(torrent, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateData_WhenTorrentClientTorrentIsProvided_DoesNotFetchFromApiAndUpdatesTorrentAccordingly()
|
||||||
|
{
|
||||||
|
// Double check fakes are as we expect
|
||||||
|
Assert.Equal(4, Magnet1Finished.StatusCode);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = Magnet1Finished.Id.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.SetupSequence(c => c.Magnet.StatusLiveAsync(It.IsAny<Int64>(), It.IsAny<Int64>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new MagnetStatusLiveResponse
|
||||||
|
{
|
||||||
|
Counter = 1,
|
||||||
|
Magnets = [Magnet2Finished],
|
||||||
|
Fullsync = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
var torrentClientTorrent = (await allDebridTorrentClient.GetTorrents()).First();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.UpdateData(torrent, torrentClientTorrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(torrentClientTorrent.Filename, torrent.RdName);
|
||||||
|
Assert.Equal(torrentClientTorrent.Bytes, result.RdSize);
|
||||||
|
Assert.Equal(TorrentStatus.Finished, torrent.RdStatus);
|
||||||
|
Assert.Equal(Provider.AllDebrid, torrent.ClientKind);
|
||||||
|
|
||||||
|
// It mutates the original object:
|
||||||
|
Assert.Equal(torrent, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_WhenTorrentRdIdNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = null
|
||||||
|
};
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
mocks.AllDebridClientFactoryMock.Verify(f => f.GetClient(), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_ByDefault_GetsLinksForAllFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
List<File> files =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "file-1.txt",
|
||||||
|
Size = 50,
|
||||||
|
DownloadLink = "https://fake.url/file-1.txt"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "file-2.txt",
|
||||||
|
Size = 150,
|
||||||
|
DownloadLink = "https://fake.url/file-2.txt"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var expectedLinksSet = new HashSet<String>(files.Select(n => n.DownloadLink)!);
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(files);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var linksSet = new HashSet<String>(result);
|
||||||
|
Assert.Equal(expectedLinksSet, linksSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_WhenDownloadMinSizeSet_GetsLinksForOnlyFilesAboveThatSize()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "1",
|
||||||
|
|
||||||
|
// NB: this is in MB, the file sizes below are in B
|
||||||
|
DownloadMinSize = 100
|
||||||
|
};
|
||||||
|
|
||||||
|
List<File> files =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "too-small.txt",
|
||||||
|
Size = (torrent.DownloadMinSize - 1) * 1024 * 1024,
|
||||||
|
DownloadLink = "https://fake.url/too-small.txt"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "bigger-than-min.txt",
|
||||||
|
Size = (torrent.DownloadMinSize + 1) * 1024 * 1024,
|
||||||
|
DownloadLink = "https://fake.url/bigger-than-min.txt"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var expectedLinksSet = new HashSet<String>(files.Where(m => m.Size > torrent.DownloadMinSize * 1024 * 1024).Select(n => n.DownloadLink)!);
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(files);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var linksSet = new HashSet<String>(result);
|
||||||
|
Assert.Equal(expectedLinksSet, linksSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_WhenIncludeRegexSet_GetsLinksForOnlyFilesMatchingRegex()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "1",
|
||||||
|
IncludeRegex = "include"
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedLinksSet = new HashSet<String>([
|
||||||
|
"https://fake.url/include.txt", "https://fake.url/include-folder-exclude-file.txt", "https://fake.url/include-folder-include-file.txt",
|
||||||
|
"https://fake.url/exclude-folder-include-file.txt"
|
||||||
|
]);
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(IncludeExcludeFiles);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var linksSet = new HashSet<String>(result);
|
||||||
|
Assert.Equal(expectedLinksSet, linksSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_WhenExcludeRegexSet_GetsLinksForOnlyFilesNotMatchingRegex()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "1",
|
||||||
|
ExcludeRegex = "exclude"
|
||||||
|
};
|
||||||
|
|
||||||
|
var expectedLinksSet = new HashSet<String>([
|
||||||
|
"https://fake.url/include.txt", "https://fake.url/include-folder-include-file.txt"
|
||||||
|
]);
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(IncludeExcludeFiles);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var linksSet = new HashSet<String>(result);
|
||||||
|
Assert.Equal(expectedLinksSet, linksSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_WhenIncludeAndExcludeRegexSet_GetsLinksForOnlyFilesMatchingIncludeRegexEvenIfNotMatchingExcludeRegex()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrentInclude = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "1",
|
||||||
|
IncludeRegex = "include"
|
||||||
|
};
|
||||||
|
|
||||||
|
var torrentIncludeExclude = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "2",
|
||||||
|
IncludeRegex = "include",
|
||||||
|
ExcludeRegex = "exclude"
|
||||||
|
};
|
||||||
|
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(It.IsAny<Int64>(), It.IsAny<CancellationToken>())).ReturnsAsync(IncludeExcludeFiles);
|
||||||
|
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var includeOnlyResult = await allDebridTorrentClient.GetDownloadLinks(torrentInclude);
|
||||||
|
var includeExcludeResult = await allDebridTorrentClient.GetDownloadLinks(torrentIncludeExclude);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(includeOnlyResult);
|
||||||
|
Assert.NotNull(includeExcludeResult);
|
||||||
|
var includeOnlyLinksSet = new HashSet<String>(includeOnlyResult);
|
||||||
|
var includeExcludeLinksSet = new HashSet<String>(includeExcludeResult);
|
||||||
|
Assert.Equal(includeOnlyLinksSet, includeExcludeLinksSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDownloadLinks_WhenAllFilesExcluded_ReturnsAllFiles()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mocks = new Mocks();
|
||||||
|
|
||||||
|
var torrent = new Torrent
|
||||||
|
{
|
||||||
|
RdId = "1",
|
||||||
|
|
||||||
|
// NB: this is in MB, the file sizes below are in B
|
||||||
|
DownloadMinSize = 100
|
||||||
|
};
|
||||||
|
|
||||||
|
List<File> files =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "too-small-1.txt",
|
||||||
|
Size = (torrent.DownloadMinSize - 1) * 1024 * 1024,
|
||||||
|
DownloadLink = "https://fake.url/too-small-1.txt"
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FolderOrFileName = "too-small-2.txt",
|
||||||
|
Size = (torrent.DownloadMinSize - 1) * 1024 * 1024,
|
||||||
|
DownloadLink = "https://fake.url/too-small-2.txt"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var expectedLinksSet = new HashSet<String>(files.Select(n => n.DownloadLink)!);
|
||||||
|
mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny<CancellationToken>())).ReturnsAsync(files);
|
||||||
|
var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await allDebridTorrentClient.GetDownloadLinks(torrent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
var linksSet = new HashSet<String>(result);
|
||||||
|
Assert.Equal(expectedLinksSet, linksSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Mocks
|
||||||
|
{
|
||||||
|
public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock;
|
||||||
|
public readonly Mock<IAllDebridNETClient> AllDebridClientMock;
|
||||||
|
public readonly Mock<ILogger<AllDebridTorrentClient>> LoggerMock;
|
||||||
|
|
||||||
|
public Mocks()
|
||||||
|
{
|
||||||
|
LoggerMock = new Mock<ILogger<AllDebridTorrentClient>>();
|
||||||
|
AllDebridClientMock = new Mock<IAllDebridNETClient>();
|
||||||
|
AllDebridClientFactoryMock = new Mock<IAllDebridNetClientFactory>();
|
||||||
|
AllDebridClientFactoryMock.Setup(f => f.GetClient()).Returns(AllDebridClientMock.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
308
server/RdtClient.Service.Test/Services/TorrentsTest.cs
Normal file
308
server/RdtClient.Service.Test/Services/TorrentsTest.cs
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
class Mocks
|
||||||
|
{
|
||||||
|
public readonly Mock<IProcessFactory> ProcessFactoryMock;
|
||||||
|
public readonly Mock<IProcess> ProcessMock;
|
||||||
|
public readonly Mock<ILogger<TorrentsService>> TorrentsLoggerMock;
|
||||||
|
public readonly Mock<IDownloads> DownloadsMock;
|
||||||
|
public readonly Mock<ITorrentData> TorrentDataMock;
|
||||||
|
|
||||||
|
public Mocks()
|
||||||
|
{
|
||||||
|
TorrentDataMock = new();
|
||||||
|
DownloadsMock = 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 IEnumerable<Object[]> TorrentAndDownload()
|
||||||
|
{
|
||||||
|
var torrent = new Torrent()
|
||||||
|
{
|
||||||
|
RdName = "TestTorrent",
|
||||||
|
Hash = "123ABC",
|
||||||
|
Category = "Movies",
|
||||||
|
RdSize = 100,
|
||||||
|
TorrentId = new Guid()
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Download> downloads =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FileName = "file.txt",
|
||||||
|
TorrentId = torrent.TorrentId
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
yield return new Object[]
|
||||||
|
{
|
||||||
|
torrent, downloads
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(TorrentAndDownload))]
|
||||||
|
public async Task RunTorrentComplete_WhenCommandSet_ShouldRunCommand(Torrent torrent, List<Download> downloads)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var settings = new DbSettings
|
||||||
|
{
|
||||||
|
General = new DbSettingsGeneral()
|
||||||
|
{
|
||||||
|
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 MockFileData("Test file")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
|
||||||
|
mocks.TorrentDataMock.Object,
|
||||||
|
mocks.DownloadsMock.Object,
|
||||||
|
mocks.ProcessFactoryMock.Object,
|
||||||
|
fileSystemMock,
|
||||||
|
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}");
|
||||||
|
Assert.Equal(expectedArgumentsSb.ToString(), 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 DbSettingsGeneral()
|
||||||
|
{
|
||||||
|
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 MockFileData("Test file")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
|
||||||
|
mocks.TorrentDataMock.Object,
|
||||||
|
mocks.DownloadsMock.Object,
|
||||||
|
mocks.ProcessFactoryMock.Object,
|
||||||
|
fileSystemMock,
|
||||||
|
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 DbSettingsGeneral()
|
||||||
|
{
|
||||||
|
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 MockFileData("Test file")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
|
||||||
|
mocks.TorrentDataMock.Object,
|
||||||
|
mocks.DownloadsMock.Object,
|
||||||
|
mocks.ProcessFactoryMock.Object,
|
||||||
|
fileSystemMock,
|
||||||
|
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 DbSettingsGeneral()
|
||||||
|
{
|
||||||
|
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 MockFileData("Test file")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var torrents = new TorrentsService(mocks.TorrentsLoggerMock.Object,
|
||||||
|
mocks.TorrentDataMock.Object,
|
||||||
|
mocks.DownloadsMock.Object,
|
||||||
|
mocks.ProcessFactoryMock.Object,
|
||||||
|
fileSystemMock,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Net;
|
using System.IO.Abstractions;
|
||||||
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Polly;
|
using Polly;
|
||||||
|
|
@ -7,17 +8,24 @@ using RdtClient.Service.BackgroundServices;
|
||||||
using RdtClient.Service.Middleware;
|
using RdtClient.Service.Middleware;
|
||||||
using RdtClient.Service.Services;
|
using RdtClient.Service.Services;
|
||||||
using RdtClient.Service.Services.TorrentClients;
|
using RdtClient.Service.Services.TorrentClients;
|
||||||
|
using RdtClient.Service.Wrappers;
|
||||||
|
|
||||||
namespace RdtClient.Service;
|
namespace RdtClient.Service;
|
||||||
|
|
||||||
public static class DiConfig
|
public static class DiConfig
|
||||||
{
|
{
|
||||||
public const String RD_CLIENT = "RdClient";
|
public const String RD_CLIENT = "RdClient";
|
||||||
|
|
||||||
public static void RegisterRdtServices(this IServiceCollection services)
|
public static void RegisterRdtServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<IAllDebridNetClientFactory, AllDebridNetClientFactory>();
|
||||||
services.AddScoped<AllDebridTorrentClient>();
|
services.AddScoped<AllDebridTorrentClient>();
|
||||||
|
|
||||||
|
services.AddSingleton<IProcessFactory, ProcessFactory>();
|
||||||
|
services.AddSingleton<IFileSystem, FileSystem>();
|
||||||
|
|
||||||
services.AddScoped<Authentication>();
|
services.AddScoped<Authentication>();
|
||||||
|
services.AddScoped<IDownloads, Downloads>();
|
||||||
services.AddScoped<Downloads>();
|
services.AddScoped<Downloads>();
|
||||||
services.AddScoped<PremiumizeTorrentClient>();
|
services.AddScoped<PremiumizeTorrentClient>();
|
||||||
services.AddScoped<QBittorrent>();
|
services.AddScoped<QBittorrent>();
|
||||||
|
|
@ -30,7 +38,7 @@ public static class DiConfig
|
||||||
services.AddScoped<DebridLinkClient>();
|
services.AddScoped<DebridLinkClient>();
|
||||||
|
|
||||||
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
|
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();
|
||||||
|
|
||||||
services.AddHostedService<ProviderUpdater>();
|
services.AddHostedService<ProviderUpdater>();
|
||||||
services.AddHostedService<Startup>();
|
services.AddHostedService<Startup>();
|
||||||
services.AddHostedService<TaskRunner>();
|
services.AddHostedService<TaskRunner>();
|
||||||
|
|
@ -50,4 +58,4 @@ public static class DiConfig
|
||||||
services.AddHttpClient(RD_CLIENT)
|
services.AddHttpClient(RD_CLIENT)
|
||||||
.AddPolicyHandler(retryPolicy);
|
.AddPolicyHandler(retryPolicy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
using RdtClient.Data.Models.Data;
|
using System.IO.Abstractions;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
|
||||||
namespace RdtClient.Service.Helpers;
|
namespace RdtClient.Service.Helpers;
|
||||||
|
|
||||||
public static class DownloadHelper
|
public static class DownloadHelper
|
||||||
{
|
{
|
||||||
public static String? GetDownloadPath(String downloadPath, Torrent torrent, Download download)
|
public static String? GetDownloadPath(String downloadPath, Torrent torrent, Download download, IFileSystem? fileSystem = null)
|
||||||
{
|
{
|
||||||
var fileUrl = download.Link;
|
var fileUrl = download.Link;
|
||||||
|
|
||||||
|
|
@ -41,9 +42,11 @@ public static class DownloadHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Directory.Exists(torrentPath))
|
fileSystem ??= new FileSystem();
|
||||||
|
|
||||||
|
if (!fileSystem.Directory.Exists(torrentPath))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(torrentPath);
|
fileSystem.Directory.CreateDirectory(torrentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath = Path.Combine(torrentPath, fileName);
|
var filePath = Path.Combine(torrentPath, fileName);
|
||||||
|
|
|
||||||
6
server/RdtClient.Service/Helpers/IDelayProvider.cs
Normal file
6
server/RdtClient.Service/Helpers/IDelayProvider.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace RdtClient.Service.Helpers;
|
||||||
|
|
||||||
|
public interface IDelayProvider
|
||||||
|
{
|
||||||
|
public Task Delay(Int32 milliseconds);
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||||
<PackageReference Include="Synology.Api.Client" Version="0.3.87" />
|
<PackageReference Include="Synology.Api.Client" Version="0.3.87" />
|
||||||
|
<PackageReference Include="TestableIO.System.IO.Abstractions" Version="21.3.1" />
|
||||||
<PackageReference Include="TorBox.NET" Version="1.4.0" />
|
<PackageReference Include="TorBox.NET" Version="1.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
using Serilog;
|
using RdtClient.Service.Helpers;
|
||||||
|
using Serilog;
|
||||||
using Synology.Api.Client;
|
using Synology.Api.Client;
|
||||||
using Synology.Api.Client.Apis.DownloadStation.Task.Models;
|
using Synology.Api.Client.Apis.DownloadStation.Task.Models;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services.Downloaders;
|
namespace RdtClient.Service.Services.Downloaders;
|
||||||
|
|
||||||
|
class DelayProvider : IDelayProvider
|
||||||
|
{
|
||||||
|
public Task Delay(Int32 delay)
|
||||||
|
{
|
||||||
|
return Task.Delay(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class DownloadStationDownloader : IDownloader
|
public class DownloadStationDownloader : IDownloader
|
||||||
{
|
{
|
||||||
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
|
public event EventHandler<DownloadCompleteEventArgs>? DownloadComplete;
|
||||||
|
|
@ -11,16 +20,17 @@ public class DownloadStationDownloader : IDownloader
|
||||||
|
|
||||||
private const Int32 RetryCount = 5;
|
private const Int32 RetryCount = 5;
|
||||||
|
|
||||||
private readonly SynologyClient _synologyClient;
|
private readonly ISynologyClient _synologyClient;
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly String _filePath;
|
private readonly String _filePath;
|
||||||
private readonly String _uri;
|
private readonly String _uri;
|
||||||
private readonly String? _remotePath;
|
private readonly String? _remotePath;
|
||||||
|
private readonly IDelayProvider _delayProvider;
|
||||||
|
|
||||||
private String? _gid;
|
private String? _gid;
|
||||||
|
|
||||||
private DownloadStationDownloader(String? gid, String uri, String? remotePath, String filePath, String downloadPath, SynologyClient synologyClient)
|
public DownloadStationDownloader(String? gid, String uri, String? remotePath, String filePath, String downloadPath, ISynologyClient synologyClient, IDelayProvider? delayProvider = null)
|
||||||
{
|
{
|
||||||
_logger = Log.ForContext<DownloadStationDownloader>();
|
_logger = Log.ForContext<DownloadStationDownloader>();
|
||||||
_logger.Debug($"Instantiated new DownloadStation Downloader for URI {uri} to filePath {filePath} and downloadPath {downloadPath} and GID {gid}");
|
_logger.Debug($"Instantiated new DownloadStation Downloader for URI {uri} to filePath {filePath} and downloadPath {downloadPath} and GID {gid}");
|
||||||
|
|
@ -30,6 +40,7 @@ public class DownloadStationDownloader : IDownloader
|
||||||
_uri = uri;
|
_uri = uri;
|
||||||
_remotePath = remotePath;
|
_remotePath = remotePath;
|
||||||
_synologyClient = synologyClient;
|
_synologyClient = synologyClient;
|
||||||
|
_delayProvider = delayProvider ?? new DelayProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<DownloadStationDownloader> Init(String? gid, String uri, String filePath, String downloadPath, String? category)
|
public static async Task<DownloadStationDownloader> Init(String? gid, String uri, String filePath, String downloadPath, String? category)
|
||||||
|
|
@ -149,13 +160,13 @@ public class DownloadStationDownloader : IDownloader
|
||||||
|
|
||||||
retryCount++;
|
retryCount++;
|
||||||
_logger.Error($"Task not found in DownloadStation after creat Sucess. Retrying {retryCount}/{RetryCount}");
|
_logger.Error($"Task not found in DownloadStation after creat Sucess. Retrying {retryCount}/{RetryCount}");
|
||||||
await Task.Delay(retryCount * 1000);
|
await _delayProvider.Delay(retryCount * 1000);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
retryCount++;
|
retryCount++;
|
||||||
_logger.Error($"Error starting download: {e.Message}. Retrying {retryCount}/{RetryCount}");
|
_logger.Error($"Error starting download: {e.Message}. Retrying {retryCount}/{RetryCount}");
|
||||||
await Task.Delay(retryCount * 1000);
|
await _delayProvider.Delay(retryCount * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ using Download = RdtClient.Data.Models.Data.Download;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services;
|
namespace RdtClient.Service.Services;
|
||||||
|
|
||||||
public class Downloads(DownloadData downloadData)
|
public class Downloads(DownloadData downloadData) : IDownloads
|
||||||
{
|
{
|
||||||
public async Task<List<Download>> GetForTorrent(Guid torrentId)
|
public async Task<List<Download>> GetForTorrent(Guid torrentId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
24
server/RdtClient.Service/Services/IDownloads.cs
Normal file
24
server/RdtClient.Service/Services/IDownloads.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Services;
|
||||||
|
|
||||||
|
public interface IDownloads
|
||||||
|
{
|
||||||
|
Task<List<Download>> GetForTorrent(Guid torrentId);
|
||||||
|
Task<Download?> GetById(Guid downloadId);
|
||||||
|
Task<Download?> Get(Guid torrentId, String path);
|
||||||
|
Task<Download> Add(Guid torrentId, String path);
|
||||||
|
Task UpdateUnrestrictedLink(Guid downloadId, String unrestrictedLink);
|
||||||
|
Task UpdateFileName(Guid downloadId, String fileName);
|
||||||
|
Task UpdateDownloadStarted(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
Task UpdateDownloadFinished(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
Task UpdateUnpackingQueued(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
Task UpdateUnpackingStarted(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
Task UpdateUnpackingFinished(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
Task UpdateCompleted(Guid downloadId, DateTimeOffset? dateTime);
|
||||||
|
Task UpdateError(Guid downloadId, String? error);
|
||||||
|
Task UpdateRetryCount(Guid downloadId, Int32 retryCount);
|
||||||
|
Task UpdateRemoteId(Guid downloadId, String remoteId);
|
||||||
|
Task DeleteForTorrent(Guid torrentId);
|
||||||
|
Task Reset(Guid downloadId);
|
||||||
|
}
|
||||||
|
|
@ -12,13 +12,14 @@ using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services.TorrentClients;
|
namespace RdtClient.Service.Services.TorrentClients;
|
||||||
|
|
||||||
public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHttpClientFactory httpClientFactory) : ITorrentClient
|
public interface IAllDebridNetClientFactory
|
||||||
{
|
{
|
||||||
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
public IAllDebridNETClient GetClient();
|
||||||
private static List<TorrentClientTorrent> _cache = [];
|
}
|
||||||
private static Int64 _sessionCounter = 0;
|
|
||||||
|
|
||||||
private AllDebridNETClient GetClient()
|
public class AllDebridNetClientFactory(ILogger<AllDebridNetClientFactory> logger, IHttpClientFactory httpClientFactory) : IAllDebridNetClientFactory
|
||||||
|
{
|
||||||
|
public IAllDebridNETClient GetClient()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -49,6 +50,13 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IAllDebridNetClientFactory allDebridNetClientFactory) : ITorrentClient
|
||||||
|
{
|
||||||
|
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
private static List<TorrentClientTorrent> _cache = [];
|
||||||
|
private static Int64 _sessionCounter = 0;
|
||||||
|
|
||||||
private static TorrentClientTorrent Map(Magnet torrent)
|
private static TorrentClientTorrent Map(Magnet torrent)
|
||||||
{
|
{
|
||||||
|
|
@ -77,7 +85,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
|
|
||||||
public async Task<IList<TorrentClientTorrent>> GetTorrents()
|
public async Task<IList<TorrentClientTorrent>> GetTorrents()
|
||||||
{
|
{
|
||||||
var results = await GetClient().Magnet.StatusLiveAsync(SessionId, _sessionCounter);
|
var results = await allDebridNetClientFactory.GetClient().Magnet.StatusLiveAsync(SessionId, _sessionCounter);
|
||||||
|
|
||||||
_sessionCounter = results.Counter;
|
_sessionCounter = results.Counter;
|
||||||
|
|
||||||
|
|
@ -106,7 +114,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
|
|
||||||
public async Task<TorrentClientUser> GetUser()
|
public async Task<TorrentClientUser> GetUser()
|
||||||
{
|
{
|
||||||
var user = await GetClient().User.GetAsync() ?? throw new("Unable to get user");
|
var user = await allDebridNetClientFactory.GetClient().User.GetAsync() ?? throw new("Unable to get user");
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
|
|
@ -117,7 +125,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
|
|
||||||
public async Task<String> AddMagnet(String magnetLink)
|
public async Task<String> AddMagnet(String magnetLink)
|
||||||
{
|
{
|
||||||
var result = await GetClient().Magnet.UploadMagnetAsync(magnetLink);
|
var result = await allDebridNetClientFactory.GetClient().Magnet.UploadMagnetAsync(magnetLink);
|
||||||
|
|
||||||
if (result?.Id == null)
|
if (result?.Id == null)
|
||||||
{
|
{
|
||||||
|
|
@ -131,7 +139,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
|
|
||||||
public async Task<String> AddFile(Byte[] bytes)
|
public async Task<String> AddFile(Byte[] bytes)
|
||||||
{
|
{
|
||||||
var result = await GetClient().Magnet.UploadFileAsync(bytes);
|
var result = await allDebridNetClientFactory.GetClient().Magnet.UploadFileAsync(bytes);
|
||||||
|
|
||||||
if (result?.Id == null)
|
if (result?.Id == null)
|
||||||
{
|
{
|
||||||
|
|
@ -155,12 +163,12 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
|
|
||||||
public async Task Delete(String torrentId)
|
public async Task Delete(String torrentId)
|
||||||
{
|
{
|
||||||
await GetClient().Magnet.DeleteAsync(torrentId);
|
await allDebridNetClientFactory.GetClient().Magnet.DeleteAsync(torrentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<String> Unrestrict(String link)
|
public async Task<String> Unrestrict(String link)
|
||||||
{
|
{
|
||||||
var result = await GetClient().Links.DownloadLinkAsync(link);
|
var result = await allDebridNetClientFactory.GetClient().Links.DownloadLinkAsync(link);
|
||||||
|
|
||||||
if (result.Link == null)
|
if (result.Link == null)
|
||||||
{
|
{
|
||||||
|
|
@ -245,7 +253,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var allFiles = await GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
|
var allFiles = await allDebridNetClientFactory.GetClient().Magnet.FilesAsync(Int64.Parse(torrent.RdId));
|
||||||
|
|
||||||
var files = GetFiles(allFiles);
|
var files = GetFiles(allFiles);
|
||||||
|
|
||||||
|
|
@ -323,7 +331,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
{
|
{
|
||||||
Log($"{file.Path} ({file.Bytes}b) {file.DownloadLink}");
|
Log($"{file.Path} ({file.Bytes}b) {file.DownloadLink}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return files.Where(m => m.DownloadLink != null).Select(m => m.DownloadLink!.ToString()).ToList();
|
return files.Where(m => m.DownloadLink != null).Select(m => m.DownloadLink!.ToString()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,7 +349,7 @@ public class AllDebridTorrentClient(ILogger<AllDebridTorrentClient> logger, IHtt
|
||||||
|
|
||||||
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
|
private async Task<TorrentClientTorrent> GetInfo(String torrentId)
|
||||||
{
|
{
|
||||||
var result = await GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
|
var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
|
||||||
|
|
||||||
return Map(result);
|
return Map(result);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,29 @@
|
||||||
using System.Diagnostics;
|
using System.Globalization;
|
||||||
using System.Globalization;
|
using System.IO.Abstractions;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MonoTorrent;
|
using MonoTorrent;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using RdtClient.Data.Data;
|
using RdtClient.Data.Data;
|
||||||
using RdtClient.Data.Enums;
|
using RdtClient.Data.Enums;
|
||||||
|
using RdtClient.Data.Models.Data;
|
||||||
using RdtClient.Data.Models.Internal;
|
using RdtClient.Data.Models.Internal;
|
||||||
using RdtClient.Data.Models.TorrentClient;
|
using RdtClient.Data.Models.TorrentClient;
|
||||||
using RdtClient.Service.BackgroundServices;
|
using RdtClient.Service.BackgroundServices;
|
||||||
using RdtClient.Service.Helpers;
|
using RdtClient.Service.Helpers;
|
||||||
using RdtClient.Service.Services.TorrentClients;
|
using RdtClient.Service.Services.TorrentClients;
|
||||||
|
using RdtClient.Service.Wrappers;
|
||||||
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
using Torrent = RdtClient.Data.Models.Data.Torrent;
|
||||||
|
|
||||||
namespace RdtClient.Service.Services;
|
namespace RdtClient.Service.Services;
|
||||||
|
|
||||||
public class Torrents(
|
public class Torrents(
|
||||||
ILogger<Torrents> logger,
|
ILogger<Torrents> logger,
|
||||||
TorrentData torrentData,
|
ITorrentData torrentData,
|
||||||
Downloads downloads,
|
IDownloads downloads,
|
||||||
|
IProcessFactory processFactory,
|
||||||
|
IFileSystem fileSystem,
|
||||||
AllDebridTorrentClient allDebridTorrentClient,
|
AllDebridTorrentClient allDebridTorrentClient,
|
||||||
PremiumizeTorrentClient premiumizeTorrentClient,
|
PremiumizeTorrentClient premiumizeTorrentClient,
|
||||||
RealDebridTorrentClient realDebridTorrentClient,
|
RealDebridTorrentClient realDebridTorrentClient,
|
||||||
|
|
@ -691,9 +695,11 @@ public class Torrents(
|
||||||
await torrentData.Update(torrent);
|
await torrentData.Update(torrent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RunTorrentComplete(Guid torrentId)
|
public async Task RunTorrentComplete(Guid torrentId, DbSettings? settings = null)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(Settings.Get.General.RunOnTorrentCompleteFileName))
|
settings ??= Settings.Get;
|
||||||
|
|
||||||
|
if (String.IsNullOrWhiteSpace(settings.General.RunOnTorrentCompleteFileName))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -702,8 +708,8 @@ public class Torrents(
|
||||||
|
|
||||||
var downloadsForTorrent = await downloads.GetForTorrent(torrentId);
|
var downloadsForTorrent = await downloads.GetForTorrent(torrentId);
|
||||||
|
|
||||||
var fileName = Settings.Get.General.RunOnTorrentCompleteFileName;
|
var fileName = settings.General.RunOnTorrentCompleteFileName;
|
||||||
var arguments = Settings.Get.General.RunOnTorrentCompleteArguments ?? "";
|
var arguments = settings.General.RunOnTorrentCompleteArguments ?? "";
|
||||||
|
|
||||||
Log($"Parsing external program {fileName} with arguments {arguments}", torrent);
|
Log($"Parsing external program {fileName} with arguments {arguments}", torrent);
|
||||||
|
|
||||||
|
|
@ -712,7 +718,7 @@ public class Torrents(
|
||||||
|
|
||||||
var filePath = torrentPath;
|
var filePath = torrentPath;
|
||||||
|
|
||||||
var files = Directory.GetFiles(filePath);
|
var files = fileSystem.Directory.GetFiles(filePath);
|
||||||
|
|
||||||
if (files.Length == 1)
|
if (files.Length == 1)
|
||||||
{
|
{
|
||||||
|
|
@ -733,7 +739,7 @@ public class Torrents(
|
||||||
var errorSb = new StringBuilder();
|
var errorSb = new StringBuilder();
|
||||||
var outputSb = new StringBuilder();
|
var outputSb = new StringBuilder();
|
||||||
|
|
||||||
using var process = new Process();
|
using var process = processFactory.NewProcess();
|
||||||
|
|
||||||
process.StartInfo.FileName = fileName;
|
process.StartInfo.FileName = fileName;
|
||||||
process.StartInfo.Arguments = arguments;
|
process.StartInfo.Arguments = arguments;
|
||||||
|
|
@ -744,21 +750,21 @@ public class Torrents(
|
||||||
|
|
||||||
process.OutputDataReceived += (_, data) =>
|
process.OutputDataReceived += (_, data) =>
|
||||||
{
|
{
|
||||||
if (data.Data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
outputSb.AppendLine(data.Data.Trim());
|
outputSb.AppendLine(data.Trim());
|
||||||
};
|
};
|
||||||
process.ErrorDataReceived += (_, data) =>
|
process.ErrorDataReceived += (_, data) =>
|
||||||
{
|
{
|
||||||
if (data.Data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorSb.AppendLine(data.Data.Trim());
|
errorSb.AppendLine(data.Trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
@ -807,7 +813,7 @@ public class Torrents(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Log(String message, Data.Models.Data.Download? download, Torrent? torrent)
|
private void Log(String message, Download? download, Torrent? torrent)
|
||||||
{
|
{
|
||||||
if (download != null)
|
if (download != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
16
server/RdtClient.Service/Wrappers/IProcess.cs
Normal file
16
server/RdtClient.Service/Wrappers/IProcess.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Wrappers;
|
||||||
|
|
||||||
|
public interface IProcess : IDisposable
|
||||||
|
{
|
||||||
|
event EventHandler<String?>? OutputDataReceived;
|
||||||
|
event EventHandler<String?>? ErrorDataReceived;
|
||||||
|
|
||||||
|
public ProcessStartInfo StartInfo { get; set; }
|
||||||
|
|
||||||
|
void BeginOutputReadLine();
|
||||||
|
void BeginErrorReadLine();
|
||||||
|
Boolean WaitForExit(Int32 milliseconds);
|
||||||
|
void Start();
|
||||||
|
}
|
||||||
6
server/RdtClient.Service/Wrappers/IProcessFactory.cs
Normal file
6
server/RdtClient.Service/Wrappers/IProcessFactory.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace RdtClient.Service.Wrappers;
|
||||||
|
|
||||||
|
public interface IProcessFactory
|
||||||
|
{
|
||||||
|
public IProcess NewProcess();
|
||||||
|
}
|
||||||
44
server/RdtClient.Service/Wrappers/Process.cs
Normal file
44
server/RdtClient.Service/Wrappers/Process.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace RdtClient.Service.Wrappers;
|
||||||
|
|
||||||
|
public class Process : IProcess
|
||||||
|
{
|
||||||
|
private readonly System.Diagnostics.Process _process = new();
|
||||||
|
|
||||||
|
public ProcessStartInfo StartInfo
|
||||||
|
{
|
||||||
|
get => _process.StartInfo;
|
||||||
|
set => _process.StartInfo = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler<String?>? OutputDataReceived;
|
||||||
|
public event EventHandler<String?>? ErrorDataReceived;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_process.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginOutputReadLine()
|
||||||
|
{
|
||||||
|
_process.OutputDataReceived += (sender, args) => OutputDataReceived?.Invoke(sender, args.Data);
|
||||||
|
_process.BeginOutputReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginErrorReadLine()
|
||||||
|
{
|
||||||
|
_process.ErrorDataReceived += (sender, args) => ErrorDataReceived?.Invoke(sender, args.Data);
|
||||||
|
_process.BeginErrorReadLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean WaitForExit(Int32 milliseconds)
|
||||||
|
{
|
||||||
|
return _process.WaitForExit(milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_process.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
server/RdtClient.Service/Wrappers/ProcessFactory.cs
Normal file
9
server/RdtClient.Service/Wrappers/ProcessFactory.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace RdtClient.Service.Wrappers;
|
||||||
|
|
||||||
|
public class ProcessFactory: IProcessFactory
|
||||||
|
{
|
||||||
|
public IProcess NewProcess()
|
||||||
|
{
|
||||||
|
return new Process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Service", "RdtCli
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Data", "RdtClient.Data\RdtClient.Data.csproj", "{92EF8817-AD73-4301-93BD-745D7D61DD74}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RdtClient.Data", "RdtClient.Data\RdtClient.Data.csproj", "{92EF8817-AD73-4301-93BD-745D7D61DD74}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RdtClient.Service.Test", "RdtClient.Service.Test\RdtClient.Service.Test.csproj", "{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -27,6 +29,10 @@ Global
|
||||||
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.Build.0 = Release|Any CPU
|
{92EF8817-AD73-4301-93BD-745D7D61DD74}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4F8F6313-5113-4E03-9DAB-195B0DF6C2B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue