Files
PalladiumWallet/src/Core/Net/UpdateChecker.cs
T
davide 970253cbbe test(net): exercise UpdateChecker.CheckAsync end-to-end via a stub transport
CheckAsync built its own HttpClient inline, so nothing beyond tag
parsing was testable without hitting the real GitHub API. Added an
internal CheckAsync(currentVersion, HttpMessageHandler, ct) overload
(the public method now delegates to it) as a seam for a stub handler,
and tests for the full best-effort matrix: newer/equal/older/unparseable
tag, missing html_url, HTTP error status, malformed JSON, a thrown
HttpRequestException, and an unparseable local version — all of which
must resolve to null or the correct LatestRelease, never throw.
2026-07-07 16:26:06 +02:00

75 lines
2.8 KiB
C#

using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace PalladiumWallet.Core.Net;
/// <summary>A GitHub release newer than the running app.</summary>
public sealed record LatestRelease(string Tag, string HtmlUrl);
/// <summary>
/// Checks the latest GitHub release for the project against the running app version.
/// Best-effort only: any network/parse failure or an up-to-date app both resolve to null,
/// so callers never need to distinguish "no update" from "couldn't check".
/// </summary>
public static class UpdateChecker
{
private const string ReleasesApiUrl = "https://api.github.com/repos/davide3011/PalladiumWallet/releases/latest";
private sealed class GitHubReleaseResponse
{
[JsonPropertyName("tag_name")]
public string? TagName { get; set; }
[JsonPropertyName("html_url")]
public string? HtmlUrl { get; set; }
}
public static Task<LatestRelease?> CheckAsync(string currentVersion, CancellationToken ct = default) =>
CheckAsync(currentVersion, handler: null, ct);
/// <summary>Test seam: <paramref name="handler"/> replaces the real HTTP transport.</summary>
internal static async Task<LatestRelease?> CheckAsync(string currentVersion,
HttpMessageHandler? handler, CancellationToken ct = default)
{
try
{
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;
var release = await response.Content
.ReadFromJsonAsync<GitHubReleaseResponse>(ct)
.ConfigureAwait(false);
if (release?.TagName is not { Length: > 0 } tag) return null;
if (!TryParse(tag, out var remote) || !TryParse(currentVersion, out var local))
return null;
if (remote <= local) return null;
return new LatestRelease(tag, release.HtmlUrl ?? $"https://github.com/davide3011/PalladiumWallet/releases/tag/{tag}");
}
catch
{
return null;
}
}
/// <summary>Parses "v1.2.3" / "1.2.3-beta" style tags into a comparable <see cref="Version"/>.</summary>
internal static bool TryParse(string raw, out Version version)
{
var s = raw.Trim();
if (s.StartsWith('v') || s.StartsWith('V')) s = s[1..];
var dash = s.IndexOf('-');
if (dash >= 0) s = s[..dash];
return Version.TryParse(s, out version!);
}
}