Umsetzung von #565. GET /api/updates liefert die laufende Fassung (BuildInfo), die verfuegbare, den Hinweis auf ein Sicherheitsupdate und den Changelog. Geprueft wird mit demselben Aufruf wie beim Start des Desktops (UpdateClient gegen das Deploymentcenter). Den Lizenzschluessel fuer den Paketabruf liest der Server, er verlaesst ihn nicht. Zwischenspeicher: UpdateStatusService haelt das Ergebnis 30 Minuten vor. "Jetzt pruefen" umgeht das, aber fruehestens nach 60 Sekunden; bis dahin kommt der vorige Stand mit der Wartezeit zurueck. Gleichzeitige Aufrufe loesen eine einzige Pruefung aus. Ohne das fragte jeder Seitenaufruf, jedes offene Browserfenster, das Deploymentcenter. Die Pruefung liegt hinter IUpdateChecker und ist damit ohne Netz testbar. Kein Installieren ueber das WebUI, und ein Test haelt fest, dass es keine solche Route gibt: Die Installation startet den Update-Agent und beendet danach die Anwendung und mit ihr den Webserver. Richards Vorgabe beschraenkt das Beenden ueber das WebUI auf das Trading (#563). Oberflaeche: - Karte "Version & Updates" in den Einstellungen, mit "Jetzt pruefen". - Im Dashboard ein Banner wie im Desktop-Launcher, sobald ein Update vorliegt. Er verweist zur Installation auf den Desktop. Nicht durch Unit-Tests abgedeckt: DeploymentcenterUpdateChecker selbst, weil er ueber das Netz geht. Er wird auf der VM gegen das echte Deploymentcenter geprueft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.5 KiB
C#
149 lines
5.5 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 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);
|
|
}
|
|
}
|
|
}
|