Files
Cleanuparr/code/backend/Cleanuparr.Infrastructure.Tests/Features/MalwareBlocker/BlocklistProviderTests.cs
T

235 lines
8.1 KiB
C#

using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Helpers;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Shouldly;
using Xunit;
namespace Cleanuparr.Infrastructure.Tests.Features.MalwareBlocker;
public class BlocklistProviderTests : IDisposable
{
private readonly IMemoryCache _cache;
private readonly ILogger<BlocklistProvider> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly BlocklistProvider _provider;
public BlocklistProviderTests()
{
_cache = new MemoryCache(new MemoryCacheOptions());
_logger = Substitute.For<ILogger<BlocklistProvider>>();
_scopeFactory = Substitute.For<IServiceScopeFactory>();
_provider = new BlocklistProvider(_logger, _scopeFactory, _cache, TimeProvider.System);
}
public void Dispose()
{
_cache.Dispose();
}
[Fact]
public async Task LoadBlocklistsAsync_ReloadsOnlyOnceTheLoadIntervalHasElapsedOnTheInjectedClock()
{
string blocklistPath = Path.Combine(Path.GetTempPath(), $"cleanuparr-blocklist-{Guid.NewGuid():N}.txt");
await File.WriteAllTextAsync(blocklistPath, "first-pattern");
using SqliteConnection connection = new("DataSource=:memory:");
connection.Open();
DbContextOptions<DataContext> options = new DbContextOptionsBuilder<DataContext>()
.UseSqlite(connection)
.Options;
await using (DataContext seedContext = new(options))
{
await seedContext.Database.EnsureCreatedAsync();
seedContext.ContentBlockerConfigs.Add(new ContentBlockerConfig
{
Id = Guid.NewGuid(),
Enabled = true,
IgnoredDownloads = [],
Sonarr = new BlocklistSettings { Enabled = true, BlocklistPath = blocklistPath },
Radarr = new BlocklistSettings { Enabled = false },
Lidarr = new BlocklistSettings { Enabled = false },
Readarr = new BlocklistSettings { Enabled = false },
Whisparr = new BlocklistSettings { Enabled = false },
Sportarr = new BlocklistSettings { Enabled = false },
LazyLibrarian = new BlocklistSettings { Enabled = false },
});
await seedContext.SaveChangesAsync();
}
ServiceCollection services = new();
services.AddScoped(_ => new DataContext(options));
services.AddScoped(_ => new FileReader(Substitute.For<IHttpClientFactory>()));
await using ServiceProvider serviceProvider = services.BuildServiceProvider();
using MemoryCache cache = new(new MemoryCacheOptions());
FakeTimeProvider timeProvider = new(new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero));
BlocklistProvider provider = new(
_logger,
serviceProvider.GetRequiredService<IServiceScopeFactory>(),
cache,
timeProvider);
try
{
await provider.LoadBlocklistsAsync();
provider.GetPatterns(InstanceType.Sonarr).ShouldContain("first-pattern");
// Same settings, same path: the load interval has not elapsed, so the new content stays unread.
await File.WriteAllTextAsync(blocklistPath, "second-pattern");
await provider.LoadBlocklistsAsync();
provider.GetPatterns(InstanceType.Sonarr).ShouldContain("first-pattern");
provider.GetPatterns(InstanceType.Sonarr).ShouldNotContain("second-pattern");
// An absolute path parses as a file:// URI, so it takes the 4h interval.
timeProvider.Advance(TimeSpan.FromHours(5));
await provider.LoadBlocklistsAsync();
provider.GetPatterns(InstanceType.Sonarr).ShouldContain("second-pattern");
provider.GetPatterns(InstanceType.Sonarr).ShouldNotContain("first-pattern");
}
finally
{
File.Delete(blocklistPath);
}
}
[Fact]
public void GetBlocklistType_NotInCache_ReturnsDefaultBlacklist()
{
// Act
var result = _provider.GetBlocklistType(InstanceType.Sonarr);
// Assert
result.ShouldBe(BlocklistType.Blacklist);
}
[Theory]
[InlineData(InstanceType.Sonarr)]
[InlineData(InstanceType.Radarr)]
[InlineData(InstanceType.Lidarr)]
[InlineData(InstanceType.Readarr)]
[InlineData(InstanceType.Whisparr)]
public void GetBlocklistType_InCache_ReturnsCachedValue(InstanceType instanceType)
{
// Arrange
_cache.Set(CacheKeys.BlocklistType(instanceType), BlocklistType.Whitelist);
// Act
var result = _provider.GetBlocklistType(instanceType);
// Assert
result.ShouldBe(BlocklistType.Whitelist);
}
[Fact]
public void GetPatterns_NotInCache_ReturnsEmptyBag()
{
// Act
var result = _provider.GetPatterns(InstanceType.Sonarr);
// Assert
result.ShouldNotBeNull();
result.ShouldBeEmpty();
}
[Fact]
public void GetPatterns_InCache_ReturnsCachedPatterns()
{
// Arrange
var patterns = new ConcurrentBag<string> { "*.exe", "*.dll", "malware*" };
_cache.Set(CacheKeys.BlocklistPatterns(InstanceType.Radarr), patterns);
// Act
var result = _provider.GetPatterns(InstanceType.Radarr);
// Assert
result.Count.ShouldBe(3);
result.ShouldContain("*.exe");
result.ShouldContain("*.dll");
result.ShouldContain("malware*");
}
[Fact]
public void GetRegexes_NotInCache_ReturnsEmptyBag()
{
// Act
var result = _provider.GetRegexes(InstanceType.Lidarr);
// Assert
result.ShouldNotBeNull();
result.ShouldBeEmpty();
}
[Fact]
public void GetRegexes_InCache_ReturnsCachedRegexes()
{
// Arrange
var regexes = new ConcurrentBag<Regex>
{
new Regex(@"^\d+$"),
new Regex(@"test\d+\.exe")
};
_cache.Set(CacheKeys.BlocklistRegexes(InstanceType.Readarr), regexes);
// Act
var result = _provider.GetRegexes(InstanceType.Readarr);
// Assert
result.Count.ShouldBe(2);
}
[Theory]
[InlineData(InstanceType.Sonarr)]
[InlineData(InstanceType.Radarr)]
[InlineData(InstanceType.Lidarr)]
[InlineData(InstanceType.Readarr)]
[InlineData(InstanceType.Whisparr)]
public void GetPatterns_DifferentInstanceTypes_UsesCorrectCacheKey(InstanceType instanceType)
{
// Arrange - set patterns for each instance type differently
var patterns = new ConcurrentBag<string> { $"pattern_for_{instanceType}" };
_cache.Set(CacheKeys.BlocklistPatterns(instanceType), patterns);
// Act
var result = _provider.GetPatterns(instanceType);
// Assert
result.ShouldContain($"pattern_for_{instanceType}");
}
[Fact]
public void GetPatterns_DifferentInstanceTypes_ReturnsDifferentPatterns()
{
// Arrange
var sonarrPatterns = new ConcurrentBag<string> { "sonarr_pattern" };
var radarrPatterns = new ConcurrentBag<string> { "radarr_pattern" };
_cache.Set(CacheKeys.BlocklistPatterns(InstanceType.Sonarr), sonarrPatterns);
_cache.Set(CacheKeys.BlocklistPatterns(InstanceType.Radarr), radarrPatterns);
// Act
var sonarrResult = _provider.GetPatterns(InstanceType.Sonarr);
var radarrResult = _provider.GetPatterns(InstanceType.Radarr);
// Assert
sonarrResult.ShouldContain("sonarr_pattern");
sonarrResult.ShouldNotContain("radarr_pattern");
radarrResult.ShouldContain("radarr_pattern");
radarrResult.ShouldNotContain("sonarr_pattern");
}
}