using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Web.Updates; using Xunit; namespace PolyTrader.Tests.WebUi { /// /// Update-Status im WebUI (#565): Zwischenspeicher, Mindestabstand für "Jetzt prüfen", Anzeige. /// public class UpdateStatusTests { private sealed class FakeChecker : IUpdateChecker { private readonly Func _now; public int Calls; public UpdateCheckSnapshot Result { get; set; } public FakeChecker(Func now) { _now = now; Result = new UpdateCheckSnapshot(DateTime.MinValue, true, true, false, "0.2.0", "Neu: Log-Viewer", true, null); } public async Task CheckAsync(CancellationToken ct) { Interlocked.Increment(ref Calls); await Task.Delay(20, ct); return Result with { CheckedAtUtc = _now() }; } } private sealed class Clock { public DateTime Now = new(2026, 9, 13, 12, 0, 0, DateTimeKind.Utc); } [Fact] public async Task Innerhalb_der_Haltezeit_wird_nicht_erneut_gefragt() { var clock = new Clock(); var checker = new FakeChecker(() => clock.Now); var service = new UpdateStatusService(checker, () => clock.Now); var first = await service.GetAsync(force: false, CancellationToken.None); clock.Now = clock.Now.AddMinutes(29); var second = await service.GetAsync(force: false, CancellationToken.None); Assert.False(first.FromCache); Assert.True(second.FromCache); Assert.Equal(1, checker.Calls); } [Fact] public async Task Nach_Ablauf_der_Haltezeit_wird_frisch_gefragt() { var clock = new Clock(); var checker = new FakeChecker(() => clock.Now); var service = new UpdateStatusService(checker, () => clock.Now); await service.GetAsync(false, CancellationToken.None); clock.Now = clock.Now + UpdateStatusService.CacheDuration; var again = await service.GetAsync(false, CancellationToken.None); Assert.False(again.FromCache); Assert.Equal(2, checker.Calls); } [Fact] public async Task Jetzt_pruefen_zu_frueh_liefert_den_vorigen_Stand_mit_Wartezeit() { var clock = new Clock(); var checker = new FakeChecker(() => clock.Now); var service = new UpdateStatusService(checker, () => clock.Now); await service.GetAsync(false, CancellationToken.None); clock.Now = clock.Now.AddSeconds(20); var tooEarly = await service.GetAsync(force: true, CancellationToken.None); Assert.True(tooEarly.FromCache); Assert.Equal(TimeSpan.FromSeconds(40), tooEarly.RetryAfter); Assert.Equal(1, checker.Calls); clock.Now = clock.Now.AddSeconds(40); var allowed = await service.GetAsync(force: true, CancellationToken.None); Assert.False(allowed.FromCache); Assert.Null(allowed.RetryAfter); Assert.Equal(2, checker.Calls); } [Fact] public async Task Gleichzeitige_Aufrufe_fragen_nur_einmal() { var clock = new Clock(); var checker = new FakeChecker(() => clock.Now); var service = new UpdateStatusService(checker, () => clock.Now); await Task.WhenAll(Enumerable.Range(0, 10).Select(_ => service.GetAsync(false, CancellationToken.None))); Assert.Equal(1, checker.Calls); } [Fact] public async Task Route_liefert_Fassung_Stand_und_verweist_zur_Installation_auf_den_Desktop() { var clock = new Clock(); var checker = new FakeChecker(() => clock.Now); await using var web = await WebUiTestHost.StartAsync(s => s.AddSingleton(new UpdateStatusService(checker, () => clock.Now))); var status = await web.GetJsonAsync("/api/updates"); Assert.Equal(PolyTrader.Core.BuildInfo.Version, status.GetProperty("currentVersion").GetString()); Assert.True(status.GetProperty("updateAvailable").GetBoolean()); Assert.Equal("0.2.0", status.GetProperty("latestVersion").GetString()); Assert.True(status.GetProperty("isCritical").GetBoolean()); Assert.Equal("desktop", status.GetProperty("installVia").GetString()); } [Fact] public async Task Ohne_Dienst_503() { await using var web = await WebUiTestHost.StartAsync(); using var res = await web.Client.GetAsync("/api/updates"); Assert.Equal(503, (int)res.StatusCode); } [Fact] public async Task Das_WebUI_kennt_keinen_Weg_zum_Installieren() { await using var web = await WebUiTestHost.StartAsync(); var routes = web.Get().Endpoints .OfType() .Select(e => e.RoutePattern.RawText ?? string.Empty) .Where(r => r.Contains("update", StringComparison.OrdinalIgnoreCase)) .ToList(); Assert.Equal(new[] { "/api/updates" }, routes); } } }