using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Browsing; /// Unit tests for . We drive the reaper /// directly through the internal ReapOnceAsync entry point and skew /// to simulate the passage of time — /// no real wall-clock waits. public sealed class BrowseSessionReaperTests { private static BrowseSessionReaper NewReaper(BrowseSessionRegistry registry) => new(registry, NullLogger.Instance); [Fact] public async Task ReapOnceAsync_evicts_idle_session() { var registry = new BrowseSessionRegistry(); var session = new FakeBrowseSession { LastUsedUtc = DateTime.UtcNow - TimeSpan.FromMinutes(3), }; registry.Register(session); var reaper = NewReaper(registry); await reaper.ReapOnceAsync(CancellationToken.None); registry.TryGet(session.Token, out _).ShouldBeFalse(); session.Disposed.ShouldBeTrue(); } [Fact] public async Task ReapOnceAsync_preserves_recent_session() { var registry = new BrowseSessionRegistry(); var session = new FakeBrowseSession { LastUsedUtc = DateTime.UtcNow }; registry.Register(session); var reaper = NewReaper(registry); await reaper.ReapOnceAsync(CancellationToken.None); registry.TryGet(session.Token, out _).ShouldBeTrue(); session.Disposed.ShouldBeFalse(); } [Fact] public async Task ReapOnceAsync_handles_already_removed_session() { var registry = new BrowseSessionRegistry(); var session = new FakeBrowseSession { LastUsedUtc = DateTime.UtcNow - TimeSpan.FromMinutes(3), }; registry.Register(session); // Race the reaper: pull the entry out before the loop calls TryRemove. registry.TryRemove(session.Token, out _).ShouldBeTrue(); var reaper = NewReaper(registry); // No exception, no double dispose. Because we removed the session ourselves // without disposing it, Disposed should still be false. await reaper.ReapOnceAsync(CancellationToken.None); session.Disposed.ShouldBeFalse(); } [Fact] public async Task ReapOnceAsync_continues_when_one_session_dispose_throws() { var registry = new BrowseSessionRegistry(); var staleAt = DateTime.UtcNow - TimeSpan.FromMinutes(3); var bad = new FakeBrowseSession { LastUsedUtc = staleAt, ThrowOnDispose = true }; var good = new FakeBrowseSession { LastUsedUtc = staleAt }; registry.Register(bad); registry.Register(good); var reaper = NewReaper(registry); // The dispose exception is swallowed; the second session must still be reaped + disposed. await Should.NotThrowAsync(() => reaper.ReapOnceAsync(CancellationToken.None)); registry.TryGet(bad.Token, out _).ShouldBeFalse(); registry.TryGet(good.Token, out _).ShouldBeFalse(); good.Disposed.ShouldBeTrue(); } }