Files
LehrerApp/LehrerApp.Desktop.Tests/SyncStatusViewModelTests.cs

156 lines
6.0 KiB
C#

using System.Net;
using System.Net.Http.Json;
using LehrerApp.Data;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Sync;
using LehrerApp.Sync.Crypto;
using LehrerApp.Sync.Models;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class SyncStatusViewModelTests
{
[Fact]
public void Konstruktor_EngineBereitsVorhanden_ZeigtNichtDenNichtKonfiguriertText()
{
// SyncEngine feuert sein erstes StatusChanged schon im eigenen Konstruktor - läuft der vor
// dem Abonnieren in SyncStatusViewModel (z.B. weil beide DI-Singletons sind und
// GetService<SyncEngine>() den Engine-Konstruktor erst innerhalb der VM-Factory auslöst),
// ginge dieser erste Status verloren und StatusText bliebe fälschlich beim Default.
using var temp = new TempSyncEngine();
var vm = new SyncStatusViewModel(temp.Engine);
Assert.True(vm.IsServerConfigured);
Assert.NotEqual("Kein Server konfiguriert", vm.StatusText);
Assert.Equal("Synchronisiert", vm.StatusText);
}
[Fact]
public void Konstruktor_KeineEngine_ZeigtNichtKonfiguriertText()
{
var vm = new SyncStatusViewModel(null);
Assert.False(vm.IsServerConfigured);
Assert.Equal("Kein Server konfiguriert", vm.StatusText);
}
/// Regression (TODO 10.1.11): eine per Sync-Pull eingehende Änderung erschien im UI erst nach
/// manuellem Neuladen (z.B. Tab-Wechsel), weil EventApplier absichtlich an jedem ViewModel
/// vorbei direkt auf die LiteDB schreibt. SyncStatusViewModel.DataChanged ist der Hook, über
/// den MainWindowViewModel die sichtbare Seite danach neu lädt - hier wird nur geprüft, dass
/// das Ereignis von SyncEngine bis hierher tatsächlich durchgereicht wird.
[Fact]
public async Task DataChanged_PullMitEreignissen_WirdVonEngineDurchgereicht()
{
using var temp = new TempSyncEngine(new PullEventStubHandler());
var vm = new SyncStatusViewModel(temp.Engine);
var fired = false;
vm.DataChanged += () => fired = true;
await temp.Engine.SyncNowAsync();
Assert.True(fired);
}
[Fact]
public async Task DataChanged_PullOhneEreignisse_FeuertNicht()
{
using var temp = new TempSyncEngine(new EmptyPullStubHandler());
var vm = new SyncStatusViewModel(temp.Engine);
var fired = false;
vm.DataChanged += () => fired = true;
await temp.Engine.SyncNowAsync();
Assert.False(fired);
}
[Fact]
public async Task InkompatibleSyncVersion_ZeigtKlareUpdateMeldung()
{
using var temp = new TempSyncEngine(new IncompatibleVersionStubHandler());
var vm = new SyncStatusViewModel(temp.Engine);
await temp.Engine.SyncNowAsync();
Assert.True(vm.IsIncompatibleVersion);
Assert.Equal("Update erforderlich", vm.StatusText);
Assert.Contains("Bitte aktualisiere die LehrerApp", SyncStatusViewModel.IncompatibleVersionMessage);
}
private sealed class PullEventStubHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.RequestUri!.AbsolutePath == "/api/sync/pull")
{
var evt = new SyncEvent
{
DeviceId = "other-device", DeviceType = DeviceType.Desktop,
EntityType = "Student", EntityId = Guid.NewGuid().ToString(),
Operation = "Save", Payload = "irrelevant-fuer-diesen-test", SequenceNr = 1,
};
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{ Content = JsonContent.Create(new PullResponse { Events = [evt], ServerSequenceNr = 1 }) });
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{ Content = JsonContent.Create(new PushResponse()) });
}
}
private sealed class EmptyPullStubHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = JsonContent.Create(request.RequestUri!.AbsolutePath == "/api/sync/pull"
? new PullResponse()
: (object)new PushResponse()),
});
}
private sealed class IncompatibleVersionStubHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.UpgradeRequired);
response.Headers.Add(SyncProtocol.VersionHeaderName, "2");
return Task.FromResult(response);
}
}
private sealed class TempSyncEngine : IDisposable
{
private readonly string _queuePath = Path.Combine(
Path.GetTempPath(), $"lehrerapp-desktop-tests-{Guid.NewGuid():N}.db");
private readonly LiteDbContext _db = new(new MemoryStream());
private readonly HttpClient _http;
public SyncEngine Engine { get; }
public TempSyncEngine(HttpMessageHandler? handler = null)
{
_http = handler is null ? new HttpClient() : new HttpClient(handler);
_http.BaseAddress = new Uri("https://example.invalid");
var queue = new EventQueue(_queuePath);
var key = SyncCrypto.GenerateKey();
Engine = new SyncEngine(
queue, new ConflictResolver(queue), new EventApplier(_db, key),
new AttachmentSyncer(_db, _http, key), _http,
new SyncConfig { AutoSyncIntervalMinutes = 5 });
}
public void Dispose()
{
Engine.Dispose(); // disposed auch die EventQueue
_db.Dispose();
_http.Dispose();
if (File.Exists(_queuePath)) File.Delete(_queuePath);
}
}
}