using System.Collections.Specialized; 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 adas_core.Domain.Models.Providers; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using Moq; using Quartz; using Quartz.Impl; using Options = Microsoft.Extensions.Options.Options; namespace adas_core.Test.Services; [TestFixture] public class SchedulerServiceTest { [OneTimeSetUp] public async Task SetUp() { #region [Services Mock seUp] _treatmentServiceMock = new Mock(); _treatmentServiceLazy = new Lazy(() => _treatmentServiceMock.Object); _patientServiceMock = new Mock(); _patientServiceLazy = new Lazy(() => _patientServiceMock.Object); _medicineServiceMock = new Mock(); _medicineServiceLazy = new Lazy(() => _medicineServiceMock.Object); _observationServiceMock = new Mock(); _observationServiceLazy = new Lazy(() => _observationServiceMock.Object); _configObservationServiceMock = new Mock(); var configObservationServiceLazy = new Lazy(() => _configObservationServiceMock.Object); _logger = new Mock>(); _httpClientFactoryMock = new Mock(); _httpMessageHandlerMock = new Mock(); _calculatedObservationsServiceMock = new Mock(); _calculatedObservationsServiceLazy = new Lazy(() => _calculatedObservationsServiceMock.Object); _appointmentServiceServiceMock = new Mock(); _appointmentServiceServiceLazy = new Lazy(() => _appointmentServiceServiceMock.Object); _diagnosisServiceMock = new Mock(); _diagnosisServiceLazy = new Lazy(() => _diagnosisServiceMock.Object); var client = new HttpClient(_httpMessageHandlerMock.Object); _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); _optionsApiSettings = Options.Create(_apiSettings); _optionsProvidersSettings = Options.Create(_providersSettings); #endregion var unused = new SchedulerService( _optionsApiSettings, _optionsProvidersSettings, _patientServiceLazy, _treatmentServiceLazy, _appointmentServiceServiceLazy, _diagnosisServiceLazy, _medicineServiceLazy, _observationServiceLazy, configObservationServiceLazy, _logger.Object, _httpClientFactoryMock.Object, _calculatedObservationsServiceLazy, _patientCarePlanLazy ); #region [Service item fill] _sinceDate = Now.AddHours(-24); _sinceDischargeTimeToArchive = 48; _archivePatientsWithoutObservationsSinceHours = 28; _jobDataMap = new JobDataMap { { "sinceDate", _sinceDate }, { "sinceDischargeTime", _sinceDischargeTimeToArchive }, { "archivePatientsWithoutObservationsSinceHours", _archivePatientsWithoutObservationsSinceHours }, { "checkNewsJobIntervalMinutes", 15 } }; // Grab the Scheduler instance from the Factory var properties = new NameValueCollection { { "quartz.scheduler.instanceName", "MyUniqueSchedulerName" } }; _factory = new StdSchedulerFactory(properties); if (_scheduler == null) { var getFactory = await _factory.GetScheduler(); _scheduler = getFactory; } // Set up observation list var itemList = new List { new() { Name = "Resp_Rate_Calculated", CodingSystem = "ADAS", MinAlert = 14, MaxAlert = 25, Expires = 10, ShowOnExpired = false }, new() { Name = "SpO2", Code = "150456", OriginalName = "MDC_PULS_OXIM_SAT_O2", CodingSystem = "MDC", ParentCode = "69965", ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS", MinAlert = 94, MaxAlert = 100, ParentCodingSystem = "MDC", Expires = 10, ShowOnExpired = false }, new() { Name = "Temperature", Code = "386053000", OriginalName = "Temperatura", CodingSystem = "SNM", ParentCode = "386053000", ParentName = "Temperatura(ºC)", MinAlert = 34.5, MinWarn = 35.5, MaxWarn = 37.4, MaxAlert = 38, ParentCodingSystem = "SNM", Expires = 60 }, new() { CodingSystem = "MDC", Code = "150033", OriginalName = "MDC_PRESS_BLD_ART_SYS", Name = "TAs", Expires = 10 }, new() { Name = "FC", Code = "149514", OriginalName = "MDC_PULS_RATE", CodingSystem = "MDC", MinAlert = 60, MaxAlert = 100, Expires = 10, ShowOnExpired = false } }; var observations = new List { new() { Name = "Resp_Rate_Calculated", Value = 18, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId }, new() { Name = "SpO2", Value = 93, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId }, new() { Name = "Temperature", Value = 36.5, Time = DateTime.UtcNow, PatientId = PatientId }, new() { Name = "TAs", Value = 103, Time = DateTime.UtcNow, PatientId = PatientId }, new() { Name = "FC", Value = 80, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId }, new() { Name = "Resp_Rate_Calculated", Value = 18, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2 }, new() { Name = "SpO2", Value = 93, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2 }, new() { Name = "Temperature", Value = 36.5, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2 }, new() { Name = "TAs", Value = 103, Time = DateTime.UtcNow, PatientId = PatientId2 }, new() { Name = "FC", Value = 40, Time = DateTime.UtcNow, PatientId = PatientId2 } }; // Set Up patientList var patientList = new List { new() { Id = PatientId, UnitId = ObjectId.GenerateNewId(), PointOfCareId = ObjectId.GenerateNewId(), PatientNumber = "patientNumber1", Person = new Person { FirstName = "firstName1", LastName = "lastName1", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male } }, new() { Id = PatientId2, UnitId = ObjectId.GenerateNewId(), PointOfCareId = ObjectId.GenerateNewId(), PatientNumber = "patientNumber2", Person = new Person { FirstName = "firstName2", LastName = "lastName2", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male } } }; #endregion #region [Service behavior] // Set up configObservationServiceMock to return desire item _configObservationServiceMock .Setup(service => service.Get(It.IsAny(), It.IsAny())) .ReturnsAsync((BasePatientObservation obs, bool _) => { return itemList.FirstOrDefault(obsConfig => obsConfig.Name == obs.Name); }); // Set up observationService behavior _observationServiceMock .Setup(service => service.FindLastObservations(It.IsAny(), 1, It.IsAny>())) .ReturnsAsync((ObjectId patientId, int _, List? filterObservations) => { var filteredObservations = observations .Where(obs => filterObservations != null && obs.Name != null && filterObservations.Contains(obs.Name) && obs.PatientId == patientId) .ToList(); return filteredObservations; }); // Set up patientServiceMock to return patientList for FindInActivePoC _patientServiceMock.Setup(service => service.FindInActivePoC()).ReturnsAsync(patientList); #endregion // Asignación de dependencias estáticas para el job CalculateNewsJob.PatientService = _patientServiceMock.Object; CalculateNewsJob.ObservationService = _observationServiceMock.Object; CheckExpiredAlertsJob.ObservationService = _observationServiceMock.Object; CheckExpiredObservationsJob.ObservationService = _observationServiceMock.Object; CalculateNewsJob.ConfigObservationService = _configObservationServiceMock.Object; // and start it off _scheduler?.Start(); } //[TearDown] [OneTimeTearDown] public void TearDown() { // Detener el motor de Quartz.NET después de las pruebas //scheduler.Shutdown().Wait(); _scheduler?.Shutdown(); } private Mock? _treatmentServiceMock; private Mock? _patientServiceMock; private Mock? _medicineServiceMock; private Mock? _observationServiceMock; private Mock? _configObservationServiceMock; private Mock? _httpClientFactoryMock; private Mock? _httpMessageHandlerMock; private Mock? _calculatedObservationsServiceMock; private Mock? _appointmentServiceServiceMock; private Mock? _diagnosisServiceMock; private Lazy? _treatmentServiceLazy; private Lazy? _patientServiceLazy; private Lazy? _medicineServiceLazy; private Lazy? _observationServiceLazy; private Lazy _calculatedObservationsServiceLazy; private Lazy _appointmentServiceServiceLazy; private Lazy _diagnosisServiceLazy; private readonly Lazy _patientCarePlanLazy = new(); private readonly ApiSettings _apiSettings = new(); private IOptions? _optionsApiSettings; private readonly List _providersSettings = [ new() { Name = "Adas", Url = "http://localhost/info.html" } ]; private IOptions>? _optionsProvidersSettings; private Mock>? _logger; private JobDataMap _jobDataMap = new(); private DateTime _sinceDate; private int _sinceDischargeTimeToArchive; private int _archivePatientsWithoutObservationsSinceHours; private StdSchedulerFactory? _factory; private IScheduler? _scheduler; private static readonly DateTime Now = DateTime.Now; private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); private static readonly ObjectId PatientId2 = ObjectId.GenerateNewId(); /// /// Verifies that the is executed when its Quartz.NET trigger fires, /// ensuring the scheduled job invokes the patient discharge process for inactive patients at the configured interval. /// [Test] public void CheckInactivePatientsJob_ShouldExecuteOnTrigger() { var checkInactivePatientSchedulerIntervalHours = 12; // Arrange var checkInactivePatientsJob = JobBuilder.Create() .UsingJobData(_jobDataMap) .Build(); // Crear un disparador personalizado que incremente el contador var checkInactivePatientsTrigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(x => x .WithIntervalInHours(checkInactivePatientSchedulerIntervalHours) .RepeatForever()) .Build(); _patientServiceMock?.Setup(p => p.DischargeInactivePatients(_sinceDate, _sinceDischargeTimeToArchive)); // Asociar el trabajo y el desencadenador en el motor de Quartz.NET _scheduler?.ScheduleJob(checkInactivePatientsJob, checkInactivePatientsTrigger).Wait(); // Act // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces Thread.Sleep(TimeSpan.FromSeconds(1)); _patientServiceMock?.Verify(p => p.DischargeInactivePatients(It.IsAny(), It.IsAny()), Times.AtLeastOnce()); } /// /// Verifies that the is executed by the Quartz scheduler /// when triggered, and that the medicine service is invoked exactly once to retrieve medicines /// for the active treatments of the mocked patients. /// [Test] public void CheckActiveTreatmentsJob_ShouldExecuteOnTrigger() { var patient = new Patient { PatientId = PatientId.ToString(), UnitString = "NEONATAL", Bed = "CINA02", PatientNumber = "123456", Person = new Person { FirstName = "Jose", LastName = "Luis", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male } }; const int checkActiveTreatmentsSchedulerIntervalMinutes = 10; var treatmentsJob = JobBuilder.Create() .Build(); // Trigger the job to run now, and then repeat every 10 seconds var treatmentsTrigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(x => x .WithIntervalInMinutes(checkActiveTreatmentsSchedulerIntervalMinutes) .RepeatForever()) .Build(); _patientServiceMock?.Setup(p => p.FindAll(It.IsAny())).ReturnsAsync([patient]); // Create a mock of the singleton class subscribers var mockSingleton = new Mock(); // Set up the mock object to return a specific value when a method is called var patientTreatmentList = new List { new() { Id = ObjectId.GenerateNewId(), PatientId = PatientId, OrderControl = OrderControlType.Nw, PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, OrderStatus = "A", OrderTime = Now, Notes = [], Routes = [] } }; mockSingleton.Setup(x => x.GetActiveTreatmentsByPatient(PatientId)).ReturnsAsync(patientTreatmentList); _medicineServiceMock?.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) .ReturnsAsync(new List()); // Asociar el trabajo y el desencadenador en el motor de Quartz.NET _scheduler?.ScheduleJob(treatmentsJob, treatmentsTrigger).Wait(); // Act // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces Thread.Sleep(TimeSpan.FromSeconds(10)); _medicineServiceMock?.Verify(x => x.GetMedicinesOfTreatments(It.IsAny>()), Times.Once()); } /// /// Verifies that the is executed at least once by the Quartz.NET scheduler when triggered with a recurring schedule. /// [Test] public void CheckExpiredObservationsJob_ShouldExecuteOnTrigger() { var checkExpiredObservationsIntervalMinutes = 3; // Arrange var checkExpiredObservationsJob = JobBuilder.Create() .UsingJobData(_jobDataMap) .Build(); // Crear un disparador personalizado que incremente el contador var checkExpiredObservationsTrigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(x => x .WithIntervalInHours(checkExpiredObservationsIntervalMinutes) .RepeatForever()) .Build(); // Asociar el trabajo y el desencadenador en el motor de Quartz.NET _scheduler?.ScheduleJob(checkExpiredObservationsJob, checkExpiredObservationsTrigger).Wait(); // Act // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces Thread.Sleep(TimeSpan.FromSeconds(1)); _observationServiceMock?.Verify(p => p.ExpireObservationsAndRecalculateAsync(), Times.AtLeastOnce()); } /// /// Verifies that the is executed when triggered by the Quartz.NET scheduler, ensuring that the ExpireAlertsAndPowerOffAsync method on the observation service is invoked at least once. /// [Test] public void CheckExpiredAlertsJob_ShouldExecuteOnTrigger() { var checkExpiredAlertsIntervalMinutes = 3; // Arrange var checkExpiredAlertsJob = JobBuilder.Create() .UsingJobData(_jobDataMap) .Build(); // Crear un disparador personalizado que incremente el contador var checkExpiredAlertsTrigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(x => x .WithIntervalInHours(checkExpiredAlertsIntervalMinutes) .RepeatForever()) .Build(); // Asociar el trabajo y el desencadenador en el motor de Quartz.NET _scheduler?.ScheduleJob(checkExpiredAlertsJob, checkExpiredAlertsTrigger).Wait(); // Act // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces Thread.Sleep(TimeSpan.FromSeconds(1)); _observationServiceMock?.Verify(p => p.ExpireAlertsAndPowerOffAsync(), Times.AtLeastOnce()); } /// /// Verifies that the Quartz.NET job, when triggered, never invokes /// InsertObservation with a whose Name is not "NEWS", ensuring /// that only NEWS observations are considered for persistence during scheduled execution. /// [Test] public void GetProvidersObservationsJob_ShouldExecuteOnTrigger_Result_GetType_null() { var getProviderObservationsIntervalMinutes = 5; var patient = new Patient { PatientId = PatientId.ToString(), UnitString = "NEONATAL", Bed = "CINA02", PatientNumber = "123456", Person = new Person { FirstName = "Jose", LastName = "Luis", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male } }; // Arrange var getProvidersObservationsJob = JobBuilder.Create() .UsingJobData(_jobDataMap) .Build(); // Crear un disparador personalizado que incremente el contador var getProvidersObservationsTrigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(x => x .WithIntervalInHours(getProviderObservationsIntervalMinutes) .RepeatForever()) .Build(); _patientServiceMock?.Setup(p => p.FindAll(It.IsAny())).ReturnsAsync([patient]); // Asociar el trabajo y el desencadenador en el motor de Quartz.NET _scheduler?.ScheduleJob(getProvidersObservationsJob, getProvidersObservationsTrigger).Wait(); // Act // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces Thread.Sleep(TimeSpan.FromSeconds(1)); //observationServiceMock.Verify(p => p.InsertObservation(It.IsAny(),true,true), Times.AtLeastOnce()); _observationServiceMock?.Verify(p => p.InsertObservation( It.Is(obs => obs.Name != "NEWS"), true, true), Times.Never()); } /// /// Verifies that the is executed when triggered by the Quartz.NET scheduler, confirming that the associated InsertObservation call on the observation service is invoked at least once within the allowed execution window. /// [Test] public void CheckCalculateNewsJob_ShouldExecuteOnTrigger() { var checkExpiredAlertsIntervalMinutes = 3; // Arrange var calculateNewsJob = JobBuilder.Create() .UsingJobData(_jobDataMap) .Build(); // Crear un disparador personalizado que incremente el contador var checkExpiredAlertsTrigger = TriggerBuilder.Create() .StartNow() .WithSimpleSchedule(x => x .WithIntervalInHours(checkExpiredAlertsIntervalMinutes) .RepeatForever()) .Build(); // Asociar el trabajo y el desencadenador en el motor de Quartz.NET _scheduler?.ScheduleJob(calculateNewsJob, checkExpiredAlertsTrigger).Wait(); // Act // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces Thread.Sleep(TimeSpan.FromSeconds(6)); _observationServiceMock?.Verify(p => p.InsertObservation(It.IsAny(), true, true), Times.AtLeastOnce()); } /// /// Verifies that correctly inserts NEWS observations with the expected calculated values (1 and 4) for multiple patients in a single execution. /// [Test] public async Task CalculateNewsJob_Insert_ObsFull_MultiplePatient_With_Correct_Value() { var job = new CalculateNewsJob(); // Act await job.Execute(Mock.Of()); // Assert _observationServiceMock?.Verify( service => service.InsertObservation( It.Is(obs => obs.Name == "NEWS" && (int)obs.Value == 1 && obs.PatientId == PatientId), true, true), Times.Once); _observationServiceMock?.Verify( service => service.InsertObservation( It.Is(obs => obs.Name == "NEWS" && (int)obs.Value == 4 && obs.PatientId == PatientId2), true, true), Times.Once); } //Se deshabilita el mensaje de warning porque lo detecta como no usado y sugiere suprimirlo siendo necesario }