Files
adas-core/adas-core.Test/Services/ConfigObservationServiceTest.cs
T

468 lines
17 KiB
C#

using System.Security.Claims;
using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.MongoModels;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Moq;
using Options = Microsoft.Extensions.Options.Options;
namespace adas_core.Test.Services;
/// <summary>
/// Provides unit tests for the <see cref="ConfigObservationService"/> class.
/// </summary>
/// <!-- aidoc:v1 sig=b67849b -->
[TestFixture]
public class ConfigObservationServiceTest
{
private ConfigObservationService _service = null!;
private Mock<IConfigObservationRepository> _repo = null!;
private Mock<IUnitService> _unitSvc = null!;
private Mock<ILocalAuditService> _auditSvc = null!;
private Mock<IHttpContextAccessor> _http = null!;
private Mock<ICacheService> _cache = null!;
private Mock<ILogger<ConfigObservationService>> _logger = null!;
private IOptions<ApiSettings> _settings = null!;
private IOptions<CacheSettings> _cacheSettings = null!;
private static readonly ObjectId Id = ObjectId.GenerateNewId();
private static readonly ConfigObservation ConfigList = new()
{
Id = Id,
Name = "FC",
Code = "147842",
CodingSystem = "MDC",
ParentCode = "69965",
ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS",
MinAlert = 60,
MaxAlert = 100,
ForceAlert = true
};
private readonly List<ConfigObservation> _allConfigList =
[
new() { Id = ObjectId.GenerateNewId(), Name = "Hemoglobina", Code = "12345", CodingSystem = "SNM" },
new() { Id = ObjectId.GenerateNewId(), Name = "Glucemia", Code = "54321", CodingSystem = "SNM", OriginalName = "GLU" },
new() { Id = ObjectId.GenerateNewId(), Name = "Sodio", OriginalName = "Sodio" },
new()
{
Id = ObjectId.GenerateNewId(),
Name = "ph",
Code = "555",
CodingSystem = "MG4",
ParentCode = "3333",
ParentCodingSystem = "SNM"
},
new()
{
Id = ObjectId.GenerateNewId(),
Name = "SOFA",
Code = "278061009",
OriginalName = "SOFA",
CodingSystem = "SNM",
MinAlert = 10,
MinWarn = 14
},
ConfigList,
new() { Id = ObjectId.GenerateNewId(), Name = "Alarm_BlueCode", CodingSystem = "ADAS_EVENT" }
];
/// <summary>
/// Initializes the test fixture by creating mock instances of all dependencies required by <see cref="ConfigObservationService"/>,
/// including the repository, unit service, audit service, HTTP context accessor, cache service, and logger.
/// Configures a default authenticated <see cref="ClaimsPrincipal"/> in the HTTP context and sets the cache mocks to bypass caching by executing the factory function directly,
/// ensuring repository calls are invoked during tests.
/// </summary>
/// <!-- aidoc:v1 sig=dee8bf2 body=baa71ba -->
[SetUp]
public void Setup()
{
_repo = new Mock<IConfigObservationRepository>();
_unitSvc = new Mock<IUnitService>();
_auditSvc = new Mock<ILocalAuditService>();
_http = new Mock<IHttpContextAccessor>();
_cache = new Mock<ICacheService>();
_logger = new Mock<ILogger<ConfigObservationService>>();
var user = new ClaimsPrincipal(
new ClaimsIdentity([new Claim(ClaimTypes.Name, "TestUser")], "mock"));
_http.Setup(x => x.HttpContext).Returns(new DefaultHttpContext { User = user });
_settings = Options.Create(new ApiSettings
{
ConfigObservation = new ConfigObservationSettings
{
IgnoreUnknownObservation = false,
Refresh = null
},
});
_cacheSettings = Options.Create(new CacheSettings());
// KEY: mock cache to execute repository calls
_cache.Setup(c => c.GetOrSetObjectAsync(
It.IsAny<string>(),
It.IsAny<Func<Task<ICollection<ConfigObservation>>>>(),
It.IsAny<TimeSpan?>(),
It.IsAny<CancellationToken>()))
.Returns((string _, Func<Task<ICollection<ConfigObservation>>> f, TimeSpan? __, CancellationToken ___) => f());
_cache.Setup(c => c.GetOrSetObjectAsync(
It.IsAny<string>(),
It.IsAny<Func<Task<ConfigObservation>>>(),
It.IsAny<TimeSpan?>(),
It.IsAny<CancellationToken>()))
.Returns((string _, Func<Task<ConfigObservation>> f, TimeSpan? __, CancellationToken ___) => f());
_service = new ConfigObservationService(
_repo.Object,
_settings,
_cacheSettings,
_logger.Object,
_unitSvc.Object,
_http.Object,
_auditSvc.Object,
_cache.Object);
_repo.Setup(x => x.FindAll()).ReturnsAsync([ConfigList]);
}
// ---------------------------------------------------------
// TESTS
// ---------------------------------------------------------
/// <summary>
/// Verifies that querying a patient observation by <c>CodingSystem</c> only, without specifying a code or name, returns a null result.
/// </summary>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary states the query is performed 'without specifying a code or name', but the test code explicitly sets Code = \"Pump_X\" and Name = \"Alarm_Pump_X\" on the PatientObservation before calling _service.Get(obs)." -->
[Test]
public async Task Get_config_observation_item_by_codingSystem_only_return_null()
{
var obs = new PatientObservation
{
CodingSystem = "ADAS_EVENT",
Code = "Pump_X",
Name = "Alarm_Pump_X"
};
var result = await _service.Get(obs);
Assert.That(result, Is.Null);
}
/// <summary>
/// Verifies that <c>Get</c> returns the matching patient observation configuration for a given observation identified by its code and coding system.
/// Asserts that the resolved item is not null and that the displayed name is mapped from the configured lookup to the expected localized value.
/// </summary>
/// <!-- aidoc:v1 sig=d32a79d body=97b6327 -->
[Test]
public async Task Get_config_observation_item_by_code_and_codingSystem()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new BasePatientObservation
{
Name = "Hemo^GBr",
Code = "12345",
CodingSystem = "SNM"
};
var result = await _service.Get(obs);
Assert.That(result, Is.Not.Null);
Assert.That(result!.Name, Is.EqualTo("Hemoglobina"));
}
/// <summary>
/// Verifies that the service retrieves the correct configuration observation item by its name,
/// matching the requested observation against the list returned by the repository and returning
/// the item with the expected name.
/// </summary>
/// <!-- aidoc:v1 sig=3d7c0ca body=615f166 -->
[Test]
public async Task Get_config_observation_item_by_name()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new BasePatientObservation { Name = "Hemoglobina" };
var result = await _service.Get(obs);
Assert.That(result!.Name, Is.EqualTo("Hemoglobina"));
}
/// <summary>
/// Verifies that the service returns null when a patient observation item is requested by a name that does not exist in the configuration list.
/// </summary>
/// <!-- aidoc:v1 sig=fa14228 body=beae3aa -->
[Test]
public async Task Get_config_observation_item_by_name_not_exist_returns_null()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new BasePatientObservation { Name = "XXX" };
var result = await _service.Get(obs);
Assert.That(result, Is.Null);
}
/// <summary>
/// Verifies that the service retrieves the correct configuration by matching both the observation code with its coding system and the parent data code with its coding system, returning the configuration named "ph".
/// </summary>
/// <!-- aidoc:v1 sig=2711221 body=baf8942 -->
[Test]
public async Task Get_config_by_code_and_parent()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new BasePatientObservation
{
Code = "555",
CodingSystem = "MG4",
ParentData = new ParentDataClass
{
Code = "3333",
CodingSystem = "SNM"
}
};
var result = await _service.Get(obs);
Assert.That(result!.Name, Is.EqualTo("ph"));
}
/// <summary>
/// Verifies that the mapping service returns <c>null</c> when the observation name is unknown
/// and the configuration is set to ignore unknown observations.
/// </summary>
/// <!-- aidoc:v1 sig=b604759 body=1c3599d -->
[Test]
public async Task Map_unknown_ignore_true_returns_null()
{
_settings.Value.ConfigObservation!.IgnoreUnknownObservation = true;
_repo.Setup(x => x.FindAll()).ReturnsAsync([]);
var result = await _service.Map(new BasePatientObservation { Name = "XX" });
Assert.That(result, Is.Null);
}
/// <summary>
/// Verifies that when ConfigObservation.IgnoreUnknownObservation is set to <c>false</c>,
/// the <c>Map</c> method returns the original observation as-is when no matching record is found
/// in the repository, rather than discarding it as an unknown observation.
/// </summary>
/// <!-- aidoc:v1 sig=bb05709 body=c204c4b -->
[Test]
public async Task Map_unknown_ignore_false_returns_obs()
{
_settings.Value.ConfigObservation!.IgnoreUnknownObservation = false;
_repo.Setup(x => x.FindAll()).ReturnsAsync([]);
var obs = new BasePatientObservation { Name = "XX" };
var result = await _service.Map(obs);
Assert.That(result, Is.EqualTo(obs));
}
/// <summary>
/// Verifies that mapping a patient observation with a value of 14 results in an "Ok" status, confirming the threshold mapping logic returns the expected outcome.
/// </summary>
/// <!-- aidoc:v1 sig=81e9217 body=d5f8b5a -->
[Test]
public async Task Map_threshold_Ok()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new PatientObservation
{
Name = "SOFA",
CodingSystem = "SNM",
Value = 14
};
var result = await _service.Map(obs);
Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Ok));
}
/// <summary>
/// Tests that the Map method returns a Warning status when a patient observation value exceeds the configured threshold.
/// </summary>
/// <!-- aidoc:v1 sig=62b87c5 body=8312d29 -->
[Test]
public async Task Map_threshold_Warning()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new PatientObservation
{
Name = "SOFA",
CodingSystem = "SNM",
Value = 13
};
var result = await _service.Map(obs);
Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Warning));
}
/// <summary>
/// Verifies that mapping a <see cref="PatientObservation"/> with a SOFA score of 9 through the configured service results in a result with an <see cref="StatusEnum.Type.Alert"/> status.
/// </summary>
/// <!-- aidoc:v1 sig=b9d47f9 body=ff5f81e -->
[Test]
public async Task Map_threshold_Alert()
{
_repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList);
var obs = new PatientObservation
{
Name = "SOFA",
CodingSystem = "SNM",
Value = 9
};
var result = await _service.Map(obs);
Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Alert));
}
/// <summary>
/// Verifies that mapping a parent <see cref="PatientObservation"/> returns an <see cref="StatusEnum.Type.Ok"/> status
/// when the repository successfully locates the configuration by identifier.
/// </summary>
/// <!-- aidoc:v1 sig=ba42824 body=c7bbf55 -->
[Test]
public async Task Map_parent_ok()
{
_repo.Setup(x => x.FindById(Id)).ReturnsAsync(ConfigList);
var fc = new PatientObservation
{
Id = Id,
Name = "FC",
Value = 70
};
var result = await _service.Map(fc);
Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Ok));
}
/// <summary>
/// Verifies that <c>UpdateConfig</c> successfully updates an existing configuration and returns the updated <see cref="ConfigObservation"/> with the new name.
/// </summary>
/// <!-- aidoc:v1 sig=18d4b11 body=f7f80a4 -->
[Test]
public async Task UpdateConfig_ok()
{
var newCfg = new ConfigObservation
{
Id = Id,
Name = "New",
Code = "X",
CodingSystem = "S"
};
_repo.Setup(r => r.FindById(Id)).ReturnsAsync(ConfigList);
_repo.Setup(r => r.Update(It.IsAny<ConfigObservation>())).ReturnsAsync(newCfg);
var result = await _service.UpdateConfig(newCfg);
Assert.That(result!.Name, Is.EqualTo("New"));
}
/// <summary>
/// Verifies that <c>UpdateConfig</c> throws a <see cref="NotFoundException"/> when the target <c>ConfigObservation</c> cannot be found by its identifier.
/// </summary>
/// <!-- aidoc:v1 sig=470341f body=33e9665 -->
[Test]
public async Task UpdateConfig_notfound()
{
_repo
.Setup(r => r.FindById(Id))
.ReturnsAsync((ConfigObservation?)null);
Func<Task> act = () => _service.UpdateConfig(new ConfigObservation { Id = Id });
Assert.ThrowsAsync<NotFoundException>(act);
}
/// <summary>
/// Verifies that the <c>CreateConfig</c> service method successfully inserts the provided configuration and returns the expected result.
/// </summary>
/// <!-- aidoc:v1 sig=4c4fb2f body=19b37b8 -->
[Test]
public async Task CreateConfig_ok()
{
_repo.Setup(r => r.InsertOneAsyncAndReturn(ConfigList)).Returns(Task.FromResult(ConfigList));
var result = await _service.CreateConfig(ConfigList);
Assert.That(result, Is.EqualTo(ConfigList));
}
/// <summary>
/// Verifies that <see cref="BadRequestException"/> is thrown when attempting to create a configuration that already exists.
/// </summary>
/// <!-- aidoc:v1 sig=ea4710c body=04477ed -->
[Test]
public void CreateConfig_duplicate_throws()
{
_repo.Setup(r => r.FindById(It.IsAny<ObjectId>())).ReturnsAsync(ConfigList);
Func<Task> act = () => _service.CreateConfig(ConfigList);
Assert.ThrowsAsync<BadRequestException>(act);
}
/// <summary>
/// Verifies that RemoveConfigItem successfully removes a configuration item and returns the deleted entity when the repository finds and deletes it.
/// </summary>
/// <!-- aidoc:v1 sig=30c283f body=7951323 -->
[Test]
public async Task RemoveConfigItem_ok()
{
var deleted = new ConfigObservation { Id = ObjectId.GenerateNewId(), Name = "X" };
_repo.Setup(r => r.FindById(It.IsAny<ObjectId>())).ReturnsAsync(ConfigList);
_repo.Setup(r => r.Delete(It.IsAny<ObjectId>())).ReturnsAsync(deleted);
var result = await _service.RemoveConfigItem(ObjectId.GenerateNewId());
Assert.That(result, Is.EqualTo(deleted));
}
/// <summary>
/// Verifies that <c>RemoveConfigItem</c> returns <c>null</c> when the underlying delete operation on the repository yields no result, simulating the case where the configuration item does not exist.
/// </summary>
/// <!-- aidoc:v1 sig=87b0794 body=76439aa -->
[Test]
public async Task RemoveConfigItem_null()
{
_repo.Setup(r => r.FindById(Id)).ReturnsAsync(ConfigList);
_repo.Setup(r => r.Delete(Id)).ReturnsAsync((ConfigObservation?)null);
var result = await _service.RemoveConfigItem(Id);
Assert.That(result, Is.Null);
}
}