using Cleanuparr.Domain.Enums; using Cleanuparr.Infrastructure.Events; using Cleanuparr.Infrastructure.Events.Interfaces; using Cleanuparr.Infrastructure.Features.DownloadClient; using Cleanuparr.Infrastructure.Features.DownloadClient.Deluge; using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent; using Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent; using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission; using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent; using Cleanuparr.Infrastructure.Features.Files; using Cleanuparr.Infrastructure.Features.ItemStriker; using Cleanuparr.Infrastructure.Features.MalwareBlocker; using Cleanuparr.Infrastructure.Features.Notifications; using Cleanuparr.Infrastructure.Http; using Cleanuparr.Infrastructure.Hubs; using Cleanuparr.Infrastructure.Interceptors; using Cleanuparr.Infrastructure.Services.Interfaces; using Cleanuparr.Persistence; using Cleanuparr.Persistence.Models.Configuration; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using Xunit; namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient; public class DownloadServiceFactoryTests : IDisposable { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly DownloadServiceFactory _factory; private readonly MemoryCache _memoryCache; public DownloadServiceFactoryTests() { _logger = Substitute.For>(); var services = new ServiceCollection(); // Use real MemoryCache - mocks don't work properly with cache operations _memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); services.AddSingleton(_memoryCache); // Register loggers services.AddSingleton(Substitute.For>()); services.AddSingleton(Substitute.For>()); services.AddSingleton(Substitute.For>()); services.AddSingleton(Substitute.For>()); services.AddSingleton(Substitute.For()); services.AddSingleton(Substitute.For()); services.AddSingleton(Substitute.For()); services.AddSingleton(Substitute.For()); // IDynamicHttpClientProvider must return a real HttpClient for download services var httpClientProvider = Substitute.For(); httpClientProvider.CreateClient(Arg.Any()).Returns(new HttpClient()); services.AddSingleton(httpClientProvider); services.AddSingleton(Substitute.For()); services.AddSingleton(Substitute.For()); services.AddSingleton(Substitute.For()); // UTorrentService needs ILoggerFactory services.AddLogging(); // EventPublisher requires specific constructor arguments var eventsContextOptions = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var eventsContext = new EventsContext(eventsContextOptions); var hubContext = Substitute.For>(); var clients = Substitute.For(); clients.All.Returns(Substitute.For()); hubContext.Clients.Returns(clients); services.AddSingleton(new EventPublisher( eventsContext, hubContext, Substitute.For>(), Substitute.For(), Substitute.For())); // BlocklistProvider requires specific constructor arguments var scopeFactory = Substitute.For(); services.AddSingleton(new BlocklistProvider( Substitute.For>(), scopeFactory, _memoryCache)); _serviceProvider = services.BuildServiceProvider(); _factory = new DownloadServiceFactory(_logger, _serviceProvider); } public void Dispose() { _memoryCache.Dispose(); } #region GetDownloadService Tests [Fact] public void GetDownloadService_QBittorrent_ReturnsQBitService() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.qBittorrent); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); Assert.IsType(service); } [Fact] public void GetDownloadService_Deluge_ReturnsDelugeService() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.Deluge); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); Assert.IsType(service); } [Fact] public void GetDownloadService_Transmission_ReturnsTransmissionService() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.Transmission); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); Assert.IsType(service); } [Fact] public void GetDownloadService_UTorrent_ReturnsUTorrentService() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.uTorrent); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); Assert.IsType(service); } [Fact] public void GetDownloadService_RTorrent_ReturnsRTorrentService() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.rTorrent); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); Assert.IsType(service); } [Fact] public void GetDownloadService_UnsupportedType_ThrowsNotSupportedException() { // Arrange var config = new DownloadClientConfig { Id = Guid.NewGuid(), Name = "Unsupported Client", TypeName = (DownloadClientTypeName)999, // Invalid type Type = DownloadClientType.Torrent, Host = new Uri("http://test.example.com"), Enabled = true }; // Act & Assert var exception = Assert.Throws(() => _factory.GetDownloadService(config)); Assert.Contains("not supported", exception.Message); } [Fact] public void GetDownloadService_DisabledClient_LogsWarningButReturnsService() { // Arrange var config = new DownloadClientConfig { Id = Guid.NewGuid(), Name = "Disabled qBittorrent", TypeName = DownloadClientTypeName.qBittorrent, Type = DownloadClientType.Torrent, Host = new Uri("http://test.example.com"), Enabled = false }; // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); _logger.Received(1).Log( LogLevel.Warning, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>()); } [Fact] public void GetDownloadService_EnabledClient_DoesNotLogWarning() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.qBittorrent); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); _logger.DidNotReceive().Log( LogLevel.Warning, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>()); } [Theory] [InlineData(DownloadClientTypeName.qBittorrent, typeof(QBitService))] [InlineData(DownloadClientTypeName.Deluge, typeof(DelugeService))] [InlineData(DownloadClientTypeName.Transmission, typeof(TransmissionService))] [InlineData(DownloadClientTypeName.uTorrent, typeof(UTorrentService))] [InlineData(DownloadClientTypeName.rTorrent, typeof(RTorrentService))] public void GetDownloadService_AllSupportedTypes_ReturnCorrectServiceType( DownloadClientTypeName typeName, Type expectedServiceType) { // Arrange var config = CreateClientConfig(typeName); // Act var service = _factory.GetDownloadService(config); // Assert Assert.NotNull(service); Assert.IsType(expectedServiceType, service); } [Fact] public void GetDownloadService_ReturnsNewInstanceEachTime() { // Arrange var config = CreateClientConfig(DownloadClientTypeName.qBittorrent); // Act var service1 = _factory.GetDownloadService(config); var service2 = _factory.GetDownloadService(config); // Assert Assert.NotSame(service1, service2); } #endregion #region Helper Methods private static DownloadClientConfig CreateClientConfig(DownloadClientTypeName typeName) { return new DownloadClientConfig { Id = Guid.NewGuid(), Name = $"Test {typeName} Client", TypeName = typeName, Type = DownloadClientType.Torrent, Host = new Uri("http://test.example.com"), Enabled = true }; } #endregion }