Testabdeckung Kapitel 13.1 (Tests, Fehlerbehandlung)
Vier neue xUnit-Testprojekte (LehrerApp.Tests, .Data.Tests, .Desktop.Tests, .Sync.Tests) mit zusammen 104 Tests: GradingService, SchoolYearService, Repositories gegen In-Memory-LiteDB, Mitarbeits-Aggregation (3.2), Zeugnisnotenberechnung (2.4) und ConflictResolver. Dabei zwei echte Fehler in der Sync-Schicht gefunden und behoben: - SyncEvent.EventId fehlte [BsonId], wodurch EventQueue.Acknowledge() nie etwas aus der Queue löschte. - ConflictResolver verglich Zeitstempel unterschiedlicher DateTimeKind direkt (LiteDB liefert Local statt Utc zurück), was die Gleichstand- Regel außerhalb von UTC+0 verfälschte. SchoolYearService.CurrentSchoolYear()/RecentSchoolYears() um ein optionales today-Argument erweitert, um den Schuljahreswechsel deterministisch zu testen; LiteDbContext um einen Stream-Konstruktor für In-Memory-Tests.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
using LehrerApp.Sync.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Sync.Tests;
|
||||
|
||||
public sealed class ConflictResolverTests
|
||||
{
|
||||
// ── Kein Konflikt ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_KeinLokalesEreignisFuerDieselbeEntitaet_GibtNullZurueck()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var remote = MakeEvent("companion-1", DeviceType.Companion, "Student", "s1");
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_LokalesEreignisFuerAndereEntitaet_ZaehltNichtAlsKonflikt()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
temp.Queue.Enqueue("desktop-1", DeviceType.Desktop, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("companion-1", DeviceType.Companion, "Student", "s2"); // andere EntityId
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_LokalesEreignisVomSelbenGeraet_ZaehltNichtAlsKonflikt()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
temp.Queue.Enqueue("companion-1", DeviceType.Companion, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("companion-1", DeviceType.Companion, "Student", "s1"); // gleiche DeviceId
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ── Desktop gewinnt immer gegen Companion ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_LokalDesktopGegenRemoteCompanion_DesktopGewinnt()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var local = temp.Queue.Enqueue("desktop-1", DeviceType.Desktop, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("companion-1", DeviceType.Companion, "Student", "s1");
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("LocalWon", result.Resolution);
|
||||
Assert.Equal(local.EventId, result.LocalEvent.EventId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_LokalDesktopGegenRemoteCompanion_LokalesEreignisBleibtInDerQueue()
|
||||
{
|
||||
// Desktop gewinnt -> der lokale (gewinnende) Event wird NICHT bestätigt/entfernt,
|
||||
// da er ja noch zum Server gepusht werden muss.
|
||||
using var temp = new TempEventQueue();
|
||||
temp.Queue.Enqueue("desktop-1", DeviceType.Desktop, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("companion-1", DeviceType.Companion, "Student", "s1");
|
||||
|
||||
new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Equal(1, temp.Queue.PendingCount());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_LokalCompanionGegenRemoteDesktop_DesktopGewinntTrotzAeltererZeit()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var local = temp.Queue.Enqueue("companion-1", DeviceType.Companion, "Student", "s1", "Update", "{}");
|
||||
// Remote (Desktop) ist zeitlich "älter", gewinnt aber trotzdem, weil Desktop immer gewinnt.
|
||||
var remote = MakeEvent("desktop-1", DeviceType.Desktop, "Student", "s1", timestamp: local.Timestamp.AddMinutes(-5));
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Equal("RemoteWon", result!.Resolution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_RemoteGewinnt_LokalesEreignisWirdAusDerQueueEntfernt()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
temp.Queue.Enqueue("companion-1", DeviceType.Companion, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("desktop-1", DeviceType.Desktop, "Student", "s1");
|
||||
|
||||
new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Equal(0, temp.Queue.PendingCount());
|
||||
}
|
||||
|
||||
// ── Gleichstand (beide gleicher Gerätetyp): späterer Timestamp gewinnt ──────
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_BeideCompanion_SpaetererTimestampGewinnt()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var local = temp.Queue.Enqueue("companion-1", DeviceType.Companion, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("companion-2", DeviceType.Companion, "Student", "s1",
|
||||
timestamp: local.Timestamp.AddMinutes(5));
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Equal("RemoteWon", result!.Resolution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_BeideDesktopGleicherTimestamp_LokalesEreignisGewinntBeiGleichstand()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
temp.Queue.Enqueue("desktop-1", DeviceType.Desktop, "Student", "s1", "Update", "{}");
|
||||
// Den tatsächlich aus der Queue geladenen (und damit BSON-gerundeten) Zeitstempel
|
||||
// verwenden, sonst wäre ein "exakter" Gleichstand durch Rundung beim Rundtrip nicht
|
||||
// exakt reproduzierbar.
|
||||
var reloadedTimestamp = temp.Queue.GetPending()[0].Timestamp.ToUniversalTime();
|
||||
var remote = MakeEvent("desktop-2", DeviceType.Desktop, "Student", "s1", timestamp: reloadedTimestamp);
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
// ">= " in der Ungleichung: bei exaktem Gleichstand gewinnt das lokale Ereignis.
|
||||
Assert.Equal("LocalWon", result!.Resolution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryResolve_BeideCompanionAelteresLokalesEreignis_RemoteGewinntAlsNeuer()
|
||||
{
|
||||
using var temp = new TempEventQueue();
|
||||
var local = temp.Queue.Enqueue("companion-1", DeviceType.Companion, "Student", "s1", "Update", "{}");
|
||||
var remote = MakeEvent("companion-2", DeviceType.Companion, "Student", "s1",
|
||||
timestamp: local.Timestamp.AddSeconds(-1)); // remote ist älter -> lokal (neuer) gewinnt
|
||||
|
||||
var result = new ConflictResolver(temp.Queue).TryResolve(remote, "desktop-1");
|
||||
|
||||
Assert.Equal("LocalWon", result!.Resolution);
|
||||
}
|
||||
|
||||
// ── Hilfsfunktionen ───────────────────────────────────────────────────────
|
||||
|
||||
private static SyncEvent MakeEvent(string deviceId, DeviceType deviceType, string entityType,
|
||||
string entityId, DateTime? timestamp = null) => new()
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceType = deviceType,
|
||||
EntityType = entityType,
|
||||
EntityId = entityId,
|
||||
Operation = "Update",
|
||||
Payload = "{}",
|
||||
Timestamp = timestamp ?? DateTime.UtcNow,
|
||||
};
|
||||
|
||||
private sealed class TempEventQueue : IDisposable
|
||||
{
|
||||
private readonly string _directory = Path.Combine(
|
||||
Path.GetTempPath(), $"lehrerapp-sync-tests-{Guid.NewGuid():N}");
|
||||
public EventQueue Queue { get; }
|
||||
|
||||
public TempEventQueue()
|
||||
{
|
||||
Directory.CreateDirectory(_directory);
|
||||
Queue = new EventQueue(Path.Combine(_directory, "queue.db"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Queue.Dispose();
|
||||
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user