Files
PolyTraderSharp/tests/PolyTrader.Tests/WebUi/UpdateStatusTests.cs
T
RichardandClaude Opus 5 b4d8a2a647
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s
Update-Pruefung regelmaessig und ab Werk an, Installation nur auf Klick
Richards Vorgabe vom 14.09.2026 (#587): Updates nie automatisch installieren,
aber regelmaessig pruefen und darauf hinweisen.

Bisher pruefte der Desktop genau einmal beim Start, das WebUI nur beim
Seitenaufruf, und ab Werk gar nicht (UpdateCheckEnabled=false). Eine
wochenlang laufende oder frisch installierte Anwendung erfuhr nie von einem
neuen Release.

- UpdateCheckEnabled ab Werk true, neu UpdateCheckIntervalHours (6, 1..168)
- UpdateCheckBackgroundService: erste Pruefung nach 30 s, dann im Takt; laeuft
  auch ohne WebUI und kopflos; Hinweis im Log einmal je neuer Fassung
- Registrierung ueber AddPolyTraderUpdateCheck, nicht mehr am WebUI
- Desktop-Banner haengt an UpdateStatusService.Checked; "Spaeter" blendet bis
  zur naechsten neueren Fassung aus
- WebUI-Banner holt alle 10 Minuten den vorgehaltenen Stand

Auf der Linux-VM geprueft: 0.1.9 ohne Handeinstellung erkennt 0.2.0, ein
Hinweis im Log, Banner erscheint, "Spaeter" blendet aus, Beenden in 0,1 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:16:33 +02:00

223 lines
8.8 KiB
C#

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
{
/// <summary>
/// Update-Status im WebUI (#565): Zwischenspeicher, Mindestabstand für "Jetzt prüfen", Anzeige.
/// </summary>
public class UpdateStatusTests
{
private sealed class FakeChecker : IUpdateChecker
{
private readonly Func<DateTime> _now;
public int Calls;
public UpdateCheckSnapshot Result { get; set; }
public FakeChecker(Func<DateTime> now)
{
_now = now;
Result = new UpdateCheckSnapshot(DateTime.MinValue, true, true, false, "0.2.0", "Neu: Log-Viewer", true, null);
}
public async Task<UpdateCheckSnapshot> 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 Checked_feuert_nur_bei_frischer_Pruefung()
{
var clock = new Clock();
var checker = new FakeChecker(() => clock.Now);
var service = new UpdateStatusService(checker, () => clock.Now);
int events = 0;
service.Checked += _ => events++;
await service.GetAsync(false, CancellationToken.None);
await service.GetAsync(false, CancellationToken.None); // aus dem Zwischenspeicher
Assert.Equal(1, events);
Assert.Equal("0.2.0", service.Last?.LatestVersion);
}
[Fact]
public async Task Hintergrundpruefung_meldet_jede_neue_Fassung_genau_einmal_und_installiert_nichts()
{
var clock = new Clock();
var checker = new FakeChecker(() => clock.Now);
var status = new UpdateStatusService(checker, () => clock.Now);
var logger = new PolyTraderSharp.Services.TerminalLogger();
var notices = new System.Collections.Generic.List<string>();
logger.OnLogMessage += (_, e) => { if (e.Message.Contains("Update verfügbar")) notices.Add(e.Message); };
var background = new UpdateCheckBackgroundService(status, logger, null);
await background.CheckOnceAsync(CancellationToken.None);
clock.Now = clock.Now + UpdateStatusService.CacheDuration;
await background.CheckOnceAsync(CancellationToken.None); // dieselbe Fassung: kein zweiter Hinweis
checker.Result = checker.Result with { LatestVersion = "0.3.0" };
clock.Now = clock.Now + UpdateStatusService.CacheDuration;
await background.CheckOnceAsync(CancellationToken.None);
Assert.Equal(2, notices.Count);
Assert.Contains("v0.2.0", notices[0]);
Assert.Contains("v0.3.0", notices[1]);
Assert.All(notices, n => Assert.Contains("nur auf Wunsch", n));
}
[Fact]
public async Task Ohne_Update_kein_Hinweis()
{
var clock = new Clock();
var checker = new FakeChecker(() => clock.Now);
checker.Result = checker.Result with { UpdateAvailable = false, LatestVersion = null };
var logger = new PolyTraderSharp.Services.TerminalLogger();
int notices = 0;
logger.OnLogMessage += (_, e) => { if (e.Message.Contains("Update verfügbar")) notices++; };
await new UpdateCheckBackgroundService(new UpdateStatusService(checker, () => clock.Now), logger, null)
.CheckOnceAsync(CancellationToken.None);
Assert.Equal(0, notices);
}
[Theory]
[InlineData(null, 6)]
[InlineData(0, 1)]
[InlineData(12, 12)]
[InlineData(10000, 168)]
public void Pruefintervall_ist_begrenzt(int? hours, int expected)
{
var options = hours == null ? null : new PolyTrader.Core.Configuration.DeploymentcenterOptions { UpdateCheckIntervalHours = hours.Value };
Assert.Equal(TimeSpan.FromHours(expected), UpdateCheckBackgroundService.Interval(options));
}
[Fact]
public void Update_Pruefung_ist_ab_Werk_an()
{
Assert.True(new PolyTrader.Core.Configuration.DeploymentcenterOptions().UpdateCheckEnabled);
}
[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<Microsoft.AspNetCore.Routing.EndpointDataSource>().Endpoints
.OfType<Microsoft.AspNetCore.Routing.RouteEndpoint>()
.Select(e => e.RoutePattern.RawText ?? string.Empty)
.Where(r => r.Contains("update", StringComparison.OrdinalIgnoreCase))
.ToList();
Assert.Equal(new[] { "/api/updates" }, routes);
}
}
}