using Cleanuparr.Domain.Enums; using Cleanuparr.Infrastructure.Features.Notifications; using Cleanuparr.Infrastructure.Features.Notifications.Models; using Cleanuparr.Infrastructure.Tests.TestHelpers; using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.ExceptionExtensions; using Shouldly; using Xunit; namespace Cleanuparr.Infrastructure.Tests.Features.Notifications; public class NotificationServiceTests { private readonly ILogger _logger; private readonly INotificationConfigurationService _configService; private readonly INotificationProviderFactory _providerFactory; private readonly NotificationService _service; public NotificationServiceTests() { _logger = Substitute.For>(); _configService = Substitute.For(); _providerFactory = Substitute.For(); _service = new NotificationService( _logger, _configService, _providerFactory); } #region SendNotificationAsync Tests [Fact] public async Task SendNotificationAsync_NoProviders_DoesNotSendNotifications() { // Arrange var eventType = NotificationEventType.QueueItemDeleted; var context = CreateTestContext(); _configService.GetProvidersForEventAsync(eventType) .Returns(new List()); // Act await _service.SendNotificationAsync(eventType, context); // Assert _providerFactory.DidNotReceive().CreateProvider(Arg.Any()); } [Fact] public async Task SendNotificationAsync_WithProvider_SendsNotification() { // Arrange var eventType = NotificationEventType.DownloadCleaned; var context = CreateTestContext(); var providerConfig = CreateProviderConfig("TestProvider"); var provider = Substitute.For(); provider.Name.Returns("TestProvider"); _configService.GetProvidersForEventAsync(eventType) .Returns(new List { providerConfig }); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act await _service.SendNotificationAsync(eventType, context); // Assert await provider.Received(1).SendNotificationAsync(context); } [Fact] public async Task SendNotificationAsync_WithMultipleProviders_SendsToAll() { // Arrange var eventType = NotificationEventType.StalledStrike; var context = CreateTestContext(); var provider1Config = CreateProviderConfig("Provider1"); var provider2Config = CreateProviderConfig("Provider2"); var provider1 = Substitute.For(); provider1.Name.Returns("Provider1"); var provider2 = Substitute.For(); provider2.Name.Returns("Provider2"); _configService.GetProvidersForEventAsync(eventType) .Returns(new List { provider1Config, provider2Config }); _providerFactory.CreateProvider(provider1Config) .Returns(provider1); _providerFactory.CreateProvider(provider2Config) .Returns(provider2); // Act await _service.SendNotificationAsync(eventType, context); // Assert await provider1.Received(1).SendNotificationAsync(context); await provider2.Received(1).SendNotificationAsync(context); } [Fact] public async Task SendNotificationAsync_OneProviderFails_OthersStillExecute() { // Arrange var eventType = NotificationEventType.CategoryChanged; var context = CreateTestContext(); var failingProviderConfig = CreateProviderConfig("FailingProvider"); var successProviderConfig = CreateProviderConfig("SuccessProvider"); var failingProvider = Substitute.For(); failingProvider.Name.Returns("FailingProvider"); failingProvider.SendNotificationAsync(Arg.Any()) .ThrowsAsync(new Exception("Provider failed")); var successProvider = Substitute.For(); successProvider.Name.Returns("SuccessProvider"); _configService.GetProvidersForEventAsync(eventType) .Returns(new List { failingProviderConfig, successProviderConfig }); _providerFactory.CreateProvider(failingProviderConfig) .Returns(failingProvider); _providerFactory.CreateProvider(successProviderConfig) .Returns(successProvider); // Act await _service.SendNotificationAsync(eventType, context); // Assert - both providers should have been called await failingProvider.Received(1).SendNotificationAsync(context); await successProvider.Received(1).SendNotificationAsync(context); } [Fact] public async Task SendNotificationAsync_ProviderFails_LogsWarning() { // Arrange var eventType = NotificationEventType.QueueItemDeleted; var context = CreateTestContext(); var providerConfig = CreateProviderConfig("FailingProvider"); var provider = Substitute.For(); provider.Name.Returns("FailingProvider"); provider.SendNotificationAsync(Arg.Any()) .ThrowsAsync(new Exception("Provider failed")); _configService.GetProvidersForEventAsync(eventType) .Returns(new List { providerConfig }); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act await _service.SendNotificationAsync(eventType, context); // Assert _logger.ReceivedLogContaining(LogLevel.Warning, "Failed to send notification"); } [Fact] public async Task SendNotificationAsync_ConfigServiceThrows_LogsError() { // Arrange var eventType = NotificationEventType.SlowSpeedStrike; var context = CreateTestContext(); _configService.GetProvidersForEventAsync(eventType) .ThrowsAsync(new Exception("Config service failed")); // Act await _service.SendNotificationAsync(eventType, context); // Assert _logger.ReceivedLogContaining(LogLevel.Error, "Failed to send notifications"); } #endregion #region SendTestNotificationAsync Tests [Fact] public async Task SendTestNotificationAsync_SendsTestContext() { // Arrange var providerConfig = CreateProviderConfig("TestProvider"); var provider = Substitute.For(); provider.Name.Returns("TestProvider"); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act await _service.SendTestNotificationAsync(providerConfig); // Assert await provider.Received(1).SendNotificationAsync(Arg.Is(c => c.EventType == NotificationEventType.Test && c.Title == "Test Notification from Cleanuparr" && c.Description.Contains("test notification") && c.Severity == EventSeverity.Information && c.Data != null && c.Data.ContainsKey("Test time") && c.Data.ContainsKey("Provider type") )); } [Fact] public async Task SendTestNotificationAsync_Success_LogsInformation() { // Arrange var providerConfig = CreateProviderConfig("TestProvider"); var provider = Substitute.For(); provider.Name.Returns("TestProvider"); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act await _service.SendTestNotificationAsync(providerConfig); // Assert _logger.ReceivedLogContaining(LogLevel.Information, "Test notification sent successfully"); } [Fact] public async Task SendTestNotificationAsync_ProviderFails_ThrowsException() { // Arrange var providerConfig = CreateProviderConfig("FailingProvider"); var provider = Substitute.For(); provider.Name.Returns("FailingProvider"); provider.SendNotificationAsync(Arg.Any()) .ThrowsAsync(new Exception("Test notification failed")); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act & Assert await Should.ThrowAsync(() => _service.SendTestNotificationAsync(providerConfig)); } [Fact] public async Task SendTestNotificationAsync_ProviderFails_LogsError() { // Arrange var providerConfig = CreateProviderConfig("FailingProvider"); var provider = Substitute.For(); provider.Name.Returns("FailingProvider"); provider.SendNotificationAsync(Arg.Any()) .ThrowsAsync(new Exception("Test notification failed")); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act try { await _service.SendTestNotificationAsync(providerConfig); } catch { // Expected } // Assert _logger.ReceivedLogContaining(LogLevel.Error, "Failed to send test notification"); } [Fact] public async Task SendTestNotificationAsync_IncludesProviderTypeInData() { // Arrange var providerConfig = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "TestNtfyProvider", Type = NotificationProviderType.Ntfy, IsEnabled = true }; var provider = Substitute.For(); provider.Name.Returns("TestNtfyProvider"); _providerFactory.CreateProvider(providerConfig) .Returns(provider); // Act await _service.SendTestNotificationAsync(providerConfig); // Assert await provider.Received(1).SendNotificationAsync(Arg.Is(c => c.Data["Provider type"] == "Ntfy" )); } #endregion #region Helper Methods private static NotificationContext CreateTestContext() { return new NotificationContext { EventType = NotificationEventType.QueueItemDeleted, Title = "Test Title", Description = "Test Description", Severity = EventSeverity.Information, Data = new Dictionary { ["Key1"] = "Value1", ["Key2"] = "Value2" } }; } private static NotificationProviderDto CreateProviderConfig(string name) { return new NotificationProviderDto { Id = Guid.NewGuid(), Name = name, Type = NotificationProviderType.Apprise, IsEnabled = true }; } #endregion }