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>
223 lines
8.8 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|