mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-04-23 10:42:37 -04:00
445 lines
13 KiB
C#
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();
|
|
}
|
|
}
|