using System.Reflection; using adas_core.Application.Services.Caching; using adas_core.Domain.Models.AppSettings; using Microsoft.Extensions.Logging; using Moq; using Newtonsoft.Json; using StackExchange.Redis; using Options = Microsoft.Extensions.Options.Options; namespace adas_core.Test.Services; [TestFixture] public class RedisServiceTest { private record TestModel(string Name); private Mock _mockDb = null!; private LockManagerService _lockMgr = null!; [SetUp] public void SetUp() { _mockDb = new Mock(); _lockMgr = new LockManagerService( new Mock>().Object, new InMemoryLockProvider()); } private RedisService CreateSut(CacheSettings? settings = null, bool redisAvailable = true) { var sut = new RedisService( Options.Create(settings ?? new CacheSettings()), new Mock>().Object, _lockMgr); SetField(sut, "_database", _mockDb.Object); SetField(sut, "_isRedisAvailable", redisAvailable); return sut; } private static void SetField(object target, string name, object? value) => typeof(RedisService) .GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)! .SetValue(target, value); private void SetupStringSetAsync() => _mockDb .Setup(db => db.StringSetAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(true); private void SetupKeyExpire() => _mockDb .Setup(db => db.KeyExpire( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(true); #region TC-38 [Test] public async Task GetOrSetObjectAsync_ExecutesFactory_WhenRedisUnavailable() { var sut = CreateSut(redisAvailable: false); var factoryInvoked = false; var result = await sut.GetOrSetObjectAsync( "patients:latestObs:abc", () => { factoryInvoked = true; return Task.FromResult("result"); }); Assert.That(factoryInvoked, Is.True); Assert.That(result, Is.EqualTo("result")); _mockDb.Verify(db => db.StringGetAsync(It.IsAny(), It.IsAny()), Times.Never); _mockDb.Verify(db => db.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region TC-39 [Test] public async Task GetOrSetObjectAsync_ReturnsCachedObject_WhenRedisHit() { var cached = new TestModel("cached"); _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(cached)); SetupKeyExpire(); var sut = CreateSut(); var factoryInvoked = false; var result = await sut.GetOrSetObjectAsync( "patients:latestObs:abc", () => { factoryInvoked = true; return Task.FromResult(new TestModel("fresh")); }); Assert.That(factoryInvoked, Is.False); Assert.That(result?.Name, Is.EqualTo("cached")); _mockDb.Verify(db => db.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region TC-40 [Test] public async Task GetObjectAsync_CallsKeyExpire_WhenUpdateExpirationIsTrue() { var settings = new CacheSettings { Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 300 } } }; _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(new TestModel("hit"))); SetupKeyExpire(); var sut = CreateSut(settings); await sut.GetObjectAsync("patients:latestObs:abc", updateExpiration: true); _mockDb.Verify( db => db.KeyExpire( It.IsAny(), It.Is(t => t == TimeSpan.FromSeconds(300)), It.IsAny(), It.IsAny()), Times.Once); } [Test] public async Task GetObjectAsync_DoesNotCallKeyExpire_WhenUpdateExpirationIsFalse() { _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(new TestModel("hit"))); var sut = CreateSut(); await sut.GetObjectAsync("patients:latestObs:abc", updateExpiration: false); _mockDb.Verify( db => db.KeyExpire(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region TC-41 [Test] public async Task GetOrSetObjectAsync_InvokesFactoryAndPersists_WhenCacheMiss() { var settings = new CacheSettings { Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 300, PatientObservationsSeconds = 300} } }; _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(RedisValue.Null); SetupStringSetAsync(); var sut = CreateSut(settings); var expected = new TestModel("fresh"); var factoryInvoked = false; var result = await sut.GetOrSetObjectAsync( "patients:latestObs:abc", () => { factoryInvoked = true; return Task.FromResult(expected); }); var expectedJson = JsonConvert.SerializeObject(expected); Assert.That(factoryInvoked, Is.True); Assert.That(result?.Name, Is.EqualTo("fresh")); _mockDb.Verify( db => db.StringSetAsync( It.IsAny(), It.IsAny(), It.Is(t => t == TimeSpan.FromSeconds(300)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); It.Is(v => v.Equals(expectedJson)); } #endregion #region TC-42 [Test] public async Task GetOrSetObjectAsync_DoesNotPersist_WhenFactoryReturnsNull() { _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(RedisValue.Null); var sut = CreateSut(); var result = await sut.GetOrSetObjectAsync( "patients:latestObs:abc", () => Task.FromResult(null)); Assert.That(result, Is.Null); _mockDb.Verify( db => db.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region TC-43 [Test] public async Task GetObjectAsync_ReturnsObject_AndSkipsKeyExpire_WhenUpdateExpirationFalse() { var expected = new TestModel("data"); _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(expected)); var sut = CreateSut(); var result = await sut.GetObjectAsync("patients:latestObs:abc", updateExpiration: false); Assert.That(result?.Name, Is.EqualTo("data")); _mockDb.Verify( db => db.KeyExpire(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } #endregion #region TC-44 [Test] public async Task SetObjectAsync_SerializesToJson_AndPersistsWithEntityTtl() { var settings = new CacheSettings { Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 600 } } }; SetupStringSetAsync(); var sut = CreateSut(settings); var obj = new TestModel("save-me"); var expectedJson = JsonConvert.SerializeObject(obj); await sut.SetObjectAsync("patients:latestObs:abc", obj, null, true); _mockDb.Verify( db => db.StringSetAsync( It.IsAny(), It.IsAny(), It.Is(t => t == TimeSpan.FromSeconds(600)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); It.Is(v => v.Equals(expectedJson)); } #endregion }