using System.Security.Claims; using adas_core.Application.Repositories.Interfaces; using adas_core.Application.Services; using adas_core.Application.Services.Interfaces; using adas_core.Application.Subscriptions; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Models.Pumps; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using MongoDB.Bson; using Moq; using Options = Microsoft.Extensions.Options.Options; namespace adas_core.Test.Services; /// /// Provides a test fixture for verifying the behavior of the PumpService. /// /// /// This class is decorated with the TestFixture attribute and hosts the unit tests for the PumpService. /// /// [TestFixture] public class PumpServiceTest { private PumpService _service = null!; private Mock _obsRepo = null!; private Mock _stateRepo = null!; private Mock _alarmEventRepo = null!; private Mock _alarmStateRepo = null!; private Mock _archiveRepo = null!; private Mock _patientSvc = null!; private Mock _configPumps = null!; private Mock _subs = null!; private Mock _clientMsg = null!; private Mock _configUnits = null!; private Mock _calcObs = null!; private Lazy _lazyCalc = null!; private Mock _http = null!; private Mock _audit = null!; private Mock> _logger = null!; private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); private static readonly DateTime Now = DateTime.UtcNow; /// /// Initializes the unit test fixture by creating mock repositories, services, and a mocked with a test claims principal, then instantiates the under test using the configured API settings (5-second pump expiration, zero pump messages disabled). Pass-through mappings are configured for , , and so that supplied pump observations are returned unchanged. /// /// [SetUp] public void Setup() { _obsRepo = new Mock(); _stateRepo = new Mock(); _alarmEventRepo = new Mock(); _alarmStateRepo = new Mock(); _archiveRepo = new Mock(); _patientSvc = new Mock(); _configPumps = new Mock(); _subs = new Mock(); _clientMsg = new Mock(); _calcObs = new Mock(); _lazyCalc = new Lazy(() => _calcObs.Object); _http = new Mock(); _audit = new Mock(); _logger = new Mock>(); _configUnits = new Mock(); var principal = new ClaimsPrincipal( new ClaimsIdentity([new Claim(ClaimTypes.Name, "test")], "mock")); _http.Setup(x => x.HttpContext) .Returns(new DefaultHttpContext { User = principal }); _calcObs.Setup(x => x.Map(It.IsAny())) .ReturnsAsync((PumpObservation o) => o); var api = Options.Create(new ApiSettings { PumpExpiresSeconds = 5, SendPumpsZero = false }); _service = new PumpService( _obsRepo.Object, _stateRepo.Object, _alarmEventRepo.Object, _alarmStateRepo.Object, _archiveRepo.Object, _patientSvc.Object, _configPumps.Object, api, _logger.Object, _subs.Object, _clientMsg.Object, _lazyCalc, _http.Object, _audit.Object, _configUnits.Object ); _configPumps.Setup(x => x.Map(It.IsAny())) .ReturnsAsync((PumpObservation o) => o); _configUnits.Setup(x => x.Map(It.IsAny())) .ReturnsAsync((PumpObservation o) => o); } // -------------------------------------------------------------- // SAVE REQUEST — casos básicos // -------------------------------------------------------------- /// /// Verifies that processing does not insert any records /// when the request is saved without associated observations. /// /// [Test] public async Task SaveRequest_Returns_When_No_Observations() { var req = new ApiRequest { Type = "ORU_R01" }; await _service.SaveRequest(req); _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never); } /// /// Verifies that SaveRequest does not insert a when the is an unrecognized value. /// /// [Test] public async Task SaveRequest_UnknownType_DoesNotInsert() { var req = new ApiRequest { Type = "UNKNOWN", PumpObservation = new PumpObservation { Time = Now } }; await _service.SaveRequest(req); _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never); } /// /// Verifies that SaveRequest converts a single PumpObservation on an incoming /// ApiRequest into a list with one entry on the request after processing. /// /// [Test] public async Task SaveRequest_Converts_SingleObservation_ToList() { var obs = new PumpObservation { Time = Now }; var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "123" }; _patientSvc.Setup(p => p.FindPatientByApiRequest(req)) .ReturnsAsync(new Patient { Id = PatientId }); await _service.SaveRequest(req); Assert.That(req.PumpObservations, Has.Count.EqualTo(1)); } /// /// Verifies that Service.SaveRequest sets the Expires property of the before persisting it via the repository. /// /// [Test] public async Task SaveRequest_SetsExpires_BeforeInsert() { var obs = new PumpObservation { Time = Now, DeviceId = "D1" }; var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" }; _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) .ReturnsAsync(new Patient { Id = PatientId }); await _service.SaveRequest(req); _obsRepo.Verify(r => r.InsertAsync( It.Is(o => o.Expires == 5)), Times.Once); } // -------------------------------------------------------------- // PROCESS ALARM // -------------------------------------------------------------- /// /// Verifies that saving a request of type "ORU_R40" creates a /// and does not create a , ensuring alarm-phase events are /// routed to the alarm event store rather than the observation store. /// /// [Test] public async Task SaveRequest_ORU_R40_CreatesAlarmEvent() { var obs = new PumpObservation { Time = Now, DeviceId = "D1", AlarmType = PumpEnum.AlarmType.Occlusion, EventPhase = PumpEnum.EventPhase.Start }; var req = new ApiRequest { Type = "ORU_R40", PumpObservation = obs }; _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) .ReturnsAsync(new Patient { Id = PatientId }); await _service.SaveRequest(req); _alarmEventRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Once); _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never); } /// /// Verifies that processing a pump observation with EventPhase.End removes the corresponding alarm state /// by calling RemoveAsync on the alarm state repository with the matching device, alarm type, and MDC code. /// /// [Test] public async Task ProcessAlarm_End_RemovesAlarmState() { var obs = new PumpObservation { Time = Now, DeviceId = "DX", AlarmType = PumpEnum.AlarmType.Occlusion, AlarmTypeMdc = "H1", EventPhase = PumpEnum.EventPhase.End }; var req = new ApiRequest { Type = "ORU_R40", PumpObservation = obs }; _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) .ReturnsAsync(new Patient { Id = PatientId }); await _service.SaveRequest(req); _alarmStateRepo.Verify(r => r.RemoveAsync("DX", PumpEnum.AlarmType.Occlusion, "H1"), Times.Once); } // -------------------------------------------------------------- // SNAPSHOT DE BOMBA // -------------------------------------------------------------- /// /// Verifies that SaveRequest creates and upserts a new /// for the device when no existing pump state is found in the repository. /// /// [Test] public async Task SaveRequest_CreatesPumpState_IfNotExists() { var obs = new PumpObservation { Time = Now, DeviceId = "P1" }; var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" }; _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) .ReturnsAsync(new Patient { Id = PatientId }); _stateRepo.Setup(r => r.FindByDeviceIdAsync("P1")) .ReturnsAsync((PumpState?)null); await _service.SaveRequest(req); _stateRepo.Verify(r => r.UpsertAsync(It.Is(s => s.DeviceId == "P1")), Times.Once); } // -------------------------------------------------------------- // BROADCAST // -------------------------------------------------------------- /// /// Verifies that SaveRequest does not broadcast a message via the client when there are no active subscribers. /// /// A task that completes when the assertion has been executed. /// [Test] public async Task SaveRequest_NoSubscribers_NoBroadcast() { var obs = new PumpObservation { Time = Now, DeviceId = "BR1", PatientId = PatientId }; var patient = new Patient { Id = PatientId, Location = new PatientLocation("U1", "B1") }; var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" }; _subs.Setup(x => x.GetSubscribers()).Returns([]); _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) .ReturnsAsync(patient); await _service.SaveRequest(req); _clientMsg.Verify(r => r.SendAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } /// /// Verifies that falls back to when the patient cannot be resolved from the API request, broadcasts the resulting to subscribers matching that location, and skips messages when no active alarms exist. /// /// [Test] public async Task SaveRequest_WithSubscribers_UsesReqLocation_AndSendsBroadcast() { // Arrange var obs = new PumpObservation { Time = Now, DeviceId = "D22", PatientId = null // <- clave para que se use req.Location }; var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, Location = new PatientLocation("UCI5C", "Box4", "Room1"), PatientNumber = "437537" }; var subscriber = new WsSubscriber("sub1") { Locations = [ new PatientLocation("UCI5C", "Box4", "Room1") ] }; // 1) NO devolver paciente en FindPatientByApiRequest -> fuerza uso de req. Location _patientSvc.Setup(p => p.FindPatientByApiRequest(req)) .ReturnsAsync((Patient?)null); // 2) Suscriptores con la misma ubicación que req.Location _subs.Setup(x => x.GetSubscribers()) .Returns([subscriber]); // 3) Evitar null en alarmas activas al enumerar (foreach) _alarmStateRepo.Setup(r => r.FindAllActiveByDeviceAsync("D22")) .ReturnsAsync(Enumerable.Empty()); // 4) Snapshot: por claridad, fuerza que se cree uno nuevo _stateRepo.Setup(r => r.FindByDeviceIdAsync("D22")) .ReturnsAsync((PumpState?)null); // Map mínimo para que avance el flujo _configPumps.Setup(m => m.Map(It.IsAny())) .ReturnsAsync((PumpObservation o) => o); _configUnits.Setup(m => m.Map(It.IsAny())) .ReturnsAsync((PumpObservation o) => o); // Act await _service.SaveRequest(req); // Assert _clientMsg.Verify(x => x.SendAsync("sub1", OperationType.Pump, It.IsAny()), Times.Once); // No se envían PumpAlarm si no hay activas _clientMsg.Verify(x => x.SendAsync("sub1", OperationType.PumpAlarm, It.IsAny()), Times.Never); } // -------------------------------------------------------------- // RETENCIÓN — DeleteOlderDays // -------------------------------------------------------------- /// /// Verifies that SaveRequest invokes the observation repository's /// DeleteOlderThanDaysAsync method with the configured retention value when the /// retention policy returned for the pump observation is DeleteOlderDays. /// /// [Test] public async Task SaveRequest_Retention_DeleteOlderDays_CallsRepo() { var obs = new PumpObservation { Time = Now, DeviceId = "R1", PatientId = PatientId }; var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" }; _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) .ReturnsAsync(new Patient { Id = PatientId }); _configPumps.Setup(x => x.RetentionActions(It.IsAny())) .ReturnsAsync(new ObservatitonRetentionResult { RetentionPolicy = RetentionPolicy.DeleteOlderDays, RetentionPolicyValue = 7 }); await _service.SaveRequest(req); _obsRepo.Verify(r => r.DeleteOlderThanDaysAsync(7, null), Times.Once); } // -------------------------------------------------------------- // PAGINACIÓN // -------------------------------------------------------------- /// /// Verifies that GetPaginatedPump returns the correct paginated results when filtering by patient identifier. /// Ensures the first page contains the most recent observation within the configured time tolerance. /// /// [Test] public async Task GetPaginatedPump_ByPatient_Works() { var obs = new List { new() { Time = Now.AddMinutes(-1), PatientId = PatientId }, new() { Time = Now.AddMinutes(-5), PatientId = PatientId } }; _obsRepo.Setup(r => r.FindByPatientId(PatientId)) .ReturnsAsync(obs); var fileredRequest = new FilteredRequest { PatientId = PatientId.ToString() }; var filter = new PaginationFilter(1, 1, fileredRequest); var result = await _service.GetPaginatedPump(filter); using (Assert.EnterMultipleScope()) { Assert.That(result!.Data, Has.Count.EqualTo(1)); Assert.That(result.Data[0].Time, Is.EqualTo(Now.AddMinutes(-1)).Within(TimeSpan.FromMilliseconds(2))); } } // -------------------------------------------------------------- // ARCHIVO // -------------------------------------------------------------- /// /// Verifies that ArchiveByPatientId inserts the patient's pump observations into the archive /// repository and then deletes them, along with their related alarm events and alarm states. /// /// [Test] public async Task ArchiveByPatientId_InsertsArchive_ThenDeletes() { var list = new List { new() { Id = ObjectId.GenerateNewId(), PatientId = PatientId } }; _obsRepo.Setup(r => r.FindByPatientId(PatientId)).ReturnsAsync(list); await _service.ArchiveByPatientId(PatientId); _archiveRepo.Verify(r => r.InsertManyAsync(list), Times.Once); _obsRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once); _alarmEventRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once); _alarmStateRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once); } // -------------------------------------------------------------- // UPDATE MANY // -------------------------------------------------------------- /// /// Verifies that UpdateManyObjectId correctly updates the patient identifier across observations, alarms, and alarm state repositories. /// /// A task representing the asynchronous test execution. /// [Test] public async Task UpdateManyObjectId_UpdatesObs_Alarms_States() { var oldId = ObjectId.GenerateNewId(); var newId = ObjectId.GenerateNewId(); _obsRepo.Setup(r => r.UpdateManyObjectIdByFieldAsync("patientId", newId, oldId)) .ReturnsAsync(3); _alarmEventRepo.Setup(r => r.UpdateManyObjectIdByFiledNameAsync("patientId", newId, oldId)) .ReturnsAsync(1); _alarmStateRepo.Setup(r => r.UpdateManyObjectIdByFieldNameAsync("patientId", newId, oldId)) .ReturnsAsync(2); await _service.UpdateManyObjectId("patientId", newId, oldId); } // -------------------------------------------------------------- // FIND LAST OBSERVATIONS // -------------------------------------------------------------- /// /// Verifies that FindLastPumpObservations returns pump observations ordered with the most recent first, /// returning only the specified number of latest entries when the repository provides multiple observations. /// /// [Test] public async Task FindLastPumpObservations_ReturnsOrdered() { var items = new List { new() { Time = Now.AddMinutes(-10) }, new() { Time = Now.AddMinutes(-1) } }; _obsRepo.Setup(r => r.FindByPatientId(PatientId)) .ReturnsAsync(items); var result = await _service.FindLastPumpObservations(PatientId, 1); Assert.That(result, Has.Count.EqualTo(1)); Assert.That(result[0].Time, Is.EqualTo(items[1].Time)); } // -------------------------------------------------------------- // INSERT MANUAL // -------------------------------------------------------------- /// /// Verifies that InsertPumpObservation persists the observation and upserts the corresponding /// pump state when no existing state is found for the device and there are no active subscribers. /// /// [Test] public async Task InsertPumpObservation_Inserts_AndBroadcasts() { var obs = new PumpObservation { DeviceId = "D11", PatientId = PatientId, Time = Now }; _stateRepo.Setup(r => r.FindByDeviceIdAsync("D11")) .ReturnsAsync((PumpState?)null); _subs.Setup(x => x.GetSubscribers()).Returns([]); await _service.InsertPumpObservation(obs); _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Once); _stateRepo.Verify(r => r.UpsertAsync(It.IsAny()), Times.Once); } }