using adas_core.Application.Services.Caching; using adas_core.Application.Services.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.GroupedObservations; using adas_core.Domain.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using Moq; namespace adas_core.Test.Services; /// /// Provides a fake implementation of the interface, typically used as a test double or non-functional placeholder. /// public class FakeLockProvider : ILockProvider { /// /// Asynchronously attempts to acquire a resource identified by the specified key within the given timeout. /// This implementation always reports a successful acquisition by returning true. /// /// The identifier of the resource to acquire. /// The maximum time to wait for the resource to become available. /// A that always completes with true, indicating the resource was acquired. public Task AcquireAsync(string key, TimeSpan timeout) => Task.FromResult(true); /// /// Releases the resource associated with the specified key. This implementation completes immediately without performing any additional operation. /// /// The identifier of the resource to release. public Task ReleaseAsync(string key) => Task.CompletedTask; } /// /// Represents a fake implementation of , typically used as a test double to provide controlled or simplified behavior in unit tests. /// /// /// Inherits all members from and is intended to be used in place of the real service when actual locking functionality is not required. /// public class FakeLockManagerService : LockManagerService { public FakeLockManagerService() : base( Mock.Of>(), new FakeLockProvider()) { } } /// /// Represents a fake or test double implementation of that also implements , typically used to simulate Redis caching behavior in testing scenarios. /// /// /// This class combines the inheritance of with the contract of , allowing it to stand in for a real Redis-backed cache during unit tests or development. /// public class FakeRedisService : RedisService, ICacheService { public bool WasCalled { get; private set; } public string? LastKey { get; private set; } public FakeRedisService() : base( Options.Create(new CacheSettings()), Mock.Of>(), new FakeLockManagerService()) { } /// /// Retrieves a cached object associated with the specified key, or creates and stores one using the provided factory if no cached entry exists. /// This implementation bypasses caching and directly invokes the factory, recording the invocation for verification purposes while ignoring the TTL and cancellation token. /// /// The cache key used to identify the stored object. /// A delegate that asynchronously produces the object to cache when no entry exists for the specified key. /// An optional time-to-live duration for the cached entry. Not used in this implementation. /// A token to observe for cancellation requests. Not used in this implementation. /// A task that represents the asynchronous operation, containing the object produced by the factory. Task ICacheService.GetOrSetObjectAsync( string key, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; LastKey = key; return factory(); } /// /// Retrieves or sets a cached object associated with the specified grouped field and patient identifier. /// Marks the call as executed via the WasCalled flag and returns the value produced by the supplied factory delegate. /// /// The grouped field used to identify the cached object. /// The identifier of the patient associated with the cached object. /// The asynchronous factory delegate invoked to produce the value when no cached entry exists. /// An optional time-to-live duration for the cached entry. /// The token used to cancel the asynchronous operation. /// The value of type produced by the delegate. Task ICacheService.GetOrSetObjectAsync( GroupedField groupedField, ObjectId patientId, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; return factory(); } } /// /// Represents a fake implementation of that also implements the interface, typically used for testing or stubbing scenarios. /// /// /// This class combines inheritance from the concrete base class with the contract, allowing it to be used wherever an is required. /// public class FakeCacheService : CacheService, ICacheService { public bool WasCalled { get; private set; } public string? LastKey { get; private set; } public FakeCacheService() : base(new FakeLockManagerService()) { } Task ICacheService.GetOrSetObjectAsync( string key, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; LastKey = key; return factory(); } Task ICacheService.GetOrSetObjectAsync( GroupedField groupedField, ObjectId patientId, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; return factory(); } } /// /// Represents a fake implementation of the cache service, used for testing or scenarios where a no-op cache behavior is required. /// /// /// This class inherits from and implements the interface, providing a non-functional cache suitable for unit tests or environments where caching should be bypassed. /// public class FakeNoCacheService : NoCacheService, ICacheService { public bool WasCalled { get; private set; } public string? LastKey { get; private set; } Task ICacheService.GetOrSetObjectAsync( string key, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; LastKey = key; return factory(); } Task ICacheService.GetOrSetObjectAsync( GroupedField groupedField, ObjectId patientId, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; return factory(); } } [TestFixture] public class CacheDispatcherTest { private FakeRedisService _redisServiceFake = null!; private FakeCacheService _memoryServiceFake = null!; private FakeNoCacheService _noopServiceFake = null!; private CacheSettings _cacheSettings = null!; private CacheDispatcher _cacheDispatcher = null!; /// /// Initializes the test environment by instantiating fake implementations of the Redis, in-memory, and no-op cache services, along with default , and constructs a under test using these dependencies. /// [SetUp] public void SetUp() { _redisServiceFake = new FakeRedisService(); _memoryServiceFake = new FakeCacheService(); _noopServiceFake = new FakeNoCacheService(); _cacheSettings = new CacheSettings(); _cacheDispatcher = new CacheDispatcher( _redisServiceFake, _memoryServiceFake, _noopServiceFake, _cacheSettings); } #region TC-23 [Test] public async Task GetOrSetObjectAsync_UsesRedisService_WhenPatientsModeIsRedis() { // Arrange var key = "patients:latestObs:abc"; var expectedResult = new { Name = "TestPatient" }; Func> factory = () => Task.FromResult(expectedResult); _cacheSettings.Patients = CacheEnum.Mode.Redis; // Act var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); // Assert Assert.That(_redisServiceFake.WasCalled, Is.True, "RedisService debería haber sido invocado"); Assert.That(_redisServiceFake.LastKey, Is.EqualTo(key)); Assert.That(_memoryServiceFake.WasCalled, Is.False, "CacheService NO debería haber sido invocado"); Assert.That(_noopServiceFake.WasCalled, Is.False, "NoCacheService NO debería haber sido invocado"); } #endregion #region TC-24 [Test] public async Task GetOrSetObjectAsync_UsesCacheService_WhenAppointmentsModeIsCache() { // Arrange var key = "appointments:patient:id:20260224"; var expectedResult = new { Name = "TestAppointment" }; Func> factory = () => Task.FromResult(expectedResult); _cacheSettings.Appointments = CacheEnum.Mode.Cache; // Act var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); // Assert Assert.That(_memoryServiceFake.WasCalled, Is.True, "CacheService debería haber sido invocado"); Assert.That(_memoryServiceFake.LastKey, Is.EqualTo(key)); Assert.That(_redisServiceFake.WasCalled, Is.False, "RedisService NO debería haber sido invocado"); Assert.That(_noopServiceFake.WasCalled, Is.False, "NoCacheService NO debería haber sido invocado"); } #endregion #region TC-25 [Test] public async Task GetOrSetObjectAsync_UsesNoCacheService_WhenPumpObservationsModeIsNone() { // Arrange var key = "pumpObs:latest:abc:10"; var expectedResult = new { Name = "TestPump" }; Func> factory = () => Task.FromResult(expectedResult); _cacheSettings.PumpObservations = CacheEnum.Mode.None; // Act var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); // Assert Assert.That(_noopServiceFake.WasCalled, Is.True, "NoCacheService debería haber sido invocado"); Assert.That(_noopServiceFake.LastKey, Is.EqualTo(key)); Assert.That(_redisServiceFake.WasCalled, Is.False, "RedisService NO debería haber sido invocado"); Assert.That(_memoryServiceFake.WasCalled, Is.False, "CacheService NO debería haber sido invocado"); } #endregion #region TC-26 [Test] public async Task GetOrSetObjectAsync_UsesCacheService_WhenKeyIsUnrecognized() { // Arrange var key = "unknown:prefix:key"; var expectedResult = new { Name = "TestUnknown" }; Func> factory = () => Task.FromResult(expectedResult); // Act var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); // Assert - Unknown keys fallback to InMemory (Cache) Assert.That(_memoryServiceFake.WasCalled, Is.True, "CacheService debería haber sido invocado como fallback"); Assert.That(_memoryServiceFake.LastKey, Is.EqualTo(key)); Assert.That(_redisServiceFake.WasCalled, Is.False, "RedisService NO debería haber sido invocado"); Assert.That(_noopServiceFake.WasCalled, Is.False, "NoCacheService NO debería haber sido invocado"); } /// /// Verifies that returns when the provided cache key does not start with any recognized prefix. /// [Test] public void Classify_ReturnsUnknown_ForUnrecognizedPrefix() { // Arrange var key = "unknown:prefix:key"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown)); } #endregion }