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; [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); } [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); } }