diff --git a/test/ClinicalScheduler/EvaluationPolicyServiceTest.cs b/test/ClinicalScheduler/EvaluationPolicyServiceTest.cs index 1c4d0af3..e1a2ddff 100644 --- a/test/ClinicalScheduler/EvaluationPolicyServiceTest.cs +++ b/test/ClinicalScheduler/EvaluationPolicyServiceTest.cs @@ -91,7 +91,7 @@ public void RequiresPrimaryEvaluator_RotationClosed_ReturnsFalse() }; // Act - Pass rotationClosed = true - var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceWeekSize: 2, rotationClosed: true); + var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceMinConsecutiveWeeks: 2, rotationClosed: true); // Assert Assert.False(result); @@ -99,10 +99,10 @@ public void RequiresPrimaryEvaluator_RotationClosed_ReturnsFalse() #endregion - #region WeekSize = 1 Tests + #region MinConsecutiveWeeks = 1 Tests [Fact] - public void RequiresPrimaryEvaluator_WeekSize1_NonExtendedWeek_ReturnsTrue() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks1_NonExtendedWeek_ReturnsTrue() { // Arrange var weeks = new List @@ -111,14 +111,14 @@ public void RequiresPrimaryEvaluator_WeekSize1_NonExtendedWeek_ReturnsTrue() }; // Act - var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceWeekSize: 1); + var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceMinConsecutiveWeeks: 1); // Assert Assert.True(result); } [Fact] - public void RequiresPrimaryEvaluator_WeekSize1_ExtendedWeek_ReturnsFalse() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks1_ExtendedWeek_ReturnsFalse() { // Arrange var weeks = new List @@ -127,7 +127,7 @@ public void RequiresPrimaryEvaluator_WeekSize1_ExtendedWeek_ReturnsFalse() }; // Act - var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceWeekSize: 1); + var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceMinConsecutiveWeeks: 1); // Assert Assert.False(result); @@ -135,10 +135,10 @@ public void RequiresPrimaryEvaluator_WeekSize1_ExtendedWeek_ReturnsFalse() #endregion - #region WeekSize = 2 Tests + #region MinConsecutiveWeeks = 2 Tests [Fact] - public void RequiresPrimaryEvaluator_WeekSize2_StartWeek_ReturnsFalse() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks2_StartWeek_ReturnsFalse() { // Arrange - First week of a 2-week block var weeks = new List @@ -148,14 +148,14 @@ public void RequiresPrimaryEvaluator_WeekSize2_StartWeek_ReturnsFalse() }; // Act - var result = _service.RequiresPrimaryEvaluator(28, weeks, serviceWeekSize: 2); + var result = _service.RequiresPrimaryEvaluator(28, weeks, serviceMinConsecutiveWeeks: 2); // Assert Assert.False(result); // First week doesn't need evaluator } [Fact] - public void RequiresPrimaryEvaluator_WeekSize2_SecondWeek_ReturnsTrue() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks2_SecondWeek_ReturnsTrue() { // Arrange - Second week of a 2-week block var weeks = new List @@ -165,14 +165,14 @@ public void RequiresPrimaryEvaluator_WeekSize2_SecondWeek_ReturnsTrue() }; // Act - var result = _service.RequiresPrimaryEvaluator(29, weeks, serviceWeekSize: 2); + var result = _service.RequiresPrimaryEvaluator(29, weeks, serviceMinConsecutiveWeeks: 2); // Assert Assert.True(result); // Second week needs evaluator } [Fact] - public void RequiresPrimaryEvaluator_WeekSize2_ThreeWeekRotationWithExtended_ReturnsFalse() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks2_ThreeWeekRotationWithExtended_ReturnsFalse() { // Arrange - 3-week rotation where week 3 is extended var weeks = new List @@ -183,14 +183,14 @@ public void RequiresPrimaryEvaluator_WeekSize2_ThreeWeekRotationWithExtended_Ret }; // Act - Check week 32 (would normally be evaluation week, but week 33 is extended) - var result = _service.RequiresPrimaryEvaluator(32, weeks, serviceWeekSize: 2); + var result = _service.RequiresPrimaryEvaluator(32, weeks, serviceMinConsecutiveWeeks: 2); // Assert Assert.False(result); // No evaluation needed because week 3 is extended } [Fact] - public void RequiresPrimaryEvaluator_WeekSize2_RegularTwoWeekBlock_SecondWeekTrue() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks2_RegularTwoWeekBlock_SecondWeekTrue() { // Arrange - Regular 2-week block var weeks = new List @@ -201,14 +201,14 @@ public void RequiresPrimaryEvaluator_WeekSize2_RegularTwoWeekBlock_SecondWeekTru }; // Act - Check week 36 (second week of block) - var result = _service.RequiresPrimaryEvaluator(36, weeks, serviceWeekSize: 2); + var result = _service.RequiresPrimaryEvaluator(36, weeks, serviceMinConsecutiveWeeks: 2); // Assert Assert.True(result); // Second week needs evaluator } [Fact] - public void RequiresPrimaryEvaluator_WeekSize2_LastWeekOfYear_ReturnsTrue() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks2_LastWeekOfYear_ReturnsTrue() { // Arrange - Last week with no next week var weeks = new List @@ -218,7 +218,7 @@ public void RequiresPrimaryEvaluator_WeekSize2_LastWeekOfYear_ReturnsTrue() }; // Act - Check week 43 (last week, no next week) - var result = _service.RequiresPrimaryEvaluator(43, weeks, serviceWeekSize: 2); + var result = _service.RequiresPrimaryEvaluator(43, weeks, serviceMinConsecutiveWeeks: 2); // Assert Assert.True(result); // Last week needs evaluator @@ -289,7 +289,7 @@ public void RequiresPrimaryEvaluator_Rotation603Year2026_CorrectEvaluation(int w var relevantWeeks = weeks.Where(w => w.WeekNum >= weekNum - 1 && w.WeekNum <= weekNum + 1).ToList(); // Act - var result = _service.RequiresPrimaryEvaluator(weekNum, relevantWeeks, serviceWeekSize: 2); + var result = _service.RequiresPrimaryEvaluator(weekNum, relevantWeeks, serviceMinConsecutiveWeeks: 2); // Assert Assert.Equal(shouldRequireEvaluator, result); @@ -297,10 +297,10 @@ public void RequiresPrimaryEvaluator_Rotation603Year2026_CorrectEvaluation(int w #endregion - #region No WeekSize Configuration Tests + #region No MinConsecutiveWeeks Configuration Tests [Fact] - public void RequiresPrimaryEvaluator_NoWeekSize_ReturnsFalse() + public void RequiresPrimaryEvaluator_NoMinConsecutiveWeeks_ReturnsFalse() { // Arrange var weeks = new List @@ -308,17 +308,17 @@ public void RequiresPrimaryEvaluator_NoWeekSize_ReturnsFalse() new() { WeekNum = 5, ExtendedRotation = false } }; - // Act - No serviceWeekSize provided + // Act - No serviceMinConsecutiveWeeks provided var result = _service.RequiresPrimaryEvaluator(5, weeks); // Assert - Assert.False(result); // Default behavior when no weekSize + Assert.False(result); // Default behavior when no MinConsecutiveWeeks } [Theory] [InlineData(0)] [InlineData(-1)] - public void RequiresPrimaryEvaluator_InvalidWeekSize_ReturnsFalse(int weekSize) + public void RequiresPrimaryEvaluator_InvalidMinConsecutiveWeeks_ReturnsFalse(int MinConsecutiveWeeks) { // Arrange var weeks = new List @@ -327,15 +327,15 @@ public void RequiresPrimaryEvaluator_InvalidWeekSize_ReturnsFalse(int weekSize) }; // Act - var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceWeekSize: weekSize); + var result = _service.RequiresPrimaryEvaluator(5, weeks, serviceMinConsecutiveWeeks: MinConsecutiveWeeks); // Assert - Assert.False(result); // Invalid weekSize (0, negative) defaults to false + Assert.False(result); // Invalid MinConsecutiveWeeks (0, negative) defaults to false } [Fact] - public void RequiresPrimaryEvaluator_WeekSize4_OnlyLastWeekRequiresEvaluation() + public void RequiresPrimaryEvaluator_MinConsecutiveWeeks4_OnlyLastWeekRequiresEvaluation() { // Arrange - 4-week rotation blocks var weeks = new List @@ -348,10 +348,10 @@ public void RequiresPrimaryEvaluator_WeekSize4_OnlyLastWeekRequiresEvaluation() }; // Act & Assert - For 4-week blocks, only the last week (4th) should require evaluation - Assert.False(_service.RequiresPrimaryEvaluator(10, weeks, serviceWeekSize: 4)); // First week - Assert.False(_service.RequiresPrimaryEvaluator(11, weeks, serviceWeekSize: 4)); // Second week - Assert.False(_service.RequiresPrimaryEvaluator(12, weeks, serviceWeekSize: 4)); // Third week - Assert.True(_service.RequiresPrimaryEvaluator(13, weeks, serviceWeekSize: 4)); // Fourth week (last) + Assert.False(_service.RequiresPrimaryEvaluator(10, weeks, serviceMinConsecutiveWeeks: 4)); // First week + Assert.False(_service.RequiresPrimaryEvaluator(11, weeks, serviceMinConsecutiveWeeks: 4)); // Second week + Assert.False(_service.RequiresPrimaryEvaluator(12, weeks, serviceMinConsecutiveWeeks: 4)); // Third week + Assert.True(_service.RequiresPrimaryEvaluator(13, weeks, serviceMinConsecutiveWeeks: 4)); // Fourth week (last) } #endregion diff --git a/test/ClinicalScheduler/RotationMappingExtensionsTests.cs b/test/ClinicalScheduler/RotationMappingExtensionsTests.cs index 85a41612..c6731429 100644 --- a/test/ClinicalScheduler/RotationMappingExtensionsTests.cs +++ b/test/ClinicalScheduler/RotationMappingExtensionsTests.cs @@ -22,7 +22,7 @@ public void ToDto_MapsRotationPropertiesCorrectly() ServiceId = 10, ServiceName = "Surgery Service", ShortName = "Surgery", - WeekSize = 2, + MinConsecutiveWeeks = 2, ScheduleEditPermission = "SVMSecure.ClnSched.EditSurgery" } }; diff --git a/test/ClinicalScheduler/RotationsControllerTest.cs b/test/ClinicalScheduler/RotationsControllerTest.cs index 974b416b..5036b080 100644 --- a/test/ClinicalScheduler/RotationsControllerTest.cs +++ b/test/ClinicalScheduler/RotationsControllerTest.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using MockQueryable.NSubstitute; using NSubstitute; using Viper.Areas.ClinicalScheduler.Controllers; using Viper.Areas.ClinicalScheduler.Models.DTOs.Responses; using Viper.Areas.ClinicalScheduler.Services; +using Viper.Models.ClinicalScheduler; namespace Viper.test.ClinicalScheduler { @@ -330,6 +332,85 @@ public async Task GetRotation_AccessDeniedRotation_ReturnsForbidden() // The security is working correctly - users only see services they have permissions for + #endregion + + #region BuildWeekScheduleItem Tests (lines 522-528) + + // Empty InstructorSchedules avoids the Week navigation property NPE that occurs in + // GetRecentCliniciansAsync when MockQueryable doesn't load navigation properties. + private void SetupForScheduleResponse() + { + var instSched = new List().BuildMockDbSet(); + var rwp = new List().BuildMockDbSet(); + + MockContext.InstructorSchedules.Returns(instSched); + MockContext.RotationWeeklyPrefs.Returns(rwp); + + var baseDate = new DateTime(TestYear, 6, 1, 0, 0, 0, DateTimeKind.Utc); + _mockWeekService.GetWeeksAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns( + [ + new() { WeekId = 1, WeekNum = 1, DateStart = baseDate, DateEnd = baseDate.AddDays(6), TermCode = TestTermCode }, + new() { WeekId = 2, WeekNum = 2, DateStart = baseDate.AddDays(7), DateEnd = baseDate.AddDays(13), TermCode = TestTermCode } + ]); + } + + private static RotationDto CardiologyRotationWithMinConsecutiveWeeks(int? minConsecutiveWeeks) => new() + { + RotId = CardiologyRotationId, + Name = "Cardiology", + ServiceId = CardiologyServiceId, + Service = new ServiceDto + { + ServiceId = CardiologyServiceId, + ServiceName = "Cardiology Service", + MinConsecutiveWeeks = minConsecutiveWeeks + } + }; + + #endregion + + #region BuildSimpleRotationResponse Tests (lines 621-627) + + [Fact] + public async Task GetRotationSchedule_WhenNoWeeks_ServiceMinConsecutiveWeeksIsIncludedInResponse() + { + SetupMockPermissions(hasFullPermissions: true); + _mockWeekService.GetWeeksAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns([]); + + const int minConsecutiveWeeks = 4; + _mockRotationService.GetRotationAsync(CardiologyRotationId, Arg.Any()) + .Returns(CardiologyRotationWithMinConsecutiveWeeks(minConsecutiveWeeks)); + RecreateController(); + + var result = await _controller.GetRotationSchedule(CardiologyRotationId, TestYear); + + var okResult = Assert.IsType(result.Result); + var rotationProp = okResult.Value!.GetType().GetProperty("Rotation")?.GetValue(okResult.Value); + var serviceProp = rotationProp?.GetType().GetProperty("Service")?.GetValue(rotationProp); + var actual = serviceProp?.GetType().GetProperty("MinConsecutiveWeeks")?.GetValue(serviceProp); + Assert.Equal(minConsecutiveWeeks, (int?)actual); + } + + [Fact] + public async Task GetRotationSchedule_WhenNoWeeks_AndServiceIsNull_ServiceIsNullInResponse() + { + SetupMockPermissions(hasFullPermissions: true); + _mockWeekService.GetWeeksAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns([]); + _mockRotationService.GetRotationAsync(CardiologyRotationId, Arg.Any()) + .Returns(new RotationDto { RotId = CardiologyRotationId, Name = "Cardiology", ServiceId = CardiologyServiceId, Service = null }); + RecreateController(); + + var result = await _controller.GetRotationSchedule(CardiologyRotationId, TestYear); + + var okResult = Assert.IsType(result.Result); + var rotationProp = okResult.Value!.GetType().GetProperty("Rotation")?.GetValue(okResult.Value); + var serviceProp = rotationProp?.GetType().GetProperty("Service")?.GetValue(rotationProp); + Assert.Null(serviceProp); + } + #endregion #region GetRotationsWithScheduledWeeks Security Tests diff --git a/test/ClinicalScheduler/ServiceMappingExtensionsTests.cs b/test/ClinicalScheduler/ServiceMappingExtensionsTests.cs index 258a835a..4b93a83a 100644 --- a/test/ClinicalScheduler/ServiceMappingExtensionsTests.cs +++ b/test/ClinicalScheduler/ServiceMappingExtensionsTests.cs @@ -14,7 +14,7 @@ public void ToDto_MapsServicePropertiesCorrectly() ServiceId = 10, ServiceName = "Surgery Service", ShortName = "Surgery", - WeekSize = 2, + MinConsecutiveWeeks = 2, ScheduleEditPermission = "SVMSecure.ClnSched.EditSurgery" }; @@ -24,7 +24,7 @@ public void ToDto_MapsServicePropertiesCorrectly() // Assert Assert.Equal(service.ServiceId, dto.ServiceId); Assert.Equal(service.ServiceName, dto.ServiceName); - Assert.Equal(service.WeekSize, dto.WeekSize); + Assert.Equal(service.MinConsecutiveWeeks, dto.MinConsecutiveWeeks); Assert.Equal(service.ScheduleEditPermission, dto.ScheduleEditPermission); } @@ -73,7 +73,7 @@ public void ToDto_HandlesNullScheduleEditPermission() ServiceId = 10, ServiceName = "Surgery Service", ShortName = "Surgery", - WeekSize = 2, + MinConsecutiveWeeks = 2, ScheduleEditPermission = null }; @@ -86,7 +86,7 @@ public void ToDto_HandlesNullScheduleEditPermission() } [Fact] - public void ToDto_HandlesZeroWeekSize() + public void ToDto_HandlesZeroMinConsecutiveWeeks() { // Arrange var service = new Service @@ -94,14 +94,14 @@ public void ToDto_HandlesZeroWeekSize() ServiceId = 10, ServiceName = "Special Service", ShortName = "SPEC", - WeekSize = 0 + MinConsecutiveWeeks = 0 }; // Act var dto = service.ToDto(); // Assert - Assert.Equal(0, dto.WeekSize); + Assert.Equal(0, dto.MinConsecutiveWeeks); } } } diff --git a/web/Areas/ClinicalScheduler/Controllers/RotationsController.cs b/web/Areas/ClinicalScheduler/Controllers/RotationsController.cs index cc34d881..fac53270 100644 --- a/web/Areas/ClinicalScheduler/Controllers/RotationsController.cs +++ b/web/Areas/ClinicalScheduler/Controllers/RotationsController.cs @@ -522,7 +522,7 @@ private object BuildWeekScheduleItem(WeekDto week, IEnumerable 1 (2, 3, 4, etc.), the last week of each block needs a primary: - /// - For weekSize=2: usually the second week - /// - For weekSize=3: usually the third week - /// - For weekSize=4: usually the fourth week + /// 2. If MinConsecutiveWeeks = 1, every week needs a primary + /// 3. If MinConsecutiveWeeks > 1 (2, 3, 4, etc.), the last week of each block needs a primary: + /// - For MinConsecutiveWeeks=2: usually the second week + /// - For MinConsecutiveWeeks=3: usually the third week + /// - For MinConsecutiveWeeks=4: usually the fourth week /// - For blocks with ExtendedRotation=true weeks, no evaluation needed /// - Logic: For StartWeek=false, check next week: /// * If next week has ExtendedRotation=true, no primary needed @@ -25,13 +25,13 @@ public EvaluationPolicyService() /// /// The week number to check /// All weeks for the rotation in the year - /// The WeekSize from the Service table (1, 2, 3, 4, etc.) + /// The MinConsecutiveWeeks from the Service table (1, 2, 3, 4, etc.) /// Whether the rotation is closed this week (from RotationWeeklyPref) /// True if the week requires a primary evaluator public bool RequiresPrimaryEvaluator( int weekNumber, IEnumerable rotationWeeks, - int? serviceWeekSize = null, + int? serviceMinConsecutiveWeeks = null, bool rotationClosed = false) { // Rule 1: If rotation is closed for this week, no primary needed @@ -61,16 +61,16 @@ public bool RequiresPrimaryEvaluator( return false; } - // Handle null or invalid WeekSize values - if (!serviceWeekSize.HasValue || serviceWeekSize.Value <= 0) + // Handle null or invalid MinConsecutiveWeeks values + if (!serviceMinConsecutiveWeeks.HasValue || serviceMinConsecutiveWeeks.Value <= 0) { - // NULL or 0 WeekSize indicates undefined rotation structure + // NULL or 0 MinConsecutiveWeeks indicates undefined rotation structure // Default to no evaluation requirement for safety return false; } - // Handle different WeekSize values - if (serviceWeekSize == 1) + // Handle different MinConsecutiveWeeks values + if (serviceMinConsecutiveWeeks == 1) { // Single-week rotations: every week is a complete block requiring evaluation return true; diff --git a/web/Areas/ClinicalScheduler/Services/IEvaluationPolicyService.cs b/web/Areas/ClinicalScheduler/Services/IEvaluationPolicyService.cs index 884d26ed..b3374d69 100644 --- a/web/Areas/ClinicalScheduler/Services/IEvaluationPolicyService.cs +++ b/web/Areas/ClinicalScheduler/Services/IEvaluationPolicyService.cs @@ -10,13 +10,13 @@ public interface IEvaluationPolicyService /// /// Week number to check /// Collection of rotation week info - /// Week size configuration for the service (1 or 2) + /// Minimum number of consecutive weeks that require evaluation /// Whether the rotation is closed for this week /// True if a primary evaluator is required for this week bool RequiresPrimaryEvaluator( int weekNumber, IEnumerable rotationWeeks, - int? serviceWeekSize = null, + int? serviceMinConsecutiveWeeks = null, bool rotationClosed = false); } } diff --git a/web/Classes/SQLContext/ClinicalSchedulerContext.cs b/web/Classes/SQLContext/ClinicalSchedulerContext.cs index 77136a6a..3ba6ef69 100644 --- a/web/Classes/SQLContext/ClinicalSchedulerContext.cs +++ b/web/Classes/SQLContext/ClinicalSchedulerContext.cs @@ -52,7 +52,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.ServiceName).HasColumnName("ServiceName"); entity.Property(e => e.ShortName).HasColumnName("ShortName"); entity.Property(e => e.ScheduleEditPermission).HasColumnName("ScheduleEditPermission").IsRequired(false); - entity.Property(e => e.WeekSize).HasColumnName("WeekSize").IsRequired(false); + entity.Property(e => e.MinConsecutiveWeeks).HasColumnName("MinConsecutiveWeeks").IsRequired(false); }); modelBuilder.Entity(entity => diff --git a/web/Models/ClinicalScheduler/Service.cs b/web/Models/ClinicalScheduler/Service.cs index 2e663532..34e0a405 100644 --- a/web/Models/ClinicalScheduler/Service.cs +++ b/web/Models/ClinicalScheduler/Service.cs @@ -15,7 +15,7 @@ public class Service /// Evaluation frequency in weeks. Determines how often a primary evaluator is required. /// Examples: 1 = every week, 2 = every 2 weeks, null = use default logic (last week only) /// - public int? WeekSize { get; set; } + public int? MinConsecutiveWeeks { get; set; } // Navigation properties public virtual ICollection Rotations { get; set; } = new List();