using FasdDesktopUi.Basics.Services; using FasdDesktopUi.Basics.Services.Models; namespace F4SD.Cockpit.Client.Test.Basics.Sevices.TicketOverview; public class TicketOverviewUpdateServiceTest { [Fact] public async Task UpdateAvailability_FirstFetch_EmitsInitializationEventsForBothScopes() { // Arrange var communication = new FakeCommunication(); communication.SetCounts(TileScope.Personal, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["TicketsNew"] = 2 }); communication.SetCounts(TileScope.Role, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["TicketsNew"] = 5 }); var service = CreateService(communication, out _); var events = new List(); service.OverviewCountsChanged += (_, args) => events.Add(args); // Act service.UpdateAvailability(true); await WaitUntilAsync(() => service.AreAllScopesInitialized); // Assert Assert.Equal(2, events.Count); Assert.All(events, e => Assert.Empty(e.Changes)); Assert.Contains(events, e => e.InitializedScope == TileScope.Personal); Assert.Contains(events, e => e.InitializedScope == TileScope.Role); Assert.Equal(2, service.CurrentCounts["TicketsNew"].Personal); Assert.Equal(5, service.CurrentCounts["TicketsNew"].Role); } [Fact] public async Task FetchAsync_ChangedAndUnchangedCounts_EmitsOnlyRealDeltaEvent() { // Arrange var communication = new FakeCommunication(); communication.SetCounts(TileScope.Personal, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["TicketsNew"] = 1 }); communication.SetCounts(TileScope.Role, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["TicketsNew"] = 3 }); var service = CreateService(communication, out _); var events = new List(); service.OverviewCountsChanged += (_, args) => events.Add(args); service.UpdateAvailability(true); await WaitUntilAsync(() => service.AreAllScopesInitialized); events.Clear(); // Act + Assert (changed) communication.SetCounts(TileScope.Personal, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["TicketsNew"] = 4 }); await service.FetchAsync(TileScope.Personal); Assert.Single(events); Assert.Single(events[0].Changes); Assert.Equal("TicketsNew", events[0].Changes[0].Key); Assert.Equal(1, events[0].Changes[0].OldCount); Assert.Equal(4, events[0].Changes[0].NewCount); Assert.Equal(3, events[0].Changes[0].Delta); // Act + Assert (unchanged) events.Clear(); await service.FetchAsync(TileScope.Personal); Assert.Empty(events); } [Fact] public async Task Stop_ResetsInitializationAndCounts() { // Arrange var communication = new FakeCommunication(); communication.SetCounts(TileScope.Personal, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["TicketsNew"] = 7 }); communication.SetCounts(TileScope.Role, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["IncidentNew"] = 9 }); var service = CreateService(communication, out _); service.UpdateAvailability(true); await WaitUntilAsync(() => service.AreAllScopesInitialized); // Act service.Stop(); // Assert Assert.False(service.IsScopeInitialized(TileScope.Personal)); Assert.False(service.IsScopeInitialized(TileScope.Role)); Assert.All(service.CurrentCounts.Values, value => { Assert.Equal(0, value.Personal); Assert.Equal(0, value.Role); }); } private static TicketOverviewUpdateService CreateService(FakeCommunication communication, out FakeDispatcher dispatcher) { dispatcher = new FakeDispatcher(); var source = new FakeCommunicationSource(communication); var settings = new FakeSettingsProvider(); return new TicketOverviewUpdateService(source, dispatcher, settings); } private static async Task WaitUntilAsync(Func condition, int timeoutMs = 2000) { var startedAt = DateTime.UtcNow; while (!condition()) { if ((DateTime.UtcNow - startedAt).TotalMilliseconds > timeoutMs) { throw new TimeoutException("Condition was not reached within timeout."); } await Task.Delay(10); } } private sealed class FakeCommunicationSource : ITicketOverviewCommunicationSource { private readonly ITicketOverviewCommunication _communication; public FakeCommunicationSource(ITicketOverviewCommunication communication) { _communication = communication; } public ITicketOverviewCommunication Resolve() { return _communication; } } private sealed class FakeCommunication : ITicketOverviewCommunication { private Dictionary _personalCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); private Dictionary _roleCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); public bool IsDemo() { return false; } public Task> GetTicketOverviewCounts(string[] overviewKeys, bool useRoleScope) { var source = useRoleScope ? _roleCounts : _personalCounts; return Task.FromResult(new Dictionary(source, StringComparer.OrdinalIgnoreCase)); } public void RegisterGeneratedTicket(FasdCockpitCommunicationDemo.DemoTicketRecord record) { } public void SetCounts(TileScope scope, Dictionary counts) { var copy = counts == null ? new Dictionary(StringComparer.OrdinalIgnoreCase) : new Dictionary(counts, StringComparer.OrdinalIgnoreCase); if (scope == TileScope.Role) { _roleCounts = copy; } else { _personalCounts = copy; } } } private sealed class FakeDispatcher : ITicketOverviewDispatcher { public Task InvokeAsync(Action action) { action(); return Task.CompletedTask; } public Task InvokeAsync(Func action) { return action(); } public ITicketOverviewTimer CreateTimer(TimeSpan interval, Action tick) { return new FakeTimer(interval, tick); } } private sealed class FakeSettingsProvider : ITicketOverviewSettingsProvider { public int GetPollingMinutes(TileScope scope) { return 1; } } private sealed class FakeTimer : ITicketOverviewTimer { private readonly Action _tick; public FakeTimer(TimeSpan interval, Action tick) { Interval = interval; _tick = tick; } public TimeSpan Interval { get; set; } public bool IsEnabled { get; private set; } public void Start() { IsEnabled = true; } public void Stop() { IsEnabled = false; } public void Fire() { _tick?.Invoke(); } } }