Files
allstarr/allstarr.Tests/RedisCacheServiceTests.cs
T

445 lines
13 KiB
C#

using Xunit;
using Moq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text.Json;
using allstarr.Models.Domain;
using allstarr.Models.Spotify;
using allstarr.Services.Common;
using allstarr.Models.Settings;
namespace allstarr.Tests;
public class RedisCacheServiceTests
{
private readonly Mock<ILogger<RedisCacheService>> _mockLogger;
private readonly IOptions<RedisSettings> _settings;
public RedisCacheServiceTests()
{
_mockLogger = new Mock<ILogger<RedisCacheService>>();
_settings = Options.Create(new RedisSettings
{
Enabled = false, // Disabled for unit tests to avoid requiring actual Redis
ConnectionString = "localhost:6379"
});
}
private RedisCacheService CreateService(IMemoryCache? memoryCache = null, IOptions<RedisSettings>? settings = null)
{
return new RedisCacheService(
settings ?? _settings,
_mockLogger.Object,
memoryCache ?? new MemoryCache(new MemoryCacheOptions()));
}
[Fact]
public void Constructor_InitializesWithSettings()
{
// Act
var service = CreateService();
// Assert
Assert.NotNull(service);
Assert.False(service.IsEnabled); // Should be disabled in tests
}
[Fact]
public void Constructor_WithEnabledSettings_AttemptsConnection()
{
// Arrange
var enabledSettings = Options.Create(new RedisSettings
{
Enabled = true,
ConnectionString = "localhost:6379"
});
// Act - Constructor will try to connect but should handle failure gracefully
var service = CreateService(settings: enabledSettings);
// Assert - Service should be created even if connection fails
Assert.NotNull(service);
}
[Fact]
public async Task GetStringAsync_WhenDisabled_ReturnsNull()
{
// Arrange
var service = CreateService();
// Act
var result = await service.GetStringAsync("test:key");
// Assert
Assert.Null(result);
}
[Fact]
public async Task GetAsync_WhenDisabled_ReturnsNull()
{
// Arrange
var service = CreateService();
// Act
var result = await service.GetAsync<TestObject>("test:key");
// Assert
Assert.Null(result);
}
[Fact]
public async Task SetStringAsync_WhenDisabled_ReturnsFalse()
{
// Arrange
var service = CreateService();
// Act
var result = await service.SetStringAsync("test:key", "test value");
// Assert
Assert.False(result);
}
[Fact]
public async Task SetAsync_WhenDisabled_ReturnsFalse()
{
// Arrange
var service = CreateService();
var testObj = new TestObject { Id = 1, Name = "Test" };
// Act
var result = await service.SetAsync("test:key", testObj);
// Assert
Assert.False(result);
}
[Fact]
public async Task DeleteAsync_WhenDisabled_ReturnsFalse()
{
// Arrange
var service = CreateService();
// Act
var result = await service.DeleteAsync("test:key");
// Assert
Assert.False(result);
}
[Fact]
public async Task ExistsAsync_WhenDisabled_ReturnsFalse()
{
// Arrange
var service = CreateService();
// Act
var result = await service.ExistsAsync("test:key");
// Assert
Assert.False(result);
}
[Fact]
public async Task DeleteByPatternAsync_WhenDisabled_ReturnsZero()
{
// Arrange
var service = CreateService();
// Act
var result = await service.DeleteByPatternAsync("test:*");
// Assert
Assert.Equal(0, result);
}
[Fact]
public async Task SetStringAsync_WithExpiry_AcceptsTimeSpan()
{
// Arrange
var service = CreateService();
var expiry = TimeSpan.FromHours(1);
// Act
var result = await service.SetStringAsync("test:key", "value", expiry);
// Assert - Should return false when disabled, but not throw
Assert.False(result);
}
[Fact]
public async Task SetAsync_WithExpiry_AcceptsTimeSpan()
{
// Arrange
var service = CreateService();
var testObj = new TestObject { Id = 1, Name = "Test" };
var expiry = TimeSpan.FromDays(30);
// Act
var result = await service.SetAsync("test:key", testObj, expiry);
// Assert - Should return false when disabled, but not throw
Assert.False(result);
}
[Fact]
public void IsEnabled_ReflectsSettings()
{
// Arrange
var disabledService = CreateService();
var enabledSettings = Options.Create(new RedisSettings
{
Enabled = true,
ConnectionString = "localhost:6379"
});
var enabledService = CreateService(settings: enabledSettings);
// Assert
Assert.False(disabledService.IsEnabled);
// enabledService.IsEnabled may be false if connection fails, which is expected
}
[Fact]
public async Task GetAsync_DeserializesComplexObjects()
{
// Arrange
var service = CreateService();
// Act
var result = await service.GetAsync<ComplexTestObject>("test:complex");
// Assert
Assert.Null(result); // Null when disabled
}
[Fact]
public async Task SetAsync_SerializesComplexObjects()
{
// Arrange
var service = CreateService();
var complexObj = new ComplexTestObject
{
Id = 1,
Name = "Test",
Items = new System.Collections.Generic.List<string> { "Item1", "Item2" },
Metadata = new System.Collections.Generic.Dictionary<string, string>
{
{ "Key1", "Value1" },
{ "Key2", "Value2" }
}
};
// Act
var result = await service.SetAsync("test:complex", complexObj, TimeSpan.FromHours(1));
// Assert
Assert.False(result); // False when disabled
}
[Fact]
public void ConnectionString_IsConfigurable()
{
// Arrange
var customSettings = Options.Create(new RedisSettings
{
Enabled = false,
ConnectionString = "redis-server:6380,password=secret,ssl=true"
});
// Act
var service = CreateService(settings: customSettings);
// Assert
Assert.NotNull(service);
}
[Fact]
public async Task SetStringAsync_WhenDisabled_CachesValueInMemory()
{
var service = CreateService();
var setResult = await service.SetStringAsync("test:key", "test value");
var cachedValue = await service.GetStringAsync("test:key");
Assert.False(setResult);
Assert.Equal("test value", cachedValue);
}
[Fact]
public async Task SetAsync_WhenDisabled_CachesSerializedObjectInMemory()
{
var service = CreateService();
var expected = new TestObject { Id = 42, Name = "Tiered" };
var setResult = await service.SetAsync("test:object", expected);
var cachedValue = await service.GetAsync<TestObject>("test:object");
Assert.False(setResult);
Assert.NotNull(cachedValue);
Assert.Equal(expected.Id, cachedValue.Id);
Assert.Equal(expected.Name, cachedValue.Name);
}
[Fact]
public async Task ExistsAsync_WhenValueOnlyExistsInMemory_ReturnsTrue()
{
var service = CreateService();
await service.SetStringAsync("test:key", "test value");
var exists = await service.ExistsAsync("test:key");
Assert.True(exists);
}
[Fact]
public async Task DeleteAsync_WhenValueOnlyExistsInMemory_EvictsEntry()
{
var service = CreateService();
await service.SetStringAsync("test:key", "test value");
var deleted = await service.DeleteAsync("test:key");
var cachedValue = await service.GetStringAsync("test:key");
Assert.False(deleted);
Assert.Null(cachedValue);
}
[Fact]
public async Task DeleteByPatternAsync_WhenValuesOnlyExistInMemory_RemovesMatchingEntries()
{
var service = CreateService();
await service.SetStringAsync("search:one", "1");
await service.SetStringAsync("search:two", "2");
await service.SetStringAsync("other:one", "3");
var deletedCount = await service.DeleteByPatternAsync("search:*");
Assert.Equal(2, deletedCount);
Assert.Null(await service.GetStringAsync("search:one"));
Assert.Null(await service.GetStringAsync("search:two"));
Assert.Equal("3", await service.GetStringAsync("other:one"));
}
[Fact]
public async Task SetStringAsync_ImageKeysDoNotUseMemoryCache()
{
var service = CreateService();
await service.SetStringAsync("image:test:key", "binary-ish");
var cachedValue = await service.GetStringAsync("image:test:key");
var exists = await service.ExistsAsync("image:test:key");
Assert.Null(cachedValue);
Assert.False(exists);
}
[Fact]
public async Task SetAsync_WhenSongContainsRawJellyfinMetadata_CachesSerializedValueInMemory()
{
var service = CreateService();
var songs = new List<Song> { CreateLocalSongWithRawJellyfinMetadata() };
var setResult = await service.SetAsync("test:songs:raw-jellyfin", songs);
var cachedValue = await service.GetAsync<List<Song>>("test:songs:raw-jellyfin");
Assert.False(setResult);
Assert.NotNull(cachedValue);
var roundTrippedSong = Assert.Single(cachedValue!);
Assert.True(JellyfinItemSnapshotHelper.TryGetClonedRawItemSnapshot(roundTrippedSong, out var rawItem));
Assert.Equal("song-1", ((JsonElement)rawItem["Id"]!).GetString());
var mediaSources = Assert.IsType<JsonElement>(roundTrippedSong.JellyfinMetadata!["MediaSources"]);
Assert.Equal(JsonValueKind.Array, mediaSources.ValueKind);
Assert.Equal(2234068710L, mediaSources[0].GetProperty("RunTimeTicks").GetInt64());
}
[Fact]
public async Task SetAsync_WhenMatchedTracksContainRawJellyfinMetadata_CachesSerializedValueInMemory()
{
var service = CreateService();
var matchedTracks = new List<MatchedTrack>
{
new()
{
Position = 0,
SpotifyId = "spotify-1",
SpotifyTitle = "Track",
SpotifyArtist = "Artist",
MatchType = "fuzzy",
MatchedSong = CreateLocalSongWithRawJellyfinMetadata()
}
};
var setResult = await service.SetAsync("test:matched:raw-jellyfin", matchedTracks);
var cachedValue = await service.GetAsync<List<MatchedTrack>>("test:matched:raw-jellyfin");
Assert.False(setResult);
Assert.NotNull(cachedValue);
var roundTrippedMatch = Assert.Single(cachedValue!);
Assert.True(
JellyfinItemSnapshotHelper.TryGetClonedRawItemSnapshot(roundTrippedMatch.MatchedSong, out var rawItem));
Assert.Equal("song-1", ((JsonElement)rawItem["Id"]!).GetString());
var mediaSources =
Assert.IsType<JsonElement>(roundTrippedMatch.MatchedSong.JellyfinMetadata!["MediaSources"]);
Assert.Equal(JsonValueKind.Array, mediaSources.ValueKind);
Assert.Equal("song-1", mediaSources[0].GetProperty("Id").GetString());
}
private static Song CreateLocalSongWithRawJellyfinMetadata()
{
var song = new Song
{
Id = "song-1",
Title = "Track",
Artist = "Artist",
Album = "Album",
IsLocal = true
};
using var doc = JsonDocument.Parse("""
{
"Id": "song-1",
"ServerId": "c17d351d3af24c678a6d8049c212d522",
"RunTimeTicks": 2234068710,
"MediaSources": [
{
"Id": "song-1",
"RunTimeTicks": 2234068710
}
]
}
""");
JellyfinItemSnapshotHelper.StoreRawItemSnapshot(song, doc.RootElement);
song.JellyfinMetadata ??= new Dictionary<string, object?>();
song.JellyfinMetadata["MediaSources"] =
JsonSerializer.Deserialize<object>(doc.RootElement.GetProperty("MediaSources").GetRawText());
return song;
}
private class TestObject
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
private class ComplexTestObject
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public System.Collections.Generic.List<string> Items { get; set; } = new();
public System.Collections.Generic.Dictionary<string, string> Metadata { get; set; } = new();
}
}