using FluentAssertions; using Jellyfin.Plugin.SRFPlay.Api.Models; using Jellyfin.Plugin.SRFPlay.Services; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace Jellyfin.Plugin.SRFPlay.Tests.UnitTests; /// /// Unit tests for MetadataCache. /// public class MetadataCacheTests : IDisposable { private readonly Mock> _loggerMock; private readonly MetadataCache _cache; public MetadataCacheTests() { _loggerMock = new Mock>(); _cache = new MetadataCache(_loggerMock.Object); } [Fact] public void SetMediaComposition_And_GetMediaComposition_ReturnsCorrectValue() { // Arrange const string urn = "urn:srf:video:test-id"; var mediaComposition = new MediaComposition { Episode = new Episode { Id = "test-episode", Title = "Test" } }; const int cacheDurationMinutes = 10; // Act _cache.SetMediaComposition(urn, mediaComposition); var result = _cache.GetMediaComposition(urn, cacheDurationMinutes); // Assert result.Should().NotBeNull(); result.Should().Be(mediaComposition); } [Fact] public void GetMediaComposition_NonExistentKey_ReturnsNull() { // Act var result = _cache.GetMediaComposition("non-existent-urn", 10); // Assert result.Should().BeNull(); } [Fact] public void GetMediaComposition_ExpiredEntry_ReturnsNull() { // Arrange const string urn = "urn:srf:video:test-id"; var mediaComposition = new MediaComposition { Episode = new Episode { Id = "test-episode" } }; // Set with cache _cache.SetMediaComposition(urn, mediaComposition); // Wait a tiny bit to ensure expiration System.Threading.Thread.Sleep(10); // Act - Try to get with 0 minute cache duration (immediate expiration) var result = _cache.GetMediaComposition(urn, 0); // Assert - Should be null because it's expired result.Should().BeNull(); } [Fact] public void RemoveMediaComposition_ExistingUrn_RemovesValue() { // Arrange const string urn = "urn:srf:video:test-id"; var mediaComposition = new MediaComposition { Episode = new Episode { Id = "test-episode" } }; _cache.SetMediaComposition(urn, mediaComposition); // Act _cache.RemoveMediaComposition(urn); var result = _cache.GetMediaComposition(urn, 10); // Assert result.Should().BeNull(); } [Fact] public void Clear_RemovesAllValues() { // Arrange var mc1 = new MediaComposition { Episode = new Episode { Id = "episode1" } }; var mc2 = new MediaComposition { Episode = new Episode { Id = "episode2" } }; _cache.SetMediaComposition("urn1", mc1); _cache.SetMediaComposition("urn2", mc2); // Act _cache.Clear(); var result1 = _cache.GetMediaComposition("urn1", 10); var result2 = _cache.GetMediaComposition("urn2", 10); // Assert result1.Should().BeNull(); result2.Should().BeNull(); } [Fact] public void GetStatistics_ReturnsCorrectCount() { // Arrange var mc1 = new MediaComposition { Episode = new Episode { Id = "episode1" } }; var mc2 = new MediaComposition { Episode = new Episode { Id = "episode2" } }; _cache.SetMediaComposition("urn1", mc1); _cache.SetMediaComposition("urn2", mc2); // Act var (count, sizeEstimate) = _cache.GetStatistics(); // Assert count.Should().Be(2); sizeEstimate.Should().BeGreaterThan(0); } [Fact] public void ConcurrentAccess_DoesNotThrow() { // Arrange var tasks = new List(); // Act - Perform concurrent operations for (int i = 0; i < 100; i++) { var index = i; tasks.Add(Task.Run(() => { var mc = new MediaComposition { Episode = new Episode { Id = $"episode-{index}" } }; _cache.SetMediaComposition($"urn-{index}", mc); _cache.GetMediaComposition($"urn-{index}", 10); if (index % 2 == 0) { _cache.RemoveMediaComposition($"urn-{index}"); } })); } // Assert - Should not throw var action = async () => await Task.WhenAll(tasks); action.Should().NotThrowAsync(); } public void Dispose() { _cache?.Dispose(); } }