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;
///
/// Contains unit tests that verify the behavior of the class.
///
///
/// Marked as a test fixture so that the contained test methods are discovered and executed by the test runner.
///
///
[TestFixture]
public class SchedulerServiceTest
{
///
/// One-time setup that initializes mocked services, lazy wrappers, and test data
/// required to exercise along with the Quartz-based
/// news and expiration jobs (, ,
/// ). It configures and
/// behaviors to match observations by name and patient, and the
/// to return the predefined patient list from
/// , then starts the underlying scheduler.
///
///
[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]
///
/// Stops the Quartz.NET scheduler engine after all tests in the fixture have run. Uses the null-conditional operator so the call is safely skipped when the scheduler was never initialized.
///
///
[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
}