diff --git a/src/Core/Net/UpdateChecker.cs b/src/Core/Net/UpdateChecker.cs index 80ce8c1..cb10930 100644 --- a/src/Core/Net/UpdateChecker.cs +++ b/src/Core/Net/UpdateChecker.cs @@ -28,11 +28,19 @@ public static class UpdateChecker public string? HtmlUrl { get; set; } } - public static async Task CheckAsync(string currentVersion, CancellationToken ct = default) + public static Task CheckAsync(string currentVersion, CancellationToken ct = default) => + CheckAsync(currentVersion, handler: null, ct); + + /// Test seam: replaces the real HTTP transport. + internal static async Task CheckAsync(string currentVersion, + HttpMessageHandler? handler, CancellationToken ct = default) { try { - using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + using var http = handler is null + ? new HttpClient() + : new HttpClient(handler, disposeHandler: false); + http.Timeout = TimeSpan.FromSeconds(10); http.DefaultRequestHeaders.UserAgent.ParseAdd("PalladiumWallet"); using var response = await http.GetAsync(ReleasesApiUrl, ct).ConfigureAwait(false); if (!response.IsSuccessStatusCode) return null; diff --git a/tests/PalladiumWallet.Tests/Net/UpdateCheckerTests.cs b/tests/PalladiumWallet.Tests/Net/UpdateCheckerTests.cs index 1f2d72f..02c8835 100644 --- a/tests/PalladiumWallet.Tests/Net/UpdateCheckerTests.cs +++ b/tests/PalladiumWallet.Tests/Net/UpdateCheckerTests.cs @@ -1,14 +1,86 @@ +using System.Net; +using System.Text; using PalladiumWallet.Core.Net; namespace PalladiumWallet.Tests.Net; /// -/// Tests for the release-tag parsing used by the update check. The network -/// call itself is best-effort by design (any failure → null) and is not -/// exercised here: only the version-comparison logic is deterministic. +/// Tests for the update check: tag parsing plus the full CheckAsync flow via +/// a stub HttpMessageHandler (the same seam the production overload wraps), +/// so no real GitHub call is ever made. /// public class UpdateCheckerTests { + /// Handler stub: replies with a fixed status/body, or throws. + private sealed class StubHandler(HttpStatusCode status, string? body = null, + Exception? throws = null) : HttpMessageHandler + { + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken ct) => + throws is not null + ? Task.FromException(throws) + : Task.FromResult(new HttpResponseMessage(status) + { + Content = new StringContent(body ?? "", Encoding.UTF8, "application/json"), + }); + } + + private static Task Check(string current, HttpStatusCode status, + string? body = null, Exception? throws = null) => + UpdateChecker.CheckAsync(current, new StubHandler(status, body, throws)); + + [Fact] + public async Task Una_release_piu_nuova_viene_segnalata_con_tag_e_url() + { + var release = await Check("0.9.1", HttpStatusCode.OK, + """{"tag_name":"v1.0.0","html_url":"https://example.test/rel/v1.0.0"}"""); + + Assert.NotNull(release); + Assert.Equal("v1.0.0", release!.Tag); + Assert.Equal("https://example.test/rel/v1.0.0", release.HtmlUrl); + } + + [Fact] + public async Task Senza_html_url_viene_costruito_il_link_alla_pagina_della_release() + { + var release = await Check("0.9.1", HttpStatusCode.OK, """{"tag_name":"v1.0.0"}"""); + + Assert.NotNull(release); + Assert.Contains("/releases/tag/v1.0.0", release!.HtmlUrl); + } + + [Theory] + [InlineData("""{"tag_name":"v0.9.1"}""")] // same version + [InlineData("""{"tag_name":"v0.9.0"}""")] // older + [InlineData("""{"tag_name":"main"}""")] // unparseable tag + [InlineData("""{"tag_name":""}""")] // empty tag + [InlineData("""{}""")] // missing tag + public async Task Nessun_aggiornamento_quando_il_tag_non_e_piu_nuovo(string body) + { + Assert.Null(await Check("0.9.1", HttpStatusCode.OK, body)); + } + + [Fact] + public async Task Una_risposta_http_di_errore_risolve_a_null() + { + Assert.Null(await Check("0.9.1", HttpStatusCode.NotFound)); + Assert.Null(await Check("0.9.1", HttpStatusCode.InternalServerError)); + } + + [Fact] + public async Task Json_malformato_o_errore_di_rete_risolvono_a_null() + { + Assert.Null(await Check("0.9.1", HttpStatusCode.OK, "not json at all")); + Assert.Null(await Check("0.9.1", HttpStatusCode.OK, + """{"tag_name":"v9.9.9"}""", throws: new HttpRequestException("offline"))); + } + + [Fact] + public async Task Una_versione_locale_non_parsabile_risolve_a_null() + { + Assert.Null(await Check("dev-build", HttpStatusCode.OK, """{"tag_name":"v9.9.9"}""")); + } + [Theory] [InlineData("v1.2.3", "1.2.3")] [InlineData("V0.9.1", "0.9.1")]