From 402a7cedf559339e8e919208061acaa0e59f1a92 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Tue, 12 May 2026 23:04:34 -0700 Subject: [PATCH 1/3] fix: close CodeQL correctness alerts Three rule families consolidated into one PR because each fix needs per-site judgment rather than mechanical replacement. cs/virtual-call-in-constructor (5): seal CMSFile and StudentClassYear - neither is inherited from in C#, neither is mocked, and the project has no UseLazyLoadingProxies so EF does not need them non-sealed. Drops `virtual` on properties newly declared inside the now-sealed classes. For RoleTemplateSimplified the same alert is closed by dropping `virtual` from the Roles property, which is a smaller change than sealing and keeps the public constructor surface intact. cs/unsafe-year-construction (4): in PercentRolloverService, bound the HTTP-derived year parameter with ArgumentOutOfRangeException and use .AddYears(1) instead of `new DateTime(year+1, ...)` for the target end date. In AcademicYearHelper.GetAcademicYearStart, build the July date first then .AddYears(-1) instead of subtracting from .Year before constructing. cs/constant-condition (21): remove redundant null-propagation that the analyzer can already prove dead - `?.` chains after `!= null` guards, an unreachable CompetencyId range check after the FindAsync record-existence check, and a duplicated `RegionEndpoint` element lookup in SetAwsCredentials. RAPSController null-flow was restructured to lift the path null check up. The SetAwsCredentials region lookup also gets hardened to truly close the alert: trim the value, look it up with BindingFlags.IgnoreCase, and fall back to USWest1 when the name is non-empty but does not match any known region (previously left profile.Region as null). --- test/CTS/AssessmentControllerTest.cs | 2 +- web/Areas/CMS/Models/CMSFile.cs | 2 +- .../CTS/Controllers/AssessmentController.cs | 2 +- .../CTS/Controllers/CompetencyController.cs | 4 ---- web/Areas/CTS/Models/AuditRow.cs | 6 +++--- .../Effort/Services/PercentRolloverService.cs | 6 +++++- .../Effort/Services/PercentageService.cs | 3 ++- web/Areas/RAPS/Controllers/RAPSController.cs | 16 +++++++++------ .../Controllers/RoleTemplatesController.cs | 4 ++-- .../RAPS/Models/RoleTemplateSimplified.cs | 2 +- web/Areas/RAPS/Services/RAPSAuditService.cs | 6 +++--- web/Classes/Utilities/AcademicYearHelper.cs | 4 ++-- web/Controllers/HomeController.cs | 5 +++-- web/Models/Students/StudentClassYear.cs | 10 +++++----- web/Program.cs | 20 +++++++++++-------- 15 files changed, 51 insertions(+), 41 deletions(-) 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/web/Areas/CMS/Models/CMSFile.cs b/web/Areas/CMS/Models/CMSFile.cs index 23b42e996..b6abcb36f 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 partial 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..14359200a 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 int 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..020665665 100644 --- a/web/Areas/RAPS/Controllers/RoleTemplatesController.cs +++ b/web/Areas/RAPS/Controllers/RoleTemplatesController.cs @@ -186,8 +186,8 @@ public async Task> RoleTemplateApply(stri return new RoleTemplateApplyPreview() { - DisplayName = user?.DisplayFullName ?? "User not found", - MemberId = user?.MothraId ?? "", + DisplayName = user.DisplayFullName ?? "User not found", + 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); From f7f81204fcdcaa27912eba089655ef3e7fbe31ae Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Wed, 13 May 2026 13:57:27 -0700 Subject: [PATCH 2/3] chore(resharper): silence PR-scoped gate warnings introduced by codeql/3 These were flagged by the ReSharper PR-scoped gate as new warnings on lines my CodeQL 3 fix touched: - CMSFile.cs: drop redundant 'partial' modifier (PartialTypeWithSinglePart) - RAPSController.cs: 'is int idx' on int? -> 'is { } idx' (ConvertTypeCheckPatternToNullCheck) - RoleTemplatesController.cs: drop '??' fallbacks; both properties are declared non-null (NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract) Also folds in the controller cleanup from CodeRabbit on the same lines: replace foreach + Add with .Select(...).ToList() to match the project's LINQ conventions. --- web/Areas/CMS/Models/CMSFile.cs | 2 +- web/Areas/RAPS/Controllers/RAPSController.cs | 2 +- .../RAPS/Controllers/RoleTemplatesController.cs | 13 +++++-------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/web/Areas/CMS/Models/CMSFile.cs b/web/Areas/CMS/Models/CMSFile.cs index b6abcb36f..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 sealed 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/RAPS/Controllers/RAPSController.cs b/web/Areas/RAPS/Controllers/RAPSController.cs index 14359200a..6c1325644 100644 --- a/web/Areas/RAPS/Controllers/RAPSController.cs +++ b/web/Areas/RAPS/Controllers/RAPSController.cs @@ -50,7 +50,7 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context string instance = "VIPER"; string page = ""; // rapsIdx is non-null iff path is non-null (it's derived from path?.FindIndex). - if (rapsIdx is int idx && idx > -1) + if (rapsIdx is { } idx && idx > -1) { if (path!.Count > idx + 1) { diff --git a/web/Areas/RAPS/Controllers/RoleTemplatesController.cs b/web/Areas/RAPS/Controllers/RoleTemplatesController.cs index 020665665..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 }; } From 9743d933a30cd07ac26751fd766a798e4c718a92 Mon Sep 17 00:00:00 2001 From: Rex Lorenzo Date: Wed, 13 May 2026 13:57:51 -0700 Subject: [PATCH 3/3] test(raps): cover RoleTemplateSimplified constructor mapping Two unit tests for the constructor that drives the GetRoleTemplates projection: - Constructor_MapsScalarsAndFlattensRoles: verifies the scalar properties and the nested RoleTemplateRoles -> Roles flattening via Role.RoleId / Role.FriendlyName. - Constructor_EmptyRoles_ReturnsEmptyCollection: ensures the Roles initializer does not depend on a non-empty source collection. Pins the constructor's mapping behavior so the cs/virtual-call-in- constructor fix (dropping `virtual` from Roles) does not silently regress the projection. --- test/RAPS/RoleTemplateSimplifiedTests.cs | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/RAPS/RoleTemplateSimplifiedTests.cs 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); + } + } +}