using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using ZB.MOM.WW.GalaxyRepository; namespace ZB.MOM.WW.GalaxyRepository.Tests; /// /// Server-005 regression: the initial RefreshAsync call in /// must not let a transient, /// non-cancellation first-load failure (e.g. a /// or from connection /// establishment) escape and fault the host BackgroundService. /// public sealed class GalaxyHierarchyRefreshServiceTests { /// Verifies that the background service does not fault when the first refresh throws a non-cancellation exception. [Fact] public async Task ExecuteAsync_WhenFirstRefreshThrowsNonCancellationException_DoesNotFaultBackgroundService() { ThrowingCache cache = new(new TimeoutException("connection establishment timed out")); GalaxyHierarchyRefreshService service = CreateService(cache); using CancellationTokenSource cts = new(); await service.StartAsync(cts.Token); // Wait until the first RefreshAsync has actually been attempted (and // thrown) before cancelling, so cancellation cannot race ahead of the // first-load path under test — this is what made the test flaky under // parallel load. await cache.FirstRefreshAttempted.WaitAsync(TimeSpan.FromSeconds(10)); await cts.CancelAsync(); // The background loop must have stopped cleanly: ExecuteTask reaches a // terminal state that is not Faulted (RanToCompletion or Canceled) // rather than faulting on the first refresh. WhenAny is used so a // Canceled task does not rethrow before the IsFaulted assertion. Task? executeTask = service.ExecuteTask; Assert.NotNull(executeTask); Task completed = await Task.WhenAny(executeTask, Task.Delay(TimeSpan.FromSeconds(10))); Assert.Same(executeTask, completed); Assert.False(executeTask.IsFaulted); Assert.Equal(1, cache.RefreshCallCount); await service.StopAsync(CancellationToken.None); } private static GalaxyHierarchyRefreshService CreateService(IGalaxyHierarchyCache cache) { GalaxyRepositoryOptions options = new() { DashboardRefreshIntervalSeconds = 3600, }; return new GalaxyHierarchyRefreshService( cache, Options.Create(options), NullLogger.Instance); } private sealed class ThrowingCache(Exception toThrow) : IGalaxyHierarchyCache { private readonly TaskCompletionSource firstRefreshAttempted = new(TaskCreationOptions.RunContinuationsAsynchronously); /// Gets the number of refresh calls. public int RefreshCallCount { get; private set; } /// Gets a task that completes once refresh has been invoked at least once. public Task FirstRefreshAttempted => firstRefreshAttempted.Task; /// Gets the current cache entry. public GalaxyHierarchyCacheEntry Current => GalaxyHierarchyCacheEntry.Empty; /// Refreshes the cache asynchronously and throws the configured exception. /// Token to observe for cancellation. public Task RefreshAsync(CancellationToken cancellationToken) { RefreshCallCount++; firstRefreshAttempted.TrySetResult(); throw toThrow; } /// Waits for the first load and completes immediately. /// Token to observe for cancellation. public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }