Files
adas-core/adas-core.Test/Services/CacheDispatcherTest.cs
T
2026-06-27 15:23:26 -07:00

375 lines
18 KiB
C#

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;
/// <summary>
/// Provides a fake implementation of the <see cref="ILockProvider"/> interface, typically used as a test double or non-functional placeholder.
/// </summary>
public class FakeLockProvider : ILockProvider
{
/// <summary>
/// Asynchronously attempts to acquire a resource identified by the specified key within the given timeout.
/// This implementation always reports a successful acquisition by returning <c>true</c>.
/// </summary>
/// <param name="key">The identifier of the resource to acquire.</param>
/// <param name="timeout">The maximum time to wait for the resource to become available.</param>
/// <returns>A <see cref="Task{TResult}"/> that always completes with <c>true</c>, indicating the resource was acquired.</returns>
public Task<bool> AcquireAsync(string key, TimeSpan timeout) => Task.FromResult(true);
/// <summary>
/// Releases the resource associated with the specified key. This implementation completes immediately without performing any additional operation.
/// </summary>
/// <param name="key">The identifier of the resource to release.</param>
public Task ReleaseAsync(string key) => Task.CompletedTask;
}
/// <summary>
/// Represents a fake implementation of <see cref="LockManagerService"/>, typically used as a test double to provide controlled or simplified behavior in unit tests.
/// </summary>
/// <remarks>
/// Inherits all members from <see cref="LockManagerService"/> and is intended to be used in place of the real service when actual locking functionality is not required.
/// </remarks>
public class FakeLockManagerService : LockManagerService
{
/// <summary>
/// Initializes a new instance of <see cref="FakeLockManagerService"/>, a test fake of <see cref="LockManagerService"/>, by forwarding a mocked <see cref="ILogger{LockManagerService}"/> and a <see cref="FakeLockProvider"/> to the base constructor.
/// </summary>
/// <!-- aidoc:v1 sig=1d9bbc1 body=4448e1d -->
public FakeLockManagerService() : base(
Mock.Of<ILogger<LockManagerService>>(),
new FakeLockProvider())
{
}
}
/// <summary>
/// Represents a fake or test double implementation of <see cref="RedisService"/> that also implements <see cref="ICacheService"/>, typically used to simulate Redis caching behavior in testing scenarios.
/// </summary>
/// <remarks>
/// This class combines the inheritance of <see cref="RedisService"/> with the contract of <see cref="ICacheService"/>, allowing it to stand in for a real Redis-backed cache during unit tests or development.
/// </remarks>
public class FakeRedisService : RedisService, ICacheService
{
public bool WasCalled { get; private set; }
public string? LastKey { get; private set; }
/// <summary>
/// Initializes a new instance of <see cref="FakeRedisService"/>, a test double for <see cref="RedisService"/>, supplying default <see cref="CacheSettings"/>, a mocked <see cref="ILogger{RedisService}"/>, and a <see cref="FakeLockManagerService"/> to the base constructor.
/// </summary>
/// <!-- aidoc:v1 sig=82d8847 body=4448e1d -->
public FakeRedisService() : base(
Options.Create(new CacheSettings()),
Mock.Of<ILogger<RedisService>>(),
new FakeLockManagerService())
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="key">The cache key used to identify the stored object.</param>
/// <param name="factory">A delegate that asynchronously produces the object to cache when no entry exists for the specified key.</param>
/// <param name="ttl">An optional time-to-live duration for the cached entry. Not used in this implementation.</param>
/// <param name="cancellationToken">A token to observe for cancellation requests. Not used in this implementation.</param>
/// <returns>A task that represents the asynchronous operation, containing the object produced by the factory.</returns>
Task<T> ICacheService.GetOrSetObjectAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? ttl,
CancellationToken cancellationToken)
{
WasCalled = true;
LastKey = key;
return factory();
}
/// <summary>
/// Retrieves or sets a cached object associated with the specified grouped field and patient identifier.
/// Marks the call as executed via the <c>WasCalled</c> flag and returns the value produced by the supplied factory delegate.
/// </summary>
/// <param name="groupedField">The grouped field used to identify the cached object.</param>
/// <param name="patientId">The identifier of the patient associated with the cached object.</param>
/// <param name="factory">The asynchronous factory delegate invoked to produce the value when no cached entry exists.</param>
/// <param name="ttl">An optional time-to-live duration for the cached entry.</param>
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
/// <returns>The value of type <typeparamref name="T"/> produced by the <paramref name="factory"/> delegate.</returns>
Task<T> ICacheService.GetOrSetObjectAsync<T>(
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl,
CancellationToken cancellationToken)
{
WasCalled = true;
return factory();
}
}
/// <summary>
/// Represents a fake implementation of <see cref="CacheService"/> that also implements the <see cref="ICacheService"/> interface, typically used for testing or stubbing scenarios.
/// </summary>
/// <remarks>
/// This class combines inheritance from the concrete <see cref="CacheService"/> base class with the <see cref="ICacheService"/> contract, allowing it to be used wherever an <see cref="ICacheService"/> is required.
/// </remarks>
public class FakeCacheService : CacheService, ICacheService
{
public bool WasCalled { get; private set; }
public string? LastKey { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="FakeCacheService"/> test double, supplying a newly created <see cref="FakeLockManagerService"/> to the base cache service as its lock-management dependency.
/// </summary>
/// <!-- aidoc:v1 sig=e01af03 body=4448e1d -->
public FakeCacheService() : base(new FakeLockManagerService())
{
}
/// <summary>
/// Stub implementation of <see cref="ICacheService.GetOrSetObjectAsync{T}"/> that records the invocation and the supplied key without performing any caching, and returns the value produced by <paramref name="factory"/>.
/// </summary>
/// <param name="key">The cache key under which the value would be stored; recorded for later verification.</param>
/// <param name="factory">The <see cref="Func{Task{T}}"/> invoked to produce the value returned by this method.</param>
/// <param name="ttl">Optional time-to-live for the cache entry; ignored by this implementation.</param>
/// <param name="cancellationToken">Token to cancel the operation; ignored by this implementation.</param>
/// <returns>The <see cref="Task{T}"/> produced by invoking <paramref name="factory"/>.</returns>
/// <!-- aidoc:v1 sig=f7027e3 body=b366c2a -->
Task<T> ICacheService.GetOrSetObjectAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? ttl,
CancellationToken cancellationToken)
{
WasCalled = true;
LastKey = key;
return factory();
}
/// <summary>
/// Retrieves an object from the cache for the given <paramref name="groupedField"/> and <paramref name="patientId"/>, or produces it via <paramref name="factory"/> when no cached entry exists. This implementation unconditionally invokes <paramref name="factory"/> and records the call by setting <see cref="WasCalled"/> to <c>true</c>, ignoring any cached value and the <paramref name="ttl"/>.
/// </summary>
/// <typeparam name="T">The type of the object being retrieved or created.</typeparam>
/// <param name="groupedField">The grouped field used as part of the cache lookup key.</param>
/// <param name="patientId">The patient identifier used as part of the cache lookup key.</param>
/// <param name="factory">The asynchronous factory invoked to produce the value when no cached entry is available.</param>
/// <param name="ttl">An optional time-to-live for the cached entry. Not used by this implementation.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the operation.</param>
/// <returns>A <see cref="Task{T}"/> that resolves to the value returned by <paramref name="factory"/>.</returns>
/// <!-- aidoc:v1 sig=efeef03 body=0bb8f50 -->
Task<T> ICacheService.GetOrSetObjectAsync<T>(
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl,
CancellationToken cancellationToken)
{
WasCalled = true;
return factory();
}
}
/// <summary>
/// Represents a fake implementation of the cache service, used for testing or scenarios where a no-op cache behavior is required.
/// </summary>
/// <remarks>
/// This class inherits from <see cref="NoCacheService"/> and implements the <see cref="ICacheService"/> interface, providing a non-functional cache suitable for unit tests or environments where caching should be bypassed.
/// </remarks>
public class FakeNoCacheService : NoCacheService, ICacheService
{
public bool WasCalled { get; private set; }
public string? LastKey { get; private set; }
Task<T> ICacheService.GetOrSetObjectAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? ttl,
CancellationToken cancellationToken)
{
WasCalled = true;
LastKey = key;
return factory();
}
Task<T> ICacheService.GetOrSetObjectAsync<T>(
GroupedField groupedField,
ObjectId patientId,
Func<Task<T>> factory,
TimeSpan? ttl,
CancellationToken cancellationToken)
{
WasCalled = true;
return factory();
}
}
/// <summary>
/// Represents a test fixture that exercises the <see cref="CacheDispatcher"/> to validate its expected runtime behavior.
/// </summary>
/// <!-- aidoc:v1 sig=e92eb06 -->
[TestFixture]
public class CacheDispatcherTest
{
private FakeRedisService _redisServiceFake = null!;
private FakeCacheService _memoryServiceFake = null!;
private FakeNoCacheService _noopServiceFake = null!;
private CacheSettings _cacheSettings = null!;
private CacheDispatcher _cacheDispatcher = null!;
/// <summary>
/// Initializes the test environment by instantiating fake implementations of the Redis, in-memory, and no-op cache services, along with default <see cref="CacheSettings"/>, and constructs a <see cref="CacheDispatcher"/> under test using these dependencies.
/// </summary>
[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
/// <summary>
/// Verifies that GetOrSetObjectAsync uses the Redis service when the patients cache mode is configured as <see cref="CacheEnum.Mode.Redis"/>, and does not fall back to the memory or no-op cache services.
/// </summary>
/// <!-- aidoc:v1 sig=3f07bc5 body=9d2c439 -->
[Test]
public async Task GetOrSetObjectAsync_UsesRedisService_WhenPatientsModeIsRedis()
{
// Arrange
var key = "patients:latestObs:abc";
var expectedResult = new { Name = "TestPatient" };
Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);
_cacheSettings.Patients = CacheEnum.Mode.Redis;
// Act
var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(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
/// <summary>
/// Verifies that <see cref="CacheDispatcher.GetOrSetObjectAsync{T}"/> delegates to the in-memory cache service when <see cref="CacheSettings.Appointments"/> is set to <see cref="CacheEnum.Mode.Cache"/>, forwarding the supplied key and ensuring that neither the Redis nor the no-op cache implementations are invoked in this scenario.
/// </summary>
/// <!-- aidoc:v1 sig=69efd2a body=e0ce152 -->
[Test]
public async Task GetOrSetObjectAsync_UsesCacheService_WhenAppointmentsModeIsCache()
{
// Arrange
var key = "appointments:patient:id:20260224";
var expectedResult = new { Name = "TestAppointment" };
Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);
_cacheSettings.Appointments = CacheEnum.Mode.Cache;
// Act
var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(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
/// <summary>
/// Verifies that <see cref="CacheDispatcher.GetOrSetObjectAsync"/> delegates to the no-op cache service and skips both the Redis and in-memory cache services when <see cref="CacheSettings.PumpObservations"/> is configured with <see cref="CacheEnum.Mode.None"/>.
/// </summary>
/// <!-- aidoc:v1 sig=c516b35 body=f3aa2f5 -->
[Test]
public async Task GetOrSetObjectAsync_UsesNoCacheService_WhenPumpObservationsModeIsNone()
{
// Arrange
var key = "pumpObs:latest:abc:10";
var expectedResult = new { Name = "TestPump" };
Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);
_cacheSettings.PumpObservations = CacheEnum.Mode.None;
// Act
var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(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
/// <summary>
/// Verifies that <see cref="GetOrSetObjectAsync{T}(string, Func{Task{T}})"/> 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 <see cref="IRedisCacheService"/> or <see cref="INoCacheService"/>.
/// </summary>
/// <returns>A <see cref="Task"/> that completes when the fallback behavior has been validated through the assertions.</returns>
/// <!-- aidoc:v1 sig=b24df70 body=15e546d -->
[Test]
public async Task GetOrSetObjectAsync_UsesCacheService_WhenKeyIsUnrecognized()
{
// Arrange
var key = "unknown:prefix:key";
var expectedResult = new { Name = "TestUnknown" };
Func<Task<object>> factory = () => Task.FromResult<object>(expectedResult);
// Act
var result = await _cacheDispatcher.GetOrSetObjectAsync<object>(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");
}
/// <summary>
/// Verifies that <see cref="CacheKeyClassifier.Classify"/> returns <see cref="CacheEnum.EntityType.Unknown"/> when the provided cache key does not start with any recognized prefix.
/// </summary>
[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
}