using adas_core.Application.Services.Caching; using adas_core.Domain.Enums; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Utils; using MongoDB.Bson; using Moq; using Microsoft.Extensions.Logging; namespace adas_core.Test.Utilities; [TestFixture] public class CacheKeyClassifierTest { #region TC-01 /// /// Verifies that returns when the cache key starts with the "patients" prefix. /// [Test] public void Classify_ReturnsPatients_ForKeyStartingWithPatients() { // Arrange var key = "patients:latestObs:abc123:HR:0"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Patients)); } /// /// Verifies that returns when the cache key starts with the "patients:xyz:" prefix. /// [Test] public void Classify_ReturnsPatients_ForKeyStartingWithPatientsXYZ() { // Arrange var key = "patients:xyz:any:key"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Patients)); } #endregion #region TC-02 /// /// Verifies that correctly returns /// for a cache key matching the "configDisplays:display:<id>:base" pattern. /// [Test] public void Classify_ReturnsDisplays_ForKeyStartingWithConfigDisplaysBase() { // Arrange //var key2 = "configDisplays:display:id:base"; var key = "configDisplays:display:690b341c4a349b5898898241:base "; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Displays)); } /// /// Verifies that returns when the supplied cache key begins with the configDisplays: prefix. /// [Test] public void Classify_ReturnsDisplays_ForKeyStartingWithConfigDisplaysConfig() { // Arrange var key = "configDisplays:display:id:config"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Displays)); } #endregion #region TC-03 /// /// Verifies that returns /// when the provided cache key starts with the "pumpObs" prefix. /// [Test] public void Classify_ReturnsPumpObservations_ForKeyStartingWithPumpObs() { // Arrange var key = "pumpObs:latest:patientId:10"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.PumpObservations)); } #endregion #region TC-04 /// /// Verifies that returns when the cache key starts with the "appointments:patient:" prefix. /// [Test] public void Classify_ReturnsAppointments_ForKeyStartingWithAppointmentsPatient() { // Arrange var key = "appointments:patient:id:20260224"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Appointments)); } /// /// Verifies that returns when the provided cache key begins with the "appointments:PoC:" segment. /// [Test] public void Classify_ReturnsAppointments_ForKeyStartingWithAppointmentsPoc() { // Arrange var key = "appointments:PoC:pocId:20260224"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Appointments)); } #endregion #region TC-05 /// /// Verifies that returns for cache keys that begin with the "groupedObs:" prefix, such as those following the "groupedObs:HR:patient:{id}" pattern. /// [Test] public void Classify_ReturnsGroupedObservations_ForKeyStartingWithGroupedObs() { // Arrange var key = "groupedObs:HR:patient:abc123"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.GroupedObservations)); } #endregion #region TC-06 /// /// Verifies that returns when the supplied cache key starts with the configObservations: prefix. /// [Test] public void Classify_ReturnsConfigObservations_ForKeyStartingWithConfigObservations() { // Arrange var key = "configObservations:all"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.ConfigObservations)); } #endregion #region TC-07 /// /// Verifies that the cache key classifier returns when a key has a prefix that is not recognized. /// [Test] public void Classify_ReturnsUnknown_ForUnrecognizedPrefixDiagnostics() { // Arrange var key = "diagnostics:patient:abc"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown)); } /// /// Verifies that returns when the supplied cache key uses an unrecognized prefix. /// [Test] public void Classify_ReturnsUnknown_ForUnrecognizedPrefixRandom() { // Arrange var key = "random:key"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown)); } #endregion #region TC-08 /// /// Verifies that returns when the cache key is provided in upper case. /// [Test] public void Classify_ReturnsPatients_ForKeyInUpperCase() { // Arrange var key = "PATIENTS:latestObs:abc"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Patients)); } /// /// Verifies that correctly returns the entity type when the supplied cache key contains a mixture of uppercase and lowercase characters, confirming that key classification is case-insensitive. /// [Test] public void Classify_ReturnsAppointments_ForKeyInMixedCase() { // Arrange var key = "Appointments:PoC:id:date"; // Act var result = CacheKeyClassifier.Classify(key); // Assert Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Appointments)); } #endregion #region TC-09 /// /// Verifies that returns null when the global cache is disabled, regardless of the configured entity-specific cache mode and Redis TTL settings. /// [Test] public void ResolveForEntity_ReturnsNull_WhenGlobalCacheDisabled() { // Arrange var settings = new CacheSettings { IsEnabled = false, Patients = CacheEnum.Mode.Redis, Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 600 } } }; // Act var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); // Assert Assert.That(result, Is.Null); } /// /// Verifies that returns null for every supported entity type /// when caching is globally disabled via set to false. /// [Test] public void ResolveForEntity_ReturnsNull_WhenGlobalDisabled_ForAllEntities() { // Arrange var entities = new[] { CacheEnum.EntityType.Patients, CacheEnum.EntityType.Displays, CacheEnum.EntityType.PumpObservations, CacheEnum.EntityType.Appointments, CacheEnum.EntityType.PontOfCare, CacheEnum.EntityType.GroupedObservations, CacheEnum.EntityType.PatientObservations, CacheEnum.EntityType.ConfigObservations }; var settings = new CacheSettings { IsEnabled = false }; // Act & Assert foreach (var entity in entities) { var result = CacheKeyTtl.ResolveForEntity(settings, entity); Assert.That(result, Is.Null, $"Expected null for entity {entity}"); } } #endregion #region TC-10 /// /// Verifies that returns null when the /// entity mode is set to , regardless of the in-memory TTL configuration. /// [Test] public void ResolveForEntity_ReturnsNull_WhenEntityModeIsNone() { // Arrange var settings = new CacheSettings { IsEnabled = true, PumpObservations = CacheEnum.Mode.None, InMemory = new InMemorySettings { Ttl = new TtlSettings { PumpObservationsSeconds = 300 } } }; // Act var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PumpObservations); // Assert Assert.That(result, Is.Null); } #endregion #region TC-11 /// /// Verifies that returns the entity-specific TTL /// (configured via PatientsSeconds) when the cache mode for the entity is set to /// , instead of falling back to the global TTL. /// [Test] public void ResolveForEntity_UsesEntitySpecificTtl_WhenModeIsCache() { // Arrange var settings = new CacheSettings { IsEnabled = true, Patients = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { GlobalSeconds = 3600, PatientsSeconds = 600 } } }; // Act var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); // Assert Assert.That(result, Is.EqualTo(TimeSpan.FromSeconds(600))); Assert.That(result, Is.Not.EqualTo(TimeSpan.FromSeconds(3600))); } #endregion #region TC-12 /// /// Verifies that returns the entity-specific Redis TTL (AppointmentsSeconds) /// when the cache mode for the entity is set to . /// [Test] public void ResolveForEntity_UsesRedisTtl_WhenModeIsRedis() { // Arrange var settings = new CacheSettings { IsEnabled = true, Appointments = CacheEnum.Mode.Redis, Redis = new RedisSettings { Ttl = new TtlSettings { GlobalSeconds = 3600, AppointmentsSeconds = 900 } } }; // Act var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments); // Assert Assert.That(result, Is.EqualTo(TimeSpan.FromSeconds(900))); } #endregion #region TC-13 /// /// Verifies that falls back to the globally configured TTL /// (in seconds) when no entity-specific TTL is defined for the requested entity type. /// [Test] public void ResolveForEntity_UsesGlobalSeconds_WhenNoEntitySpecificTtl() { // Arrange var settings = new CacheSettings { IsEnabled = true, Patients = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { GlobalSeconds = 3600 // No PatientsSeconds defined } } }; // Act var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); // Assert Assert.That(result, Is.EqualTo(TimeSpan.FromSeconds(3600))); } #endregion #region TC-14 /// /// Verifies that CacheKeyTtl.ResolveForEntity returns null when the provided CacheSettings enable caching for the entity but no TTL values are configured (neither GlobalSeconds nor the entity-specific seconds). /// [Test] public void ResolveForEntity_ReturnsNull_WhenNoTtlConfigured() { // Arrange var settings = new CacheSettings { IsEnabled = true, Patients = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings() // No GlobalSeconds, no PatientsSeconds } }; // Act var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); // Assert Assert.That(result, Is.Null); } #endregion #region TC-15 /// /// Verifies that CacheKeys.PatientAppointmentsTodayKeyWithTtl generates a cache key in the format /// "appointments:patient:{patientId}:{yyyyMMdd}" and returns a TTL of 120 seconds when caching is enabled /// and the Appointments mode is set to . /// /// A method; assertions are used to validate the generated key and TTL values. [Test] public void PatientAppointmentsTodayKeyWithTtl_GeneratesCorrectFormat() { // Arrange var patientId = ObjectId.GenerateNewId(); var today = DateTime.UtcNow.ToString("yyyyMMdd"); var settings = new CacheSettings { IsEnabled = true, Appointments = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { AppointmentsSeconds = 120 } } }; // Act var (key, ttl) = CacheKeys.PatientAppointmentsTodayKeyWithTtl(settings, patientId); // Assert Assert.That(key, Is.EqualTo($"appointments:patient:{patientId}:{today}")); Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(120))); } #endregion #region TC-16 /// /// Verifies that generates the expected cache key format /// ("appointments:PoC:{pocId}:{yyyyMMdd}") and returns the TTL value configured for point of care entries. /// [Test] public void PocAppointmentsTodayKeyWithTtl_GeneratesCorrectFormat() { // Arrange var pocId = ObjectId.GenerateNewId(); var today = DateTime.UtcNow.ToString("yyyyMMdd"); var settings = new CacheSettings { IsEnabled = true, PointOfCares = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { PointOfCaresSeconds = 180 } } }; // Act var (key, ttl) = CacheKeys.PocAppointmentsTodayKeyWithTtl(settings, pocId); // Assert Assert.That(key, Is.EqualTo($"appointments:PoC:{pocId}:{today}")); Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(180))); } #endregion #region TC-17 /// /// Verifies that generates a deterministic cache key by sorting the supplied field names alphabetically, regardless of their input order. /// [Test] public void LatestObservationsKeyWithTtl_SortsFieldNamesAlphabetically() { // Arrange var patientId = ObjectId.GenerateNewId(); var settings = new CacheSettings(); // Act var (key1, _) = CacheKeys.LatestObservationsKeyWithTtl( settings, patientId, new[] { "Systolic", "HR", "Diastolic" }); var (key2, _) = CacheKeys.LatestObservationsKeyWithTtl( settings, patientId, new[] { "HR", "Diastolic", "Systolic" }); // Assert Assert.That(key1, Is.EqualTo(key2)); Assert.That(key1, Does.Contain("Diastolic|HR|Systolic")); } #endregion #region TC-18 /// /// Verifies that the cache key generated by excludes empty strings and null entries from the provided observation types, producing a clean delimiter-separated key for the remaining valid values. /// [Test] public void LatestObservationsKeyWithTtl_ExcludesEmptyFields() { // Arrange var patientId = ObjectId.GenerateNewId(); var settings = new CacheSettings(); // Act var (key, _) = CacheKeys.LatestObservationsKeyWithTtl( settings, patientId, new[] { "HR", "", "Diastolic", null! }); // Assert Assert.That(key.Contains("||"), Is.False); Assert.That(key, Does.Contain("Diastolic|HR")); } #endregion #region TC-19 /// /// Verifies that generates the expected base display cache key /// using the "configDisplays:display:{displayId}:base" format and returns the TTL value /// configured in the in-memory cache settings. /// [Test] public void DisplayBaseKeyWithTtl_GeneratesCorrectFormat() { // Arrange var displayId = ObjectId.GenerateNewId(); var settings = new CacheSettings { IsEnabled = true, Displays = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { DisplaysSeconds = 300 } } }; // Act var (key, ttl) = CacheKeys.DisplayBaseKeyWithTtl(settings, displayId); // Assert Assert.That(key, Is.EqualTo($"configDisplays:display:{displayId}:base")); Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(300))); } #endregion #region TC-20 /// /// Verifies that produces a cache key in the expected /// "configDisplays:display:{displayId}:config" format and that it differs from the base display key /// generated by . /// [Test] public void DisplayWithConfigKeyWithTtl_GeneratesCorrectFormat() { // Arrange var displayId = ObjectId.GenerateNewId(); var settings = new CacheSettings(); // Act var (keyBase, _) = CacheKeys.DisplayBaseKeyWithTtl(settings, displayId); var (keyConfig, _) = CacheKeys.DisplayWithConfigKeyWithTtl(settings, displayId); // Assert Assert.That(keyConfig, Is.EqualTo($"configDisplays:display:{displayId}:config")); Assert.That(keyBase, Is.Not.EqualTo(keyConfig)); } #endregion #region TC-21 /// /// Verifies that generates a cache key with the expected /// "pointOfCare:{pocId}:base" format and applies the configured point-of-care TTL in seconds from the in-memory settings. /// [Test] public void PointOfCareBaseKeyWithTtl_GeneratesCorrectFormat() { // Arrange var pocId = ObjectId.GenerateNewId(); var settings = new CacheSettings { IsEnabled = true, PointOfCares = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { PointOfCaresSeconds = 240 } } }; // Act var (key, ttl) = CacheKeys.PointOfCareBaseKeyWithTtl(settings, pocId); // Assert Assert.That(key, Is.EqualTo($"pointOfCare:{pocId}:base")); Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(240))); } #endregion #region TC-22 /// /// Verifies that generates the cache key /// "configObservations:all" consistently regardless of the supplied , /// including when TTL configuration and enabled state differ from defaults. /// [Test] public void ConfigObservationsAllKeyWithTtl_GeneratesCorrectFormat() { // Arrange var settings1 = new CacheSettings(); var settings2 = new CacheSettings { IsEnabled = true, ConfigObservations = CacheEnum.Mode.Cache, InMemory = new InMemorySettings { Ttl = new TtlSettings { ConfigObservationsSeconds = 600 } } }; // Act var (key1, _) = CacheKeys.ConfigObservationsAllKeyWithTtl(settings1); var (key2, _) = CacheKeys.ConfigObservationsAllKeyWithTtl(settings2); // Assert Assert.That(key1, Is.EqualTo("configObservations:all")); Assert.That(key2, Is.EqualTo("configObservations:all")); Assert.That(key1, Is.EqualTo(key2)); } #endregion #region TC-59 /// /// Verifies that returns the correct cache key pattern for each supported /// , mapping known entity types to their specific namespace patterns (e.g., /// patients:*, displays:*, pumpObs:*, appointments:*, groupedObs:*, /// configObservation:*) and falling back to a wildcard pattern (*) for . /// /// The entity type under test for which a cache key pattern is requested. /// The cache key pattern that is expected to return for the given entity type. [TestCase(CacheEnum.EntityType.Patients, "patients:*")] [TestCase(CacheEnum.EntityType.Displays, "displays:*")] [TestCase(CacheEnum.EntityType.PumpObservations, "pumpObs:*")] [TestCase(CacheEnum.EntityType.Appointments, "appointments:*")] [TestCase(CacheEnum.EntityType.GroupedObservations, "groupedObs:*")] [TestCase(CacheEnum.EntityType.ConfigObservations, "configObservation:*")] [TestCase(CacheEnum.EntityType.Unknown, "*")] public void ForEntity_ReturnsCorrectPatternForEachEntityType( CacheEnum.EntityType entity, string expectedPattern) { var result = CacheKeyPatterns.ForEntity(entity); Assert.That(result, Is.EqualTo(expectedPattern)); } #endregion #region TC-60 /// /// Verifies that generates a cache key pattern /// that embeds both the supplied prefix and patient identifier, producing the expected /// format of {prefix}:*:{patientId}*. /// [Test] public void ByPatient_ReturnsPatternContainingPrefixAndPatientId() { const string prefix = "patients:latestObs"; const string patientId = "abc123"; var pattern = CacheKeyPatterns.ByPatient(prefix, patientId); Assert.That(pattern, Is.EqualTo($"{prefix}:*:{patientId}*")); Assert.That(pattern, Does.Contain(prefix)); Assert.That(pattern, Does.Contain(patientId)); } /// /// Verifies that DeleteByPatternAsync removes only cache entries whose keys match the /// patient-specific pattern built via CacheKeyPatterns.ByPatient, leaving entries /// belonging to other patients and unrelated entity types intact, and reports the count of deleted keys. /// [Test] public async Task ByPatient_DeleteByPatternAsync_OnlyRemovesMatchingPatientKeys() { var lockMgr = new LockManagerService( new Mock>().Object, new InMemoryLockProvider()); var svc = new CacheService(lockMgr); const string prefix = "patients:latestObs"; const string patientId = "abc123"; const string otherId = "def456"; var pattern = CacheKeyPatterns.ByPatient(prefix, patientId); var effectiveSubstring = pattern.Replace("*", ""); var matchingKey = $"{effectiveSubstring}:HR:0"; var nonMatchingKey = $"patients:latestObs::{otherId}:HR:0"; var differentEntityKey = $"appointments::{patientId}:data"; await svc.SetObjectAsync(matchingKey, "val1"); await svc.SetObjectAsync(nonMatchingKey, "val2"); await svc.SetObjectAsync(differentEntityKey, "val3"); var deleted = await svc.DeleteByPatternAsync(pattern); Assert.That(deleted, Is.EqualTo(1)); Assert.That(await svc.GetObjectAsync(matchingKey), Is.Null); Assert.That(await svc.GetObjectAsync(nonMatchingKey), Is.EqualTo("val2")); Assert.That(await svc.GetObjectAsync(differentEntityKey), Is.EqualTo("val3")); } #endregion }