using Cleanuparr.Domain.Enums; using Cleanuparr.Infrastructure.Features.Notifications; using Cleanuparr.Infrastructure.Features.Notifications.Consumers; using Cleanuparr.Infrastructure.Features.Notifications.Models; using MassTransit; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; using Moq; using Xunit; namespace Cleanuparr.Infrastructure.Tests.Features.Notifications; public class NotificationConsumerTests { private readonly Mock> _serviceLoggerMock; private readonly Mock _configurationServiceMock; private readonly Mock _providerFactoryMock; private readonly NotificationService _notificationService; private readonly FakeTimeProvider _timeProvider; public NotificationConsumerTests() { _serviceLoggerMock = new Mock>(); _configurationServiceMock = new Mock(); _providerFactoryMock = new Mock(); _timeProvider = new FakeTimeProvider(); _notificationService = new NotificationService( _serviceLoggerMock.Object, _configurationServiceMock.Object, _providerFactoryMock.Object); } #region Consume Tests - FailedImportStrikeNotification [Fact] public async Task Consume_FailedImportStrikeNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new FailedImportStrikeNotification { Title = "Test Failed Import", Description = "Test Description", Level = NotificationLevel.Warning, InstanceType = InstanceType.Radarr, InstanceUrl = new Uri("http://radarr.local"), Hash = "TEST123" }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.FailedImportStrike, capturedEventType); } #endregion #region Consume Tests - StalledStrikeNotification [Fact] public async Task Consume_StalledStrikeNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new StalledStrikeNotification { Title = "Test Stalled", Description = "Stalled Description", Level = NotificationLevel.Important, InstanceType = InstanceType.Sonarr, InstanceUrl = new Uri("http://sonarr.local"), Hash = "STALL123" }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.StalledStrike, capturedEventType); } #endregion #region Consume Tests - SlowSpeedStrikeNotification [Fact] public async Task Consume_SlowSpeedStrikeNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new SlowSpeedStrikeNotification { Title = "Slow Speed", Description = "Download too slow", Level = NotificationLevel.Warning, InstanceType = InstanceType.Radarr, InstanceUrl = new Uri("http://radarr.local"), Hash = "SLOW123" }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.SlowSpeedStrike, capturedEventType); } #endregion #region Consume Tests - SlowTimeStrikeNotification [Fact] public async Task Consume_SlowTimeStrikeNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new SlowTimeStrikeNotification { Title = "Slow Time", Description = "Download taking too long", Level = NotificationLevel.Warning, InstanceType = InstanceType.Radarr, InstanceUrl = new Uri("http://radarr.local"), Hash = "TIME123" }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.SlowTimeStrike, capturedEventType); } #endregion #region Consume Tests - QueueItemDeletedNotification [Fact] public async Task Consume_QueueItemDeletedNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new QueueItemDeletedNotification { Title = "Item Deleted", Description = "Queue item removed", Level = NotificationLevel.Important, InstanceType = InstanceType.Lidarr, InstanceUrl = new Uri("http://lidarr.local"), Hash = "DEL123" }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.QueueItemDeleted, capturedEventType); } #endregion #region Consume Tests - DownloadCleanedNotification [Fact] public async Task Consume_DownloadCleanedNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new DownloadCleanedNotification { Title = "Download Cleaned", Description = "Old download removed", Level = NotificationLevel.Information }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.DownloadCleaned, capturedEventType); } #endregion #region Consume Tests - CategoryChangedNotification [Fact] public async Task Consume_CategoryChangedNotification_SendsCorrectEventType() { // Arrange var consumer = CreateConsumer(); var notification = new CategoryChangedNotification { Title = "Category Changed", Description = "Category updated", Level = NotificationLevel.Information }; var contextMock = CreateConsumeContextMock(notification); NotificationEventType? capturedEventType = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .Callback(e => capturedEventType = e) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock.Setup(p => p.SendNotificationAsync(It.IsAny())).Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.Equal(NotificationEventType.CategoryChanged, capturedEventType); } #endregion #region NotificationContext Conversion Tests [Theory] [InlineData(NotificationLevel.Information, EventSeverity.Information)] [InlineData(NotificationLevel.Warning, EventSeverity.Warning)] [InlineData(NotificationLevel.Important, EventSeverity.Important)] public async Task Consume_MapsNotificationLevelToSeverity(NotificationLevel level, EventSeverity expectedSeverity) { // Arrange var consumer = CreateConsumer(); var notification = new FailedImportStrikeNotification { Title = "Test", Description = "Test", Level = level, InstanceType = InstanceType.Radarr, InstanceUrl = new Uri("http://radarr.local"), Hash = "LEVEL123" }; var contextMock = CreateConsumeContextMock(notification); NotificationContext? capturedContext = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock .Setup(p => p.SendNotificationAsync(It.IsAny())) .Callback(c => capturedContext = c) .Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.NotNull(capturedContext); Assert.Equal(expectedSeverity, capturedContext.Severity); } [Fact] public async Task Consume_ArrNotification_IncludesArrDataInContext() { // Arrange var consumer = CreateConsumer(); var notification = new FailedImportStrikeNotification { Title = "Test", Description = "Test", Level = NotificationLevel.Warning, InstanceType = InstanceType.Sonarr, InstanceUrl = new Uri("http://sonarr.local"), Hash = "ABC123", Image = new Uri("http://example.com/image.jpg") }; var contextMock = CreateConsumeContextMock(notification); NotificationContext? capturedContext = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock .Setup(p => p.SendNotificationAsync(It.IsAny())) .Callback(c => capturedContext = c) .Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.NotNull(capturedContext); Assert.Equal("Sonarr", capturedContext.Data["Instance type"]); Assert.Equal("http://sonarr.local/", capturedContext.Data["Url"]); Assert.Equal("ABC123", capturedContext.Data["Hash"]); Assert.Equal(new Uri("http://example.com/image.jpg"), capturedContext.Image); } [Fact] public async Task Consume_WithCustomFields_IncludesFieldsInContext() { // Arrange var consumer = CreateConsumer(); var notification = new FailedImportStrikeNotification { Title = "Test", Description = "Test", Level = NotificationLevel.Warning, InstanceType = InstanceType.Radarr, InstanceUrl = new Uri("http://radarr.local"), Hash = "XYZ789", Fields = new List { new() { Key = "CustomKey1", Value = "CustomValue1" }, new() { Key = "CustomKey2", Value = "CustomValue2" } } }; var contextMock = CreateConsumeContextMock(notification); NotificationContext? capturedContext = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock .Setup(p => p.SendNotificationAsync(It.IsAny())) .Callback(c => capturedContext = c) .Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.NotNull(capturedContext); Assert.Equal("CustomValue1", capturedContext.Data["CustomKey1"]); Assert.Equal("CustomValue2", capturedContext.Data["CustomKey2"]); } [Fact] public async Task Consume_NonArrNotification_DoesNotIncludeArrData() { // Arrange var consumer = CreateConsumer(); var notification = new DownloadCleanedNotification { Title = "Download Cleaned", Description = "Test", Level = NotificationLevel.Information }; var contextMock = CreateConsumeContextMock(notification); NotificationContext? capturedContext = null; var providerMock = new Mock(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise }; _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .ReturnsAsync(new List { providerDto }); _providerFactoryMock .Setup(f => f.CreateProvider(It.IsAny())) .Returns(providerMock.Object); providerMock .Setup(p => p.SendNotificationAsync(It.IsAny())) .Callback(c => capturedContext = c) .Returns(Task.CompletedTask); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert Assert.NotNull(capturedContext); Assert.False(capturedContext.Data.ContainsKey("Instance type")); Assert.False(capturedContext.Data.ContainsKey("Url")); Assert.False(capturedContext.Data.ContainsKey("Hash")); } #endregion #region No Providers Configured Tests [Fact] public async Task Consume_WhenNoProvidersConfigured_DoesNotSendNotification() { // Arrange var consumer = CreateConsumer(); var notification = new FailedImportStrikeNotification { Title = "Test", Description = "Test", Level = NotificationLevel.Warning, InstanceType = InstanceType.Radarr, InstanceUrl = new Uri("http://radarr.local"), Hash = "NOPROV123" }; var contextMock = CreateConsumeContextMock(notification); _configurationServiceMock .Setup(s => s.GetProvidersForEventAsync(It.IsAny())) .ReturnsAsync(new List()); // Act await ConsumeWithTimeAdvance(consumer, contextMock); // Assert _providerFactoryMock.Verify(f => f.CreateProvider(It.IsAny()), Times.Never); } #endregion #region Helper Methods private NotificationConsumer CreateConsumer() where T : Notification { var loggerMock = new Mock>>(); return new NotificationConsumer(loggerMock.Object, _notificationService, _timeProvider); } private static Mock> CreateConsumeContextMock(T message) where T : class { var mock = new Mock>(); mock.Setup(c => c.Message).Returns(message); return mock; } /// /// Executes the consumer and advances time past the 1-second spam prevention delay /// private async Task ConsumeWithTimeAdvance(NotificationConsumer consumer, Mock> contextMock) where T : Notification { var task = consumer.Consume(contextMock.Object); _timeProvider.Advance(TimeSpan.FromSeconds(1)); await task; } #endregion }