From 44704e36d468c5296350d472b3e54f439cda5401 Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:04:47 +0200 Subject: [PATCH 1/8] Refactor client and tests --- .../AuthentificationTests.cs | 1 + DelugeRPCClient.Net.Tests/ConfigTests.cs | 3 +- DelugeRPCClient.Net.Tests/Constants.cs | 6 +- .../DelugeClientMockTests.cs | 64 +++++++++++++++++++ DelugeRPCClient.Net.Tests/DelugeClientTest.cs | 15 +++-- .../DelugeRPCClient.Net.Tests.csproj | 10 +-- DelugeRPCClient.Net.Tests/LabelTests.cs | 7 +- .../MockHttpMessageHandler.cs | 24 +++++++ DelugeRPCClient.Net.Tests/TorrentsTests.cs | 13 +++- .../Core/CoreDelugeWebClient.cs | 42 ++++++++++-- DelugeRPCClient.Net/Core/DelugeResponse.cs | 2 +- DelugeRPCClient.Net/DelugeClient.cs | 7 +- 12 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs create mode 100644 DelugeRPCClient.Net.Tests/MockHttpMessageHandler.cs diff --git a/DelugeRPCClient.Net.Tests/AuthentificationTests.cs b/DelugeRPCClient.Net.Tests/AuthentificationTests.cs index 23d141a..f8be4de 100644 --- a/DelugeRPCClient.Net.Tests/AuthentificationTests.cs +++ b/DelugeRPCClient.Net.Tests/AuthentificationTests.cs @@ -9,6 +9,7 @@ public class AuthentificationTests : DelugeClientTest [TestMethod] public async Task LoginLogout() { + SkipIfNoIntegration(); DelugeClient client = await Login(); bool logoutResult = await client.Logout(); Assert.IsTrue(logoutResult); diff --git a/DelugeRPCClient.Net.Tests/ConfigTests.cs b/DelugeRPCClient.Net.Tests/ConfigTests.cs index 3b3ef30..3640d95 100644 --- a/DelugeRPCClient.Net.Tests/ConfigTests.cs +++ b/DelugeRPCClient.Net.Tests/ConfigTests.cs @@ -9,11 +9,12 @@ namespace DelugeRPCClient.Net.Tests { [TestClass] - public class ConfigTests + public class ConfigTests : DelugeClientTest { [TestMethod] public async Task ListConfigs() { + SkipIfNoIntegration(); DelugeClient client = new DelugeClient(url: Constants.DelugeUrl, password: Constants.DelugePassword); bool loginResult = await client.Login(); diff --git a/DelugeRPCClient.Net.Tests/Constants.cs b/DelugeRPCClient.Net.Tests/Constants.cs index a411622..ad63e4d 100644 --- a/DelugeRPCClient.Net.Tests/Constants.cs +++ b/DelugeRPCClient.Net.Tests/Constants.cs @@ -4,10 +4,10 @@ namespace DelugeRPCClient.Net.Tests { - internal class Constants + internal static class Constants { - internal const string DelugeUrl = "http://localhost:8112/json"; - internal const string DelugePassword = "deluge"; + internal static string DelugeUrl => Environment.GetEnvironmentVariable("DELUGE_URL") ?? "http://localhost:8112/json"; + internal static string DelugePassword => Environment.GetEnvironmentVariable("DELUGE_PASSWORD") ?? "deluge"; internal const string TestLabelName = "testlabel"; internal const string TorrentMagnet = "magnet:?xt=urn:btih:30987c19cf0eae3cf47766f387c621fa78a58ab9&dn=debian-9.2.1-amd64-netinst.iso"; internal const string TestTorrentFilename = "test.torrent"; diff --git a/DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs b/DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs new file mode 100644 index 0000000..948535d --- /dev/null +++ b/DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs @@ -0,0 +1,64 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace DelugeRPCClient.Net.Tests +{ + [TestClass] + public class DelugeClientMockTests + { + private HttpClient CreateClient() + { + var handler = new MockHttpMessageHandler(request => + { + var json = request.Content.ReadAsStringAsync().Result; + dynamic payload = JsonConvert.DeserializeObject(json); + int id = payload.id; + string method = payload.method; + object result = method switch + { + "auth.login" => true, + "auth.delete_session" => true, + "core.pause_torrent" => null, + "core.resume_torrent" => null, + _ => null + }; + var responseJson = JsonConvert.SerializeObject(new { id, result, error = (object)null }); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }; + }); + return new HttpClient(handler) + { + BaseAddress = new Uri("http://mock") + }; + } + + [TestMethod] + public async Task LoginLogoutWithMock() + { + using var httpClient = CreateClient(); + var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); + bool login = await client.Login(); + Assert.IsTrue(login); + bool logout = await client.Logout(); + Assert.IsTrue(logout); + } + + [TestMethod] + public async Task PauseResumeWithMock() + { + using var httpClient = CreateClient(); + var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); + bool pause = await client.PauseTorrent("hash"); + Assert.IsTrue(pause); + bool resume = await client.ResumeTorrent("hash"); + Assert.IsTrue(resume); + } + } +} diff --git a/DelugeRPCClient.Net.Tests/DelugeClientTest.cs b/DelugeRPCClient.Net.Tests/DelugeClientTest.cs index 1fdeb8a..4cb1f72 100644 --- a/DelugeRPCClient.Net.Tests/DelugeClientTest.cs +++ b/DelugeRPCClient.Net.Tests/DelugeClientTest.cs @@ -12,6 +12,13 @@ namespace DelugeRPCClient.Net.Tests { public class DelugeClientTest { + protected void SkipIfNoIntegration() + { + if (Environment.GetEnvironmentVariable("RUN_INTEGRATION_TESTS") != "true") + { + Assert.Inconclusive("Integration tests disabled"); + } + } protected async Task Login() { DelugeClientConfig config = new DelugeClientConfig() @@ -36,7 +43,7 @@ protected async Task AddTestTorrent(DelugeClient client) { Torrent torrent = await client.AddTorrentByFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Constants.TestTorrentFilename)); Assert.IsNotNull(torrent); - Thread.Sleep(1000); + await Task.Delay(1000); return torrent; } @@ -44,21 +51,21 @@ protected async Task RemoveTestTorrent(DelugeClient client, Torrent torrent) { bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); Assert.IsTrue(removeTorrentResult); - Thread.Sleep(1000); + await Task.Delay(1000); } protected async Task AddTestLabel(DelugeClient client) { bool addLabelResult = await client.AddLabel(Constants.TestLabelName); Assert.IsTrue(addLabelResult); - Thread.Sleep(1000); + await Task.Delay(1000); } protected async Task RemoveTestLabel(DelugeClient client) { bool removeLabelResult = await client.RemoveLabel(Constants.TestLabelName); Assert.IsTrue(removeLabelResult); - Thread.Sleep(1000); + await Task.Delay(1000); } } } diff --git a/DelugeRPCClient.Net.Tests/DelugeRPCClient.Net.Tests.csproj b/DelugeRPCClient.Net.Tests/DelugeRPCClient.Net.Tests.csproj index 029f6df..e2ee612 100644 --- a/DelugeRPCClient.Net.Tests/DelugeRPCClient.Net.Tests.csproj +++ b/DelugeRPCClient.Net.Tests/DelugeRPCClient.Net.Tests.csproj @@ -1,16 +1,16 @@ - netcoreapp3.1 + net8.0 false - - - - + + + + diff --git a/DelugeRPCClient.Net.Tests/LabelTests.cs b/DelugeRPCClient.Net.Tests/LabelTests.cs index 3fff10d..33f5413 100644 --- a/DelugeRPCClient.Net.Tests/LabelTests.cs +++ b/DelugeRPCClient.Net.Tests/LabelTests.cs @@ -15,6 +15,7 @@ public class LabelTests : DelugeClientTest [TestMethod] public async Task ListLabels() { + SkipIfNoIntegration(); DelugeClient client = await Login(); await AddTestLabel(client); @@ -31,6 +32,7 @@ public async Task ListLabels() [TestMethod] public async Task AddAndRemoveLabel() { + SkipIfNoIntegration(); DelugeClient client = await Login(); await AddTestLabel(client); @@ -43,6 +45,7 @@ public async Task AddAndRemoveLabel() [TestMethod] public async Task AssignLabel() { + SkipIfNoIntegration(); DelugeClient client = await Login(); await AddTestLabel(client); @@ -51,11 +54,11 @@ public async Task AssignLabel() bool assignResult = await client.SetTorrentLabel(testTorrent.Hash, Constants.TestLabelName); Assert.IsTrue(assignResult); - Thread.Sleep(1000); + await Task.Delay(1000); bool unsertLabelResult = await client.SetTorrentLabel(testTorrent.Hash, null); Assert.IsTrue(unsertLabelResult); - Thread.Sleep(1000); + await Task.Delay(1000); await RemoveTestLabel(client); diff --git a/DelugeRPCClient.Net.Tests/MockHttpMessageHandler.cs b/DelugeRPCClient.Net.Tests/MockHttpMessageHandler.cs new file mode 100644 index 0000000..56bc9ad --- /dev/null +++ b/DelugeRPCClient.Net.Tests/MockHttpMessageHandler.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace DelugeRPCClient.Net.Tests +{ + internal class MockHttpMessageHandler : HttpMessageHandler + { + private readonly Func _handler; + + public MockHttpMessageHandler(Func handler) + { + _handler = handler; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(_handler(request)); + } + } +} diff --git a/DelugeRPCClient.Net.Tests/TorrentsTests.cs b/DelugeRPCClient.Net.Tests/TorrentsTests.cs index 83673df..70276aa 100644 --- a/DelugeRPCClient.Net.Tests/TorrentsTests.cs +++ b/DelugeRPCClient.Net.Tests/TorrentsTests.cs @@ -17,6 +17,7 @@ public class TorrentsTests : DelugeClientTest [TestMethod] public async Task ListAndGetTorrent() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent testTorrent = await AddTestTorrent(client); @@ -36,6 +37,7 @@ public async Task ListAndGetTorrent() [TestMethod] public async Task ListAndGetTorrentExtended() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent testTorrent = await AddTestTorrent(client); @@ -55,12 +57,13 @@ public async Task ListAndGetTorrentExtended() [TestMethod] public async Task AddRemoveTorrentByMagnet() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent torrent = await client.AddTorrentByMagnet(Constants.TorrentMagnet); Assert.IsNotNull(torrent); - Thread.Sleep(1000); + await Task.Delay(1000); bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); Assert.IsTrue(removeTorrentResult); @@ -71,12 +74,13 @@ public async Task AddRemoveTorrentByMagnet() [TestMethod] public async Task AddRemoveTorrentByFile() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent torrent = await client.AddTorrentByFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Constants.TestTorrentFilename)); Assert.IsNotNull(torrent); - Thread.Sleep(1000); + await Task.Delay(1000); bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); Assert.IsTrue(removeTorrentResult); @@ -87,12 +91,13 @@ public async Task AddRemoveTorrentByFile() [TestMethod] public async Task AddRemoveTorrentByUrl() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent torrent = await client.AddTorrentByUrl(Constants.TestTorrentUrl); Assert.IsNotNull(torrent); - Thread.Sleep(1000); + await Task.Delay(1000); bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); Assert.IsTrue(removeTorrentResult); @@ -103,6 +108,7 @@ public async Task AddRemoveTorrentByUrl() [TestMethod] public async Task PauseResumeTorrent() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent testTorrent = await AddTestTorrent(client); @@ -136,6 +142,7 @@ public async Task PauseResumeTorrent() [TestMethod] public async Task RecheckTorrents() { + SkipIfNoIntegration(); DelugeClient client = await Login(); Torrent testTorrent = await AddTestTorrent(client); diff --git a/DelugeRPCClient.Net/Core/CoreDelugeWebClient.cs b/DelugeRPCClient.Net/Core/CoreDelugeWebClient.cs index 863abf9..7b15546 100644 --- a/DelugeRPCClient.Net/Core/CoreDelugeWebClient.cs +++ b/DelugeRPCClient.Net/Core/CoreDelugeWebClient.cs @@ -11,15 +11,16 @@ namespace DelugeRPCClient.Net.Core { - public class CoreDelugeWebClient + public class CoreDelugeWebClient : IDisposable { private string Url { get; set; } private HttpClientHandler HttpClientHandler { get; set; } + private readonly bool _ownsClient; private HttpClient HttpClient { get; set; } private int RequestId { get; set; } private DelugeClientConfig DelugeClientConfig { get; set; } - public CoreDelugeWebClient(string url, DelugeClientConfig config = null) + public CoreDelugeWebClient(string url, DelugeClientConfig config = null, HttpClient httpClient = null) { DelugeClientConfig = config ?? new DelugeClientConfig(); @@ -36,7 +37,16 @@ public CoreDelugeWebClient(string url, DelugeClientConfig config = null) HttpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }; } - HttpClient = new HttpClient(HttpClientHandler, true); + if (httpClient == null) + { + HttpClient = new HttpClient(HttpClientHandler, true); + _ownsClient = true; + } + else + { + HttpClient = httpClient; + _ownsClient = false; + } HttpClient.Timeout = DelugeClientConfig.Timeout; RequestId = 1; @@ -59,7 +69,7 @@ protected async Task SendRequest(DelugeRequest webRequest) var responseJson = await PostJson(requestJson); - var webResponse = JsonConvert.DeserializeObject>(responseJson); + var webResponse = JsonConvert.DeserializeObject>(responseJson); if (webResponse.Error != null) throw new DelugeClientException(webResponse.Error.Message); if (webResponse.ResponseId != webRequest.RequestId) throw new DelugeClientException("Desync."); @@ -79,6 +89,29 @@ private async Task PostJson(String json) return responseJson; } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_ownsClient) + { + HttpClient?.Dispose(); + HttpClientHandler?.Dispose(); + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~CoreDelugeWebClient() + { + Dispose(false); + } + protected DelugeRequest CreateRequest(string method, params object[] parameters) { if (String.IsNullOrWhiteSpace(method)) throw new ArgumentException(nameof(method)); @@ -86,3 +119,4 @@ protected DelugeRequest CreateRequest(string method, params object[] parameters) } } } + diff --git a/DelugeRPCClient.Net/Core/DelugeResponse.cs b/DelugeRPCClient.Net/Core/DelugeResponse.cs index 5282b96..194ada6 100644 --- a/DelugeRPCClient.Net/Core/DelugeResponse.cs +++ b/DelugeRPCClient.Net/Core/DelugeResponse.cs @@ -5,7 +5,7 @@ namespace DelugeRPCClient.Net.Core { - internal class DelugeResponsee + internal class DelugeResponse { [JsonProperty(PropertyName = "id")] public int ResponseId { get; set; } diff --git a/DelugeRPCClient.Net/DelugeClient.cs b/DelugeRPCClient.Net/DelugeClient.cs index 30dd21e..4dbe483 100644 --- a/DelugeRPCClient.Net/DelugeClient.cs +++ b/DelugeRPCClient.Net/DelugeClient.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -25,7 +26,7 @@ public class DelugeClient : Core.CoreDelugeWebClient /// /// url of delugeweb (like http://localhost:8112/json) /// delugeweb password - public DelugeClient(string url, string password,DelugeClientConfig config = null) : base(url, config) + public DelugeClient(string url, string password, DelugeClientConfig config = null, HttpClient httpClient = null) : base(url, config, httpClient) { Password = password; } @@ -183,7 +184,7 @@ public async Task RemoveTorrent(string hash, bool removeData = false) public async Task PauseTorrent(string hash) { bool? result = await SendRequest("core.pause_torrent", hash); - Thread.Sleep(3000); + await Task.Delay(3000); return result == null; } @@ -195,7 +196,7 @@ public async Task PauseTorrent(string hash) public async Task ResumeTorrent(string hash) { bool? result = await SendRequest("core.resume_torrent", hash); - Thread.Sleep(3000); + await Task.Delay(3000); return result == null; } From cb17703ee31e49f442f6fadf8b1e26e33ad195a2 Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:10:13 +0200 Subject: [PATCH 2/8] Update .NET version to 8.0.16 in GitHub Actions workflow --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2f690dd..f93e690 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 6.0.102 + dotnet-version: 8.0.16 - name: Restore dependencies run: dotnet restore - name: Build From 2adae387edfcdb6968db0b2fe13d797612e179ea Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:27:03 +0200 Subject: [PATCH 3/8] Refactor tests with mocks and update workflow --- .github/workflows/dotnet.yml | 2 + .../AuthentificationTests.cs | 18 -- DelugeRPCClient.Net.Tests/ConfigTests.cs | 30 ---- .../DelugeClientCoverageTests.cs | 113 ++++++++++++ .../DelugeClientMockTests.cs | 64 ------- DelugeRPCClient.Net.Tests/DelugeClientTest.cs | 71 -------- DelugeRPCClient.Net.Tests/LabelTests.cs | 70 -------- DelugeRPCClient.Net.Tests/TestUtils.cs | 58 +++++++ DelugeRPCClient.Net.Tests/TorrentsTests.cs | 164 ------------------ 9 files changed, 173 insertions(+), 417 deletions(-) delete mode 100644 DelugeRPCClient.Net.Tests/AuthentificationTests.cs delete mode 100644 DelugeRPCClient.Net.Tests/ConfigTests.cs create mode 100644 DelugeRPCClient.Net.Tests/DelugeClientCoverageTests.cs delete mode 100644 DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs delete mode 100644 DelugeRPCClient.Net.Tests/DelugeClientTest.cs delete mode 100644 DelugeRPCClient.Net.Tests/LabelTests.cs create mode 100644 DelugeRPCClient.Net.Tests/TestUtils.cs delete mode 100644 DelugeRPCClient.Net.Tests/TorrentsTests.cs diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f93e690..b4d3125 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,6 +16,8 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: + - name: Test + run: dotnet test --no-build --verbosity normal dotnet-version: 8.0.16 - name: Restore dependencies run: dotnet restore diff --git a/DelugeRPCClient.Net.Tests/AuthentificationTests.cs b/DelugeRPCClient.Net.Tests/AuthentificationTests.cs deleted file mode 100644 index f8be4de..0000000 --- a/DelugeRPCClient.Net.Tests/AuthentificationTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Threading.Tasks; - -namespace DelugeRPCClient.Net.Tests -{ - [TestClass] - public class AuthentificationTests : DelugeClientTest - { - [TestMethod] - public async Task LoginLogout() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - bool logoutResult = await client.Logout(); - Assert.IsTrue(logoutResult); - } - } -} diff --git a/DelugeRPCClient.Net.Tests/ConfigTests.cs b/DelugeRPCClient.Net.Tests/ConfigTests.cs deleted file mode 100644 index 3640d95..0000000 --- a/DelugeRPCClient.Net.Tests/ConfigTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using DelugeRPCClient.Net.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace DelugeRPCClient.Net.Tests -{ - [TestClass] - public class ConfigTests : DelugeClientTest - { - [TestMethod] - public async Task ListConfigs() - { - SkipIfNoIntegration(); - DelugeClient client = new DelugeClient(url: Constants.DelugeUrl, password: Constants.DelugePassword); - - bool loginResult = await client.Login(); - Assert.IsTrue(loginResult); - - Config configs = await client.ListConfigs(); - Assert.IsNotNull(configs); - - bool logoutResult = await client.Logout(); - Assert.IsTrue(logoutResult); - } - } -} diff --git a/DelugeRPCClient.Net.Tests/DelugeClientCoverageTests.cs b/DelugeRPCClient.Net.Tests/DelugeClientCoverageTests.cs new file mode 100644 index 0000000..37bd357 --- /dev/null +++ b/DelugeRPCClient.Net.Tests/DelugeClientCoverageTests.cs @@ -0,0 +1,113 @@ +using DelugeRPCClient.Net.Models; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace DelugeRPCClient.Net.Tests +{ + [TestClass] + public class DelugeClientCoverageTests + { + [TestMethod] + public async Task FullClientWorkflow() + { + using var httpClient = TestUtils.CreateMockClient(TestUtils.DefaultResponses); + var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); + Assert.IsTrue(await client.Login()); + Assert.IsNotNull(await client.ListTorrents()); + Assert.IsNotNull(await client.ListTorrentsExtended()); + Assert.IsNotNull(await client.GetTorrent("hash")); + Assert.IsNotNull(await client.GetTorrentExtended("hash")); + string torrentPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Constants.TestTorrentFilename); + Assert.IsNotNull(await client.AddTorrentByMagnet("magnet")); + Assert.IsNotNull(await client.AddTorrentByFile(torrentPath)); + Assert.IsNotNull(await client.AddTorrentByUrl("http://example.com/file.torrent")); + Assert.IsTrue(await client.RemoveTorrent("hash")); + Assert.IsTrue(await client.PauseTorrent("hash")); + Assert.IsTrue(await client.ResumeTorrent("hash")); + Assert.IsNull(await client.RecheckTorrents(new List { "hash" })); + Assert.IsNotNull(await client.ListConfigs()); + Assert.IsNotNull(await client.ListLabels()); + Assert.IsTrue(await client.LabelExists("lbl")); + Assert.IsTrue(await client.AddLabel("lbl")); + Assert.IsTrue(await client.RemoveLabel("lbl")); + Assert.IsTrue(await client.SetTorrentLabel("hash", "lbl")); + Assert.IsTrue(await client.Logout()); + } + + [TestMethod] + public async Task PauseResumeReturnsFalseOnBool() + { + var dict = new Dictionary(TestUtils.DefaultResponses) + { + ["core.pause_torrent"] = false, + ["core.resume_torrent"] = false + }; + using var httpClient = TestUtils.CreateMockClient(dict); + var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); + Assert.IsFalse(await client.PauseTorrent("hash")); + Assert.IsFalse(await client.ResumeTorrent("hash")); + } + + [TestMethod] + public void AddLabelThrowsOnInvalid() + { + using var httpClient = TestUtils.CreateMockClient(TestUtils.DefaultResponses); + var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); + Assert.ThrowsExceptionAsync(() => client.AddLabel(null)); + } + + [TestMethod] + public async Task ErrorAndDesyncThrow() + { + using var errClient = TestUtils.CreateMockClient(TestUtils.DefaultResponses, error: true); + var client = new DelugeClient("http://mock", "pwd", httpClient: errClient); + try + { + await client.ListLabels(); + Assert.Fail("no exception"); + } + catch (Exception) + { + } + + using var desyncClient = TestUtils.CreateMockClient(TestUtils.DefaultResponses, mismatchId: true); + var client2 = new DelugeClient("http://mock", "pwd", httpClient: desyncClient); + try + { + await client2.ListLabels(); + Assert.Fail("no exception"); + } + catch (Exception) + { + } + } + + [TestMethod] + public void ModelPropertyCoverage() + { + var types = new[] { typeof(Config), typeof(Torrent), typeof(TorrentExtended), typeof(TorrentOptions) }; + foreach (var t in types) + { + var instance = Activator.CreateInstance(t); + foreach (var prop in t.GetProperties()) + { + if (prop.CanWrite) + { + object value = prop.PropertyType.IsValueType ? Activator.CreateInstance(prop.PropertyType) : null; + prop.SetValue(instance, value); + } + if (prop.CanRead) + { + _ = prop.GetValue(instance); + } + } + } + Assert.IsTrue(typeof(Torrent).GetProperties().Length > 0); + } + } +} diff --git a/DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs b/DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs deleted file mode 100644 index 948535d..0000000 --- a/DelugeRPCClient.Net.Tests/DelugeClientMockTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; -using System; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace DelugeRPCClient.Net.Tests -{ - [TestClass] - public class DelugeClientMockTests - { - private HttpClient CreateClient() - { - var handler = new MockHttpMessageHandler(request => - { - var json = request.Content.ReadAsStringAsync().Result; - dynamic payload = JsonConvert.DeserializeObject(json); - int id = payload.id; - string method = payload.method; - object result = method switch - { - "auth.login" => true, - "auth.delete_session" => true, - "core.pause_torrent" => null, - "core.resume_torrent" => null, - _ => null - }; - var responseJson = JsonConvert.SerializeObject(new { id, result, error = (object)null }); - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(responseJson, Encoding.UTF8, "application/json") - }; - }); - return new HttpClient(handler) - { - BaseAddress = new Uri("http://mock") - }; - } - - [TestMethod] - public async Task LoginLogoutWithMock() - { - using var httpClient = CreateClient(); - var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); - bool login = await client.Login(); - Assert.IsTrue(login); - bool logout = await client.Logout(); - Assert.IsTrue(logout); - } - - [TestMethod] - public async Task PauseResumeWithMock() - { - using var httpClient = CreateClient(); - var client = new DelugeClient("http://mock", "pwd", httpClient: httpClient); - bool pause = await client.PauseTorrent("hash"); - Assert.IsTrue(pause); - bool resume = await client.ResumeTorrent("hash"); - Assert.IsTrue(resume); - } - } -} diff --git a/DelugeRPCClient.Net.Tests/DelugeClientTest.cs b/DelugeRPCClient.Net.Tests/DelugeClientTest.cs deleted file mode 100644 index 4cb1f72..0000000 --- a/DelugeRPCClient.Net.Tests/DelugeClientTest.cs +++ /dev/null @@ -1,71 +0,0 @@ -using DelugeRPCClient.Net.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace DelugeRPCClient.Net.Tests -{ - public class DelugeClientTest - { - protected void SkipIfNoIntegration() - { - if (Environment.GetEnvironmentVariable("RUN_INTEGRATION_TESTS") != "true") - { - Assert.Inconclusive("Integration tests disabled"); - } - } - protected async Task Login() - { - DelugeClientConfig config = new DelugeClientConfig() - { - IgnoreSslErrors = true, - Timeout = new TimeSpan(0, 0, 30) - }; - DelugeClient client = new DelugeClient(url: Constants.DelugeUrl, password: Constants.DelugePassword, config); - bool loginResult = await client.Login(); - Assert.IsTrue(loginResult); - - return client; - } - - protected async Task Logout(DelugeClient client) - { - bool logoutResult = await client.Logout(); - Assert.IsTrue(logoutResult); - } - - protected async Task AddTestTorrent(DelugeClient client) - { - Torrent torrent = await client.AddTorrentByFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Constants.TestTorrentFilename)); - Assert.IsNotNull(torrent); - await Task.Delay(1000); - return torrent; - } - - protected async Task RemoveTestTorrent(DelugeClient client, Torrent torrent) - { - bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); - Assert.IsTrue(removeTorrentResult); - await Task.Delay(1000); - } - - protected async Task AddTestLabel(DelugeClient client) - { - bool addLabelResult = await client.AddLabel(Constants.TestLabelName); - Assert.IsTrue(addLabelResult); - await Task.Delay(1000); - } - - protected async Task RemoveTestLabel(DelugeClient client) - { - bool removeLabelResult = await client.RemoveLabel(Constants.TestLabelName); - Assert.IsTrue(removeLabelResult); - await Task.Delay(1000); - } - } -} diff --git a/DelugeRPCClient.Net.Tests/LabelTests.cs b/DelugeRPCClient.Net.Tests/LabelTests.cs deleted file mode 100644 index 33f5413..0000000 --- a/DelugeRPCClient.Net.Tests/LabelTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using DelugeRPCClient.Net.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace DelugeRPCClient.Net.Tests -{ - [TestClass] - public class LabelTests : DelugeClientTest - { - [TestMethod] - public async Task ListLabels() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - await AddTestLabel(client); - - List labels = await client.ListLabels(); - Assert.IsNotNull(labels); - Assert.AreNotEqual(0, labels.Count); - - await RemoveTestLabel(client); - - await Logout(client); - } - - [TestMethod] - public async Task AddAndRemoveLabel() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - await AddTestLabel(client); - - await RemoveTestLabel(client); - - await Logout(client); - } - - [TestMethod] - public async Task AssignLabel() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - await AddTestLabel(client); - - Torrent testTorrent = await AddTestTorrent(client); - - bool assignResult = await client.SetTorrentLabel(testTorrent.Hash, Constants.TestLabelName); - Assert.IsTrue(assignResult); - await Task.Delay(1000); - - bool unsertLabelResult = await client.SetTorrentLabel(testTorrent.Hash, null); - Assert.IsTrue(unsertLabelResult); - await Task.Delay(1000); - - await RemoveTestLabel(client); - - await RemoveTestTorrent(client, testTorrent); - - await Logout(client); - } - } -} diff --git a/DelugeRPCClient.Net.Tests/TestUtils.cs b/DelugeRPCClient.Net.Tests/TestUtils.cs new file mode 100644 index 0000000..132ce90 --- /dev/null +++ b/DelugeRPCClient.Net.Tests/TestUtils.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; + +namespace DelugeRPCClient.Net.Tests +{ + internal static class TestUtils + { + public static HttpClient CreateMockClient(Dictionary responses, bool mismatchId = false, bool error = false) + { + var handler = new MockHttpMessageHandler(request => + { + var json = request.Content.ReadAsStringAsync().Result; + dynamic payload = JsonConvert.DeserializeObject(json); + int id = payload.id; + string method = payload.method; + responses.TryGetValue(method, out object result); + var responseObj = new + { + id = mismatchId ? id + 1 : id, + result = error ? null : result, + error = error ? new { message = "err", code = 1 } : null + }; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(responseObj), Encoding.UTF8, "application/json") + }; + }); + return new HttpClient(handler) { BaseAddress = new Uri("http://mock") }; + } + + public static Dictionary DefaultResponses => new() + { + ["auth.login"] = true, + ["auth.delete_session"] = true, + ["core.get_torrents_status"] = new Dictionary + { + ["hash"] = new DelugeRPCClient.Net.Models.Torrent { Hash = "hash", Name = "t", Paused = false, Ratio = 0, Message = "m", Label = "lbl" } + }, + ["core.add_torrent_magnet"] = "hash", + ["core.add_torrent_file"] = "hash", + ["web.download_torrent_from_url"] = "temp.torrent", + ["web.add_torrents"] = new List> { new() { true, "hash" } }, + ["core.remove_torrent"] = true, + ["core.pause_torrent"] = null, + ["core.resume_torrent"] = null, + ["core.force_recheck"] = null, + ["core.get_config"] = new DelugeRPCClient.Net.Models.Config(), + ["label.get_labels"] = new List { "lbl" }, + ["label.add"] = null, + ["label.remove"] = null, + ["label.set_torrent"] = null + }; + } +} diff --git a/DelugeRPCClient.Net.Tests/TorrentsTests.cs b/DelugeRPCClient.Net.Tests/TorrentsTests.cs deleted file mode 100644 index 70276aa..0000000 --- a/DelugeRPCClient.Net.Tests/TorrentsTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -using DelugeRPCClient.Net.Models; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace DelugeRPCClient.Net.Tests -{ - [TestClass] - public class TorrentsTests : DelugeClientTest - { - [TestMethod] - public async Task ListAndGetTorrent() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent testTorrent = await AddTestTorrent(client); - - List torrents = await client.ListTorrents(); - Assert.IsNotNull(torrents); - Assert.AreNotEqual(0, torrents.Count); - - Torrent torrent = await client.GetTorrent(torrents[0].Hash); - Assert.IsNotNull(torrent); - - await RemoveTestTorrent(client, testTorrent); - - await Logout(client); - } - - [TestMethod] - public async Task ListAndGetTorrentExtended() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent testTorrent = await AddTestTorrent(client); - - List torrents = await client.ListTorrentsExtended(); - Assert.IsNotNull(torrents); - Assert.AreNotEqual(0, torrents.Count); - - TorrentExtended torrent = await client.GetTorrentExtended(torrents[0].Hash); - Assert.IsNotNull(torrent); - - await RemoveTestTorrent(client, testTorrent); - - await Logout(client); - } - - [TestMethod] - public async Task AddRemoveTorrentByMagnet() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent torrent = await client.AddTorrentByMagnet(Constants.TorrentMagnet); - Assert.IsNotNull(torrent); - - await Task.Delay(1000); - - bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); - Assert.IsTrue(removeTorrentResult); - - await Logout(client); - } - - [TestMethod] - public async Task AddRemoveTorrentByFile() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent torrent = await client.AddTorrentByFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Constants.TestTorrentFilename)); - Assert.IsNotNull(torrent); - - await Task.Delay(1000); - - bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); - Assert.IsTrue(removeTorrentResult); - - await Logout(client); - } - - [TestMethod] - public async Task AddRemoveTorrentByUrl() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent torrent = await client.AddTorrentByUrl(Constants.TestTorrentUrl); - Assert.IsNotNull(torrent); - - await Task.Delay(1000); - - bool removeTorrentResult = await client.RemoveTorrent(torrent.Hash); - Assert.IsTrue(removeTorrentResult); - - await Logout(client); - } - - [TestMethod] - public async Task PauseResumeTorrent() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent testTorrent = await AddTestTorrent(client); - - List torrents = await client.ListTorrents(); - Assert.IsNotNull(torrents); - Assert.AreNotEqual(0, torrents.Count); - - Torrent torrent = torrents[0]; - - if(torrent.Paused) - { - bool resumeResult = await client.ResumeTorrent(torrent.Hash); - Assert.IsTrue(resumeResult); - bool pauseResult = await client.PauseTorrent(torrent.Hash); - Assert.IsTrue(pauseResult); - } - else - { - bool pauseResult = await client.PauseTorrent(torrent.Hash); - Assert.IsTrue(pauseResult); - bool resumeResult = await client.ResumeTorrent(torrent.Hash); - Assert.IsTrue(resumeResult); - } - - await RemoveTestTorrent(client, testTorrent); - - await Logout(client); - } - - [TestMethod] - public async Task RecheckTorrents() - { - SkipIfNoIntegration(); - DelugeClient client = await Login(); - - Torrent testTorrent = await AddTestTorrent(client); - - List torrents = await client.ListTorrents(); - Assert.IsNotNull(torrents); - Assert.AreNotEqual(0, torrents.Count); - - Torrent torrent = torrents[0]; - - bool? recheckResult = await client.RecheckTorrents(torrent.Hash.Split(",").ToList()); - Assert.IsNull(recheckResult); - - await RemoveTestTorrent(client, testTorrent); - - await Logout(client); - } - } -} From 0e245a0a0c93fd9e922250eeb5bd77d5e8144ebd Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:28:47 +0200 Subject: [PATCH 4/8] Revert .NET version to 6.0.102 in GitHub Actions workflow --- .github/workflows/dotnet.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index f93e690..8e5e91c 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,11 +16,13 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.16 + dotnet-version: 6.0.102 - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal - name: Publish DelugeRPCClient.Net uses: brandedoutcast/publish-nuget@v2.5.5 with: From a82dc9e8bfe4a8d4461acb199c54d5300c44c3e8 Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:31:40 +0200 Subject: [PATCH 5/8] Update .NET setup action to version 4.3.1 in workflow --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 8e5e91c..09330f1 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4.3.1 with: dotnet-version: 6.0.102 - name: Restore dependencies From 529ad8a5fb530231b91edab7d6274c927a0e8670 Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:31:54 +0200 Subject: [PATCH 6/8] Update .NET version to 8.0.16 in workflow --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 09330f1..9879ca3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4.3.1 with: - dotnet-version: 6.0.102 + dotnet-version: 8.0.16 - name: Restore dependencies run: dotnet restore - name: Build From b34aa8d3b26e0eae56bb9d11e75f55c64d47ef82 Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:34:11 +0200 Subject: [PATCH 7/8] Update .NET version to 8.0.410 in workflow --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9879ca3..25e7236 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4.3.1 with: - dotnet-version: 8.0.16 + dotnet-version: 8.0.410 - name: Restore dependencies run: dotnet restore - name: Build From f3a90756a4199e948362847e82ea3fb3c0f9a320 Mon Sep 17 00:00:00 2001 From: Daniel Silvestre Date: Mon, 9 Jun 2025 23:37:07 +0200 Subject: [PATCH 8/8] Remove publishing step from .NET build workflow and add release workflow for NuGet package publishing --- .github/workflows/dotnet.yml | 5 ----- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 25e7236..f729bb8 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -23,8 +23,3 @@ jobs: run: dotnet build --no-restore - name: Test run: dotnet test --no-build --verbosity normal - - name: Publish DelugeRPCClient.Net - uses: brandedoutcast/publish-nuget@v2.5.5 - with: - PROJECT_FILE_PATH: DelugeRPCClient.Net/DelugeRPCClient.Net.csproj - NUGET_KEY: ${{secrets.nugetsAPIKEY}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..111132e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release .NET + +on: + push: + tags: + - '*' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4.3.1 + with: + dotnet-version: 8.0.410 + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal + - name: Publish DelugeRPCClient.Net + uses: brandedoutcast/publish-nuget@v2.5.5 + with: + PROJECT_FILE_PATH: DelugeRPCClient.Net/DelugeRPCClient.Net.csproj + NUGET_KEY: ${{secrets.nugetsAPIKEY}} \ No newline at end of file