diff --git a/test/CTS/AssessmentControllerTest.cs b/test/CTS/AssessmentControllerTest.cs index 289fcd92f..1abb1215e 100644 --- a/test/CTS/AssessmentControllerTest.cs +++ b/test/CTS/AssessmentControllerTest.cs @@ -258,7 +258,7 @@ private static bool IsForbidResult(ActionResult a) var forbidResult = a.Result as ForbidResult; if (result != null) { - Assert.Equal((int)HttpStatusCode.Forbidden, result?.StatusCode); + Assert.Equal((int)HttpStatusCode.Forbidden, result.StatusCode); } Assert.True(result != null || forbidResult != null); 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/CMS/Models/CMSFile.cs b/web/Areas/CMS/Models/CMSFile.cs index 23b42e996..39eeb9d70 100644 --- a/web/Areas/CMS/Models/CMSFile.cs +++ b/web/Areas/CMS/Models/CMSFile.cs @@ -1,6 +1,6 @@ namespace Viper.Areas.CMS.Models { - public partial class CMSFile : Viper.Models.VIPER.File + public sealed class CMSFile : Viper.Models.VIPER.File { public string FriendlyURL { get; set; } diff --git a/web/Areas/CTS/Controllers/AssessmentController.cs b/web/Areas/CTS/Controllers/AssessmentController.cs index 3d8f8a3dd..6bbc142d5 100644 --- a/web/Areas/CTS/Controllers/AssessmentController.cs +++ b/web/Areas/CTS/Controllers/AssessmentController.cs @@ -297,7 +297,7 @@ public async Task> CreateStudentEpa(CreateU .Include(p => p.StudentInfo) .Where(p => p.PersonId == epaData.StudentId) .FirstOrDefaultAsync(); - if (student == null || student?.StudentInfo?.ClassLevel == null) + if (student == null || student.StudentInfo?.ClassLevel == null) { return BadRequest("Student not found"); } diff --git a/web/Areas/CTS/Controllers/CompetencyController.cs b/web/Areas/CTS/Controllers/CompetencyController.cs index 8c729ab04..057e79154 100644 --- a/web/Areas/CTS/Controllers/CompetencyController.cs +++ b/web/Areas/CTS/Controllers/CompetencyController.cs @@ -136,10 +136,6 @@ public async Task> UpdateComptency(int competencyId, return NotFound(); } - if (competency.CompetencyId == null || competency.CompetencyId <= 0) - { - return BadRequest("CompetencyId is required."); - } if (competency.Name.Length < 2 || competency.Number.Length < 1) { return BadRequest("Name and Number are required."); 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 0589961f9..3170820cf 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 cf544e83e..6c1325644 100644 --- a/web/Areas/RAPS/Controllers/RAPSController.cs +++ b/web/Areas/RAPS/Controllers/RAPSController.cs @@ -48,14 +48,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 d6c57c7b3..cca4257a1 100644 --- a/web/Areas/RAPS/Controllers/RoleTemplatesController.cs +++ b/web/Areas/RAPS/Controllers/RoleTemplatesController.cs @@ -42,12 +42,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 @@ -186,8 +183,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/Models/RoleTemplateSimplified.cs b/web/Areas/RAPS/Models/RoleTemplateSimplified.cs index 72d6cc5a9..149ef9d13 100644 --- a/web/Areas/RAPS/Models/RoleTemplateSimplified.cs +++ b/web/Areas/RAPS/Models/RoleTemplateSimplified.cs @@ -11,7 +11,7 @@ public class RoleTemplateSimplified public string Description { get; set; } = null!; - public virtual ICollection Roles { get; set; } = new List(); + public ICollection Roles { get; set; } = new List(); public RoleTemplateSimplified() { } public RoleTemplateSimplified(RoleTemplate rt) diff --git a/web/Areas/RAPS/Services/RAPSAuditService.cs b/web/Areas/RAPS/Services/RAPSAuditService.cs index eda28d872..1dc25f41a 100644 --- a/web/Areas/RAPS/Services/RAPSAuditService.cs +++ b/web/Areas/RAPS/Services/RAPSAuditService.cs @@ -145,11 +145,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 d854ff6ed..d7af6b21d 100644 --- a/web/Controllers/HomeController.cs +++ b/web/Controllers/HomeController.cs @@ -147,7 +147,7 @@ public IActionResult EmulateUser(string loginId) if (protector != null && emulatedUser.LoginId != null) { - string? encryptedEmulatedLoginId = protector?.Protect(emulatedUser.LoginId); + string encryptedEmulatedLoginId = protector.Protect(emulatedUser.LoginId); // set emulating cached item to expire after 30 minutes of inactivity HttpHelper.Cache?.Set(ClaimsTransformer.EmulationCacheNamePrefix + trueLoginId, encryptedEmulatedLoginId, (new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(30)))); @@ -319,7 +319,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 diff --git a/web/Program.cs b/web/Program.cs index 5fee3318b..49e95f988 100644 --- a/web/Program.cs +++ b/web/Program.cs @@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; +using System.Reflection; using System.Text.Json.Serialization; using NLog; using NLog.Web; @@ -542,26 +543,29 @@ void SetAwsCredentials(Logger logger) { XElement xAwsCredentials = XElement.Load(awsCredentialsFilePath, LoadOptions.None); - if (!String.IsNullOrWhiteSpace(xAwsCredentials?.Element("AccessKeyId")?.Value) && !String.IsNullOrWhiteSpace(xAwsCredentials?.Element("SecretAccessKey")?.Value)) + var accessKey = xAwsCredentials.Element("AccessKeyId")?.Value; + var secretKey = xAwsCredentials.Element("SecretAccessKey")?.Value; + if (!String.IsNullOrWhiteSpace(accessKey) && !String.IsNullOrWhiteSpace(secretKey)) { // grab the credentials ouf of the xml file to stor in the encrypted json file inthe profile var options = new CredentialProfileOptions { - AccessKey = xAwsCredentials?.Element("AccessKeyId")?.Value.Trim(), - SecretKey = xAwsCredentials?.Element("SecretAccessKey")?.Value.Trim() + AccessKey = accessKey.Trim(), + SecretKey = secretKey.Trim() }; var profile = new CredentialProfile("default", options); // if a region was specified in the xml then use the specified region else default to USWest1 - if (!string.IsNullOrWhiteSpace(xAwsCredentials?.Element("RegionEndpoint")?.Value) && xAwsCredentials?.Element("RegionEndpoint") != null) + var regionValue = xAwsCredentials.Element("RegionEndpoint")?.Value.Trim(); + if (!string.IsNullOrWhiteSpace(regionValue)) { -#pragma warning disable CS8604 // Possible null reference argument. - profile.Region = typeof(Amazon.RegionEndpoint).GetField(xAwsCredentials?.Element("RegionEndpoint")?.Value)?.GetValue(null) as Amazon.RegionEndpoint; -#pragma warning restore CS8604 // Possible null reference argument. + const BindingFlags regionFieldFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase; + profile.Region = typeof(RegionEndpoint).GetField(regionValue, regionFieldFlags)?.GetValue(null) as RegionEndpoint + ?? RegionEndpoint.USWest1; } else { - profile.Region = Amazon.RegionEndpoint.USWest1; + profile.Region = RegionEndpoint.USWest1; } var netSDKFile = new NetSDKCredentialsFile(); netSDKFile.RegisterProfile(profile);