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 { /// /// Initializes a new instance of , a test fake of , by forwarding a mocked and a to the base constructor. /// /// 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; } /// /// Initializes a new instance of , a test double for , supplying default , a mocked , and a to the base constructor. /// /// 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; } /// /// Initializes a new instance of the test double, supplying a newly created to the base cache service as its lock-management dependency. /// /// public FakeCacheService() : base(new FakeLockManagerService()) { } /// /// Stub implementation of that records the invocation and the supplied key without performing any caching, and returns the value produced by . /// /// The cache key under which the value would be stored; recorded for later verification. /// The invoked to produce the value returned by this method. /// Optional time-to-live for the cache entry; ignored by this implementation. /// Token to cancel the operation; ignored by this implementation. /// The produced by invoking . /// /// /// Explicit implementation that always invokes and ignores and , effectively bypassing the cache and returning the value freshly produced for the supplied . /// /// The cache key identifying the requested entry. /// The asynchronous delegate invoked to produce the value. /// The optional cache lifetime. Not applied by this implementation. /// The token used to cancel the operation. Not observed by this implementation. /// A that completes with the value returned by . /// Task ICacheService.GetOrSetObjectAsync( string key, Func> factory, TimeSpan? ttl, CancellationToken cancellationToken) { WasCalled = true; LastKey = key; return factory(); } /// /// Retrieves an object from the cache for the given and , or produces it via when no cached entry exists. This implementation unconditionally invokes and records the call by setting to true, ignoring any cached value and the . /// /// The type of the object being retrieved or created. /// The grouped field used as part of the cache lookup key. /// The patient identifier used as part of the cache lookup key. /// The asynchronous factory invoked to produce the value when no cached entry is available. /// An optional time-to-live for the cached entry. Not used by this implementation. /// A to cancel the operation. /// A that resolves to the value returned by . /// /// /// Test implementation of that bypasses caching /// and directly invokes the supplied delegate, recording the call via the /// WasCalled flag for verification purposes. /// /// The identifying the group context for the cached or computed object. /// The of the patient whose data is being retrieved. /// The delegate invoked to produce the result. /// Optional time-to-live duration; ignored by this implementation. /// Token to observe for cancellation; ignored by this implementation. /// The produced by invoking . /// 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(); } } /// /// Represents a test fixture that exercises the to validate its expected runtime behavior. /// /// [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 /// /// Verifies that GetOrSetObjectAsync uses the Redis service when the patients cache mode is configured as , and does not fall back to the memory or no-op cache services. /// /// [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 /// /// Verifies that delegates to the in-memory cache service when is set to , forwarding the supplied key and ensuring that neither the Redis nor the no-op cache implementations are invoked in this scenario. /// /// [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 /// /// Verifies that delegates to the no-op cache service and skips both the Redis and in-memory cache services when is configured with . /// /// [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 /// /// Verifies that falls back to the InMemory cache service /// when the provided cache key has an unrecognized prefix, ensuring that unknown keys are still served by the default /// cache implementation rather than the or . /// /// A that completes when the fallback behavior has been validated through the assertions. /// [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 }