From 0143076562019eccf0dc82f0deaec6add79e3ffc Mon Sep 17 00:00:00 2001 From: Flaminel Date: Sun, 18 Oct 2026 23:26:23 +0300 Subject: [PATCH] added test for failed outcome recording --- .../Jobs/SeekerCommandMonitorTests.cs | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerCommandMonitorTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerCommandMonitorTests.cs index b9dc8039..fddfcfaf 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerCommandMonitorTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerCommandMonitorTests.cs @@ -7,6 +7,7 @@ using Cleanuparr.Infrastructure.Features.Arr; using Cleanuparr.Infrastructure.Features.Arr.Interfaces; using Cleanuparr.Infrastructure.Features.Jobs; using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers; +using Cleanuparr.Infrastructure.Tests.TestHelpers; using Cleanuparr.Persistence; using Cleanuparr.Persistence.Models.Configuration.Arr; using Cleanuparr.Persistence.Models.Events; @@ -29,6 +30,7 @@ public class SeekerCommandMonitorTests : IAsyncDisposable private readonly FakeTimeProvider _timeProvider; private readonly IArrClient _arrClient; private readonly IEventPublisher _eventPublisher; + private readonly ILogger _logger; private readonly SeekerCommandMonitor _sut; private readonly CancellationTokenSource _cts; @@ -39,9 +41,9 @@ public class SeekerCommandMonitorTests : IAsyncDisposable _timeProvider = new FakeTimeProvider(); _arrClient = Substitute.For(); _eventPublisher = Substitute.For(); + _logger = Substitute.For>(); _cts = new CancellationTokenSource(); - var logger = Substitute.For>(); var arrClientFactory = Substitute.For(); var serviceProvider = Substitute.For(); @@ -60,7 +62,7 @@ public class SeekerCommandMonitorTests : IAsyncDisposable arrClientFactory.GetClient(Arg.Any(), Arg.Any()).Returns(_arrClient); - _sut = new SeekerCommandMonitor(logger, scopeFactory, _timeProvider); + _sut = new SeekerCommandMonitor(_logger, scopeFactory, _timeProvider); } public async ValueTask DisposeAsync() @@ -1251,6 +1253,46 @@ public class SeekerCommandMonitorTests : IAsyncDisposable cycle.GrabbedUnits.ShouldBe(1); } + [Fact] + public async Task Generic_exception_while_recording_the_outcome_is_logged_and_swallowed() + { + // Arrange: dispose the connection right as the outcome is published, so RecordUnitOutcomeAsync's own queries fail + ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_dataContext); + Guid cycleId = Guid.NewGuid(); + Guid eventId = SetUpUnitOutcomeScenario(radarrInstance.Id, cycleId, externalItemId: 508); + + AddTracker(radarrInstance.Id, eventId, externalItemId: 508); + await _dataContext.SaveChangesAsync(); + await _eventsContext.SaveChangesAsync(); + + StubCommandState(ArrCommandState.Completed); + _arrClient.GetQueueItemsAsync(Arg.Any(), Arg.Any()) + .Returns(new QueueListResponse + { + TotalRecords = 1, + Records = [new QueueRecord { Id = 1, MovieId = 508, Title = "Grabbed.Movie", DownloadId = "HASH508", Protocol = "torrent", Status = "downloading" }] + }); + + _eventPublisher.PublishSearchCompleted( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any?>()) + .Returns(Task.CompletedTask) + .AndDoes(_ => _eventsContext.Database.GetDbConnection().Dispose()); + + // Act + await _sut.StartAsync(_cts.Token); + _timeProvider.Advance(TimeSpan.FromSeconds(11)); + + for (int attempt = 0; attempt < 200 && !_logger.HasLogContainingAtLeastOnce(LogLevel.Warning, "Failed to record the outcome of"); attempt++) + { + await Task.Delay(10); + _timeProvider.Advance(TimeSpan.FromSeconds(60)); + } + + // Assert: the inner catch logged the warning, and the exception never escaped the background service + _logger.HasLogContainingAtLeastOnce(LogLevel.Warning, "Failed to record the outcome of").ShouldBeTrue(); + _sut.ExecuteTask!.IsFaulted.ShouldBeFalse(); + } + /// /// Adds the search-triggered event and matching unit history row RecordUnitOutcomeAsync reads to attribute an outcome. ///