diff --git a/test/RAPS/RoleTemplateSimplifiedTests.cs b/test/RAPS/RoleTemplateSimplifiedTests.cs new file mode 100644 index 000000000..aad8a1061 --- /dev/null +++ b/test/RAPS/RoleTemplateSimplifiedTests.cs @@ -0,0 +1,59 @@ +using Viper.Areas.RAPS.Models; +using Viper.Models.RAPS; + +namespace Viper.test.RAPS +{ + public class RoleTemplateSimplifiedTests + { + [Fact] + public void Constructor_MapsScalarsAndFlattensRoles() + { + // arrange + var rt = new RoleTemplate + { + RoleTemplateId = 42, + TemplateName = "Test Template", + Description = "Desc", + RoleTemplateRoles = new List + { + new() { Role = new TblRole { RoleId = 1, Role = "Alpha", DisplayName = "Alpha" } }, + new() { Role = new TblRole { RoleId = 2, Role = "Beta", DisplayName = "Beta" } } + } + }; + + // act + var dto = new RoleTemplateSimplified(rt); + + // assert + Assert.Equal(42, dto.RoleTemplateId); + Assert.Equal("Test Template", dto.TemplateName); + Assert.Equal("Desc", dto.Description); + var roles = dto.Roles.ToList(); + Assert.Equal(2, roles.Count); + Assert.Equal(1, roles[0].RoleId); + Assert.Equal("Alpha", roles[0].FriendlyName); + Assert.Equal(2, roles[1].RoleId); + Assert.Equal("Beta", roles[1].FriendlyName); + } + + [Fact] + public void Constructor_EmptyRoles_ReturnsEmptyCollection() + { + // arrange + var rt = new RoleTemplate + { + RoleTemplateId = 7, + TemplateName = "No Roles", + Description = "", + RoleTemplateRoles = new List() + }; + + // act + var dto = new RoleTemplateSimplified(rt); + + // assert + Assert.Equal(7, dto.RoleTemplateId); + Assert.Empty(dto.Roles); + } + } +} diff --git a/web/Areas/CTS/Models/AuditRow.cs b/web/Areas/CTS/Models/AuditRow.cs index 240f5da35..536a6ea37 100644 --- a/web/Areas/CTS/Models/AuditRow.cs +++ b/web/Areas/CTS/Models/AuditRow.cs @@ -22,10 +22,10 @@ public AuditRow(CtsAudit dbAudit) Timestamp = dbAudit.TimeStamp; ModifiedById = dbAudit.ModifiedBy; ModifiedByName = dbAudit.Modifier.LastName + ", " + dbAudit.Modifier.FirstName; - if (dbAudit?.Encounter?.Student != null) + if (dbAudit.Encounter?.Student != null) { - ModifiedPersonId = dbAudit.Encounter?.StudentUserId; - ModifiedPersonName = dbAudit.Encounter?.Student?.LastName + ", " + dbAudit.Encounter?.Student?.FirstName; + ModifiedPersonId = dbAudit.Encounter.StudentUserId; + ModifiedPersonName = dbAudit.Encounter.Student.LastName + ", " + dbAudit.Encounter.Student.FirstName; } } diff --git a/web/Areas/Effort/Services/PercentRolloverService.cs b/web/Areas/Effort/Services/PercentRolloverService.cs index 3fe56d08a..4e4ab1d79 100644 --- a/web/Areas/Effort/Services/PercentRolloverService.cs +++ b/web/Areas/Effort/Services/PercentRolloverService.cs @@ -30,6 +30,10 @@ public PercentRolloverService( public async Task GetRolloverPreviewAsync(int year, CancellationToken ct = default) { + // Bound year for DateTime constructions below (year and year+1 must be valid years). + ArgumentOutOfRangeException.ThrowIfLessThan(year, 1); + ArgumentOutOfRangeException.ThrowIfGreaterThan(year, 9998); + var result = new PercentRolloverPreviewDto(); result.SourceAcademicYear = year; @@ -42,7 +46,7 @@ public async Task GetRolloverPreviewAsync(int year, C var july1Start = new DateTime(year, 7, 1, 0, 0, 0, DateTimeKind.Local); result.OldEndDate = june30Start; result.NewStartDate = july1Start; - result.NewEndDate = new DateTime(year + 1, 6, 30, 0, 0, 0, DateTimeKind.Local); + result.NewEndDate = june30Start.AddYears(1); // Find assignments ending on June 30 of source year (any time on that day) var assignments = await _context.Percentages diff --git a/web/Areas/Effort/Services/PercentageService.cs b/web/Areas/Effort/Services/PercentageService.cs index 82c708650..2050c2ccc 100644 --- a/web/Areas/Effort/Services/PercentageService.cs +++ b/web/Areas/Effort/Services/PercentageService.cs @@ -306,7 +306,8 @@ public async Task ValidatePercentageAsync( .Where(p => !string.Equals(p.PercentAssignType.Class, LeaveTypeClass, StringComparison.OrdinalIgnoreCase)) .Sum(p => (decimal)EffortConstants.ToDisplayPercent(p.PercentageValue)); - if (isNewActive && type != null && !string.Equals(type.Class, LeaveTypeClass, StringComparison.OrdinalIgnoreCase)) + // type is guaranteed non-null here: early-return above when result.IsValid==false (set when type==null). + if (isNewActive && !string.Equals(type!.Class, LeaveTypeClass, StringComparison.OrdinalIgnoreCase)) { var newTotal = activeNonLeaveTotal + Math.Round(request.PercentageValue, EffortConstants.PercentDisplayDecimals); if (newTotal > 100) diff --git a/web/Areas/RAPS/Controllers/RAPSController.cs b/web/Areas/RAPS/Controllers/RAPSController.cs index ae29f6609..19f717b1f 100644 --- a/web/Areas/RAPS/Controllers/RAPSController.cs +++ b/web/Areas/RAPS/Controllers/RAPSController.cs @@ -46,14 +46,18 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context List? path = HttpContext?.Request?.Path.ToString().Split("/").ToList(); int? rapsIdx = path?.FindIndex(p => p.Equals("raps", StringComparison.OrdinalIgnoreCase)); string instance = "VIPER"; - if (rapsIdx != null && rapsIdx > -1 && path?.Count > rapsIdx + 1) - { - instance = path[(int)rapsIdx + 1]; - } string page = ""; - if (rapsIdx != null && rapsIdx > -1 && path?.Count > rapsIdx + 2) + // rapsIdx is non-null iff path is non-null (it's derived from path?.FindIndex). + if (rapsIdx is { } idx && idx > -1) { - page = path[(int)rapsIdx + 2]; + if (path!.Count > idx + 1) + { + instance = path[idx + 1]; + } + if (path.Count > idx + 2) + { + page = path[idx + 2]; + } } ViewData["ViperLeftNav"] = await Nav(roleIdValid ? roleId : null, permIdValid ? permissionId : null, diff --git a/web/Areas/RAPS/Controllers/RoleTemplatesController.cs b/web/Areas/RAPS/Controllers/RoleTemplatesController.cs index 19de64b30..3e28f77b1 100644 --- a/web/Areas/RAPS/Controllers/RoleTemplatesController.cs +++ b/web/Areas/RAPS/Controllers/RoleTemplatesController.cs @@ -41,12 +41,9 @@ public async Task>> GetRoleTemp .OrderBy(rt => rt.TemplateName) .ToListAsync(); - List roleTemplates = new(); - foreach (var rt in dbRoleTemplates) - { - roleTemplates.Add(new RoleTemplateSimplified(rt)); - } - return roleTemplates; + return dbRoleTemplates + .Select(rt => new RoleTemplateSimplified(rt)) + .ToList(); } // GET: RoleTemplates/5 @@ -185,8 +182,8 @@ public async Task> RoleTemplateApply(stri return new RoleTemplateApplyPreview { - DisplayName = user?.DisplayFullName ?? "User not found", - MemberId = user?.MothraId ?? "", + DisplayName = user.DisplayFullName, + MemberId = user.MothraId, Roles = rolesToApply }; } diff --git a/web/Areas/RAPS/Services/RAPSAuditService.cs b/web/Areas/RAPS/Services/RAPSAuditService.cs index eab5d7ef8..8515adf37 100644 --- a/web/Areas/RAPS/Services/RAPSAuditService.cs +++ b/web/Areas/RAPS/Services/RAPSAuditService.cs @@ -137,11 +137,11 @@ public async Task> GetMemberRolesAndPermissionHistory(string inst Dictionary> actionsPerformedOnObject = new(); foreach (AuditLog auditLog in auditEntries) { - if (auditLog?.RoleId != null || auditLog?.PermissionId != null) + if (auditLog.RoleId != null || auditLog.PermissionId != null) { - string key = auditLog?.RoleId != null + string key = auditLog.RoleId != null ? "role-" + auditLog.RoleId - : "permission-" + auditLog!.PermissionId; + : "permission-" + auditLog.PermissionId; if (actionsPerformedOnObject.ContainsKey(key)) { List moreRecentActions = actionsPerformedOnObject[key]; diff --git a/web/Classes/Utilities/AcademicYearHelper.cs b/web/Classes/Utilities/AcademicYearHelper.cs index 921e298ac..321beeeff 100644 --- a/web/Classes/Utilities/AcademicYearHelper.cs +++ b/web/Classes/Utilities/AcademicYearHelper.cs @@ -52,8 +52,8 @@ public static List GetTermCodesForAcademicYear(IEnumerable allTermCode /// public static DateTime GetAcademicYearStart(DateTime date) { - var year = date.Month < 7 ? date.Year - 1 : date.Year; - return new DateTime(year, 7, 1, 0, 0, 0, DateTimeKind.Local); + var julyOfYear = new DateTime(date.Year, 7, 1, 0, 0, 0, DateTimeKind.Local); + return date.Month < 7 ? julyOfYear.AddYears(-1) : julyOfYear; } /// diff --git a/web/Controllers/HomeController.cs b/web/Controllers/HomeController.cs index bb91938f5..a31956319 100644 --- a/web/Controllers/HomeController.cs +++ b/web/Controllers/HomeController.cs @@ -320,7 +320,8 @@ private async Task AuthenticateCasLogin(string? ticket, string? r { var claimsIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, validatedUserName), new Claim(ClaimTypes.NameIdentifier, validatedUserName), new Claim(ClaimTypes.AuthenticationMethod, "CAS") }, CookieAuthenticationDefaults.AuthenticationScheme); - XElement? attributesNode = successNode?.Element(_ns + "attributes"); + // successNode is guaranteed non-null here: validatedUserName is derived from successNode?.Element(user)?.Value. + XElement? attributesNode = successNode!.Element(_ns + "attributes"); if (attributesNode != null) { foreach (string attributeName in _casAttributesToCapture) diff --git a/web/Models/Students/StudentClassYear.cs b/web/Models/Students/StudentClassYear.cs index 6c77638f2..71020ab09 100644 --- a/web/Models/Students/StudentClassYear.cs +++ b/web/Models/Students/StudentClassYear.cs @@ -3,7 +3,7 @@ namespace Viper.Models.Students { - public class StudentClassYear + public sealed class StudentClassYear { public int StudentClassYearId { get; set; } @@ -20,10 +20,10 @@ public class StudentClassYear public int? UpdatedBy { get; set; } public string? Comment { get; set; } - public virtual Person? Student { get; set; } - public virtual ClassYearLeftReason? ClassYearLeftReason { get; set; } - public virtual Person? AddedByPerson { get; set; } - public virtual Person? UpdatedByPerson { get; set; } + public Person? Student { get; set; } + public ClassYearLeftReason? ClassYearLeftReason { get; set; } + public Person? AddedByPerson { get; set; } + public Person? UpdatedByPerson { get; set; } [NotMapped] public string? LeftReasonText