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() 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); } private sealed class PullEventStubHandler : HttpMessageHandler { protected override Task 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 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 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); } } }