using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using AutoMapper; using Moq; using NUnit.Framework; using Strata.Analytics.Biz.DbContexts; using Strata.Analytics.Biz.Extracts.Models; using Strata.Analytics.Biz.Extracts.Services; using Strata.Analytics.Biz.Hangfire; using Strata.Analytics.Biz.Utils; using Strata.Analytics.Test.Unit.Biz.TestData; using Strata.CoreLib.Claims; using Strata.CoreLib.Claims.Extensions; using Strata.Testing.Categories; namespace Strata.Analytics.Test.Unit.Biz.Extracts { [Category(TestCategory.Unit)] [Parallelizable(ParallelScope.None)] public class ExtractScheduleTests { private IClaimsPrincipalAccessor _claimsPrincipalAccessor; private AnalyticsDbContext _dbContext; private IMapper _mapper; private IExtractScheduleService _scheduleService; private Mock _recurringJobService; private string _userTimeZone; private static readonly DateTime Now = DateTime.Now; private static readonly DateOnly NowDate = new(Now.Year, Now.Month, Now.Day); private static readonly TimeOnly NowTime = new(Now.Hour, Now.Minute); [OneTimeSetUp] public void OneTimeSetup() { _claimsPrincipalAccessor = IClaimsPrincipalAccessorMock.GetClaimsPrincipalAccessor(5, true); _dbContext = AnalyticsDbContextMock.GetAnalyticsDbContext(_claimsPrincipalAccessor); _mapper = AutoMapperMock.GetAutoMapper(); _recurringJobService = new Mock(); _scheduleService = new ExtractScheduleService(_dbContext, _claimsPrincipalAccessor, _mapper, _recurringJobService.Object); _dbContext.Extracts.AddRange(ExtractTestData.Extracts()); _dbContext.ExtractSchedule.AddRange(ExtractTestData.ExtractSchedules()); _dbContext.SaveChanges(); _userTimeZone = _claimsPrincipalAccessor.GetTimezone(); } [OneTimeTearDown] public async Task OneTimeTearDown() { await _dbContext.Database.EnsureDeletedAsync(); } [Test] public async Task CanCreate_And_Add_RecurringJob() { var now = DateTime.UtcNow; // arrange var saveInfo = new ExtractScheduleSaveInfo { ExtractId = 1, Enabled = true, Repeater = ScheduleRepeaterType.Daily, OnDays = "", StartDate = NowDate, StartTime = NowTime, MaxOccurrences = 12 }; // act var result = await _scheduleService.CreateAsync(saveInfo, CancellationToken.None); // assert Assert.IsNotNull(result); Assert.That(result.ExtractScheduleId, Is.GreaterThan(ExtractTestData.ExtractSchedules().Max(h => h.ExtractScheduleId))); Assert.That(result.ExtractId, Is.EqualTo(saveInfo.ExtractId)); Assert.That(result.Enabled, Is.EqualTo(saveInfo.Enabled)); Assert.That(result.ToCronString(), Is.EqualTo($"{NowTime.Minute} {NowTime.Hour} * * *")); // utc to CST conversion Assert.That(result.StartDate, Is.EqualTo(saveInfo.StartDate)); Assert.IsNull(result.EndDate); Assert.That(result.CreatedByUserGuid, Is.EqualTo(_claimsPrincipalAccessor.GetCurrentClaimsPrincipal().GetUserGuid())); Assert.That(result.UpdatedByUserGuid, Is.EqualTo(_claimsPrincipalAccessor.GetCurrentClaimsPrincipal().GetUserGuid())); Assert.IsNotNull(result.CreatedDate); Assert.IsNotNull(result.UpdatedDate); _recurringJobService.Verify( s => s.AddOrUpdateExtractExportJobAsync(It.Is(r => r.ExtractScheduleId == result.ExtractScheduleId && r.ExtractId == result.ExtractId && r.Cron == result.ToCronString()), It.IsAny()), Times.Once); } [Test] public async Task CanGetById() { // act var result = await _scheduleService.GetByIdAsync(1, CancellationToken.None); // assert Assert.IsNotNull(result); } [Test] public async Task CanUpdate() { // arrange var scheduleId = _dbContext.ExtractSchedule.First(s => s.MaxOccurrences.HasValue).ExtractId; var schedule = await _scheduleService.GetByIdAsync(scheduleId, CancellationToken.None); var saveInfo = new ExtractScheduleSaveInfo(); _mapper.Map(schedule, saveInfo); saveInfo.EndDate = NowDate.AddYears(1); saveInfo.MaxOccurrences = null; saveInfo.Repeater = ScheduleRepeaterType.Monthly; saveInfo.OnDays = ""; _dbContext.ChangeTracker.Clear(); // act var result = await _scheduleService.UpdateAsync(schedule.ExtractId, saveInfo, CancellationToken.None); // assert Assert.IsNotNull(result); Assert.That(result.ExtractId, Is.EqualTo(schedule.ExtractId)); Assert.That(result.EndDate, Is.EqualTo(saveInfo.EndDate)); Assert.IsNull(result.MaxOccurrences); Assert.That(result.ToCronString(), Is.EqualTo($"{schedule.StartTime.Minute} {schedule.StartTime.Hour} {schedule.StartDate.Day} * *")); Assert.That(result.CreatedByUserGuid, Is.EqualTo(schedule.CreatedByUserGuid)); Assert.That(result.UpdatedByUserGuid, Is.EqualTo(_claimsPrincipalAccessor.GetCurrentClaimsPrincipal().GetUserGuid())); Assert.That(result.CreatedDate, Is.EqualTo(schedule.CreatedDate)); Assert.That(result.UpdatedDate, Is.EqualTo(DateTimeOffset.UtcNow).Within(500).Milliseconds); // this could fail in debug } [Test] public async Task CanGetAll_FilterNotEnabled() { // arrange var now = DateTime.UtcNow; // arrange var saveInfo = new ExtractScheduleSaveInfo { ExtractId = 1, Enabled = false, Repeater = ScheduleRepeaterType.Daily, OnDays = "", StartDate = NowDate, StartTime = NowTime, MaxOccurrences = 12 }; var disabledSchedule = await _scheduleService.CreateAsync(saveInfo, CancellationToken.None); // act var result = (await _scheduleService.GetAllAsync(CancellationToken.None)).ToList(); // assert Assert.IsNotNull(result); Assert.That(result.Count, Is.GreaterThan(0)); Assert.IsFalse(result.Select(s => s.ExtractScheduleId).Contains(disabledSchedule.ExtractScheduleId)); Assert.IsTrue(result.TrueForAll(s => s.Enabled)); } [Test] public async Task Disabling_Removes_RecurringJob() { // arrange var scheduleId = _dbContext.ExtractSchedule.Last(s => s.Enabled).ExtractId; var schedule = await _scheduleService.GetByIdAsync(scheduleId, CancellationToken.None); var saveInfo = new ExtractScheduleSaveInfo(); _mapper.Map(schedule, saveInfo); saveInfo.Enabled = false; _dbContext.ChangeTracker.Clear(); // act await _scheduleService.UpdateAsync(schedule.ExtractId, saveInfo, CancellationToken.None); // assert _recurringJobService.Verify( s => s.RemoveExtractExportJobAsync(It.IsAny(), It.Is(r => r == schedule.ExtractScheduleId), It.IsAny()), Times.Once); } [Test] public async Task HittingMaxOccurrenceLimit_Removes_RecurringJob() { var now = DateTime.UtcNow; // arrange var saveInfo = new ExtractScheduleSaveInfo { ExtractId = 1, Enabled = true, Repeater = ScheduleRepeaterType.Daily, OnDays = "", StartDate = NowDate.AddDays(-7), // starting 7 days ago StartTime = NowTime, MaxOccurrences = 6 // run a total of 6 times }; var schedule = await _scheduleService.CreateAsync(saveInfo, CancellationToken.None); _dbContext.ChangeTracker.Clear(); // act var result = await _scheduleService.UpdateLastOccurrence(schedule.ExtractScheduleId, DateTimeOffset.UtcNow, _userTimeZone, CancellationToken.None); // assert Assert.That(result.Enabled, Is.EqualTo(false)); _recurringJobService.Verify( s => s.RemoveExtractExportJobAsync(It.IsAny(), It.Is(r => r == schedule.ExtractScheduleId), It.IsAny()), Times.Once); } [Test] public async Task HittingEndDateLimit_Removes_RecurringJob() { var now = DateTime.UtcNow; // arrange var saveInfo = new ExtractScheduleSaveInfo { ExtractId = 1, Enabled = true, Repeater = ScheduleRepeaterType.Daily, OnDays = "", StartDate = NowDate.AddDays(-7), // starting 7 days ago StartTime = NowTime, EndDate = NowDate // run until today }; var schedule = await _scheduleService.CreateAsync(saveInfo, CancellationToken.None); _dbContext.ChangeTracker.Clear(); // act var result = await _scheduleService.UpdateLastOccurrence(schedule.ExtractScheduleId, DateTimeOffset.UtcNow, _userTimeZone, CancellationToken.None); // assert Assert.That(result.Enabled, Is.EqualTo(false)); _recurringJobService.Verify( s => s.RemoveExtractExportJobAsync(It.IsAny(), It.Is(r => r == schedule.ExtractScheduleId), It.IsAny()), Times.Once); } [Test] public async Task CanDelete_And_Remove_RecurringJob() { var now = DateTime.UtcNow; // arrange var saveInfo = new ExtractScheduleSaveInfo { ExtractId = 1, Enabled = true, Repeater = ScheduleRepeaterType.Daily, OnDays = "", StartDate = NowDate.AddDays(-7), // starting 7 days ago StartTime = NowTime, }; var schedule = await _scheduleService.CreateAsync(saveInfo, CancellationToken.None); _dbContext.ChangeTracker.Clear(); // act await _scheduleService.DeleteAsync(schedule.ExtractScheduleId, CancellationToken.None); // assert var result = await _scheduleService.ContainsAsync(schedule.ExtractScheduleId, CancellationToken.None); Assert.That(result, Is.EqualTo(false)); _recurringJobService.Verify( s => s.RemoveExtractExportJobAsync(It.IsAny(), It.Is(r => r == schedule.ExtractScheduleId), It.IsAny()), Times.Once); } [Test] public void CanGetDailyCronFromSchedule() { // arrange var schedule = new ExtractSchedule { Repeater = ScheduleRepeaterType.Daily, OnDays = "", StartDate = new DateOnly(2023, 01, 01), StartTime = new TimeOnly(15, 30, 00) }; // assert Assert.That(schedule.ToCronString(), Is.EqualTo("30 15 * * *")); } [Test] public void CanGetWeekdayCronFromSchedule() { // arrange var schedule = new ExtractSchedule { Repeater = ScheduleRepeaterType.Weekdays, StartDate = new DateOnly(2023, 01, 01), StartTime = new TimeOnly(15, 30, 00) }; // assert Assert.That(schedule.ToCronString(), Is.EqualTo("30 15 * * MON-FRI")); } [TestCase("MON,WED,FRI")] [TestCase("SUN,SAT")] [TestCase("SUN,THU")] public void CanGetWeeklyCronFromSchedule(string onDays) { // arrange var schedule = new ExtractSchedule { Repeater = ScheduleRepeaterType.Weekly, OnDays = onDays, StartDate = new DateOnly(2023, 01, 01), StartTime = new TimeOnly(15, 30, 00) }; // assert Assert.That(schedule.ToCronString(), onDays == string.Empty ? Is.EqualTo("30 15 * * *") : Is.EqualTo($"30 15 * * {onDays}")); } [Test] public void CanGetMonthlyCronFromSchedule() { // arrange var schedule = new ExtractSchedule { Repeater = ScheduleRepeaterType.Monthly, OnDays = "", StartDate = new DateOnly(2023, 01, 01), StartTime = new TimeOnly(15, 30, 00) }; // assert Assert.That(schedule.ToCronString(), Is.EqualTo("30 15 1 * *")); } } }