using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FrameworkBLL.RoleVsMenu; using FrameworkDAL.CustomCode.RoleVsMenu; using FrameworkDAL.DTO.RoleVsMenu; using FrameworkDAL.Query.RoleVsMenu; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using Moq; using Xunit; namespace FrameworkTests; /// /// RoleVsMenuBLL.Save's fixed diff-based shape, exercised for the "no new rows" case so /// AutoNumber (a concrete class with an IOptionsSnapshot dependency, awkward to construct /// in a unit test) never needs to be touched — GetNumberAsync is only called when at least /// one incoming (RoleId, MenuId) pair is genuinely new, which this test avoids by design. /// The new-row/AutoNumber path is a known, explicitly-acknowledged test gap, not silently /// skipped: covering it would need either a real AutoNumber (IOptionsSnapshot /// + IQueryExecutor round-trip) or refactoring AutoNumber behind an interface, neither of /// which is in scope for this fix. /// public class RoleVsMenuDiffSaveTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 }; [Fact] public async Task Save_ExistingMenuOmittedFromIncomingList_IsDeleted_NotSilentlyKept() { var existing = new List { new() { RoleVsMenuId = 100, RoleVsMenuMenuId = 501 }, new() { RoleVsMenuId = 101, RoleVsMenuMenuId = 502 }, // omitted from the incoming save below }; var dal = new Mock(); dal.Setup(x => x.GetExistingForModule(9, 3, It.IsAny())).ReturnsAsync(existing); List<(string sql, object param)>? captured = null; var qe = new Mock(); qe.Setup(x => x.ExecuteInTransactionAsync(It.IsAny(), It.IsAny>())) .Callback>((_, statements) => captured = statements.ToList()) .Returns(Task.CompletedTask); var bll = new RoleVsMenuBLL(dal.Object, null!, qe.Object); var incoming = new List { new() { RoleVsMenuRoleId = 9, RoleVsMenuMenuId = 501, RoleVsMenuSlNo = 1, RoleVsMenuAllow = "111111" }, }; await bll.Save(ModuleId: 3, PageId: -1, incoming, Login()); Assert.NotNull(captured); var deleteStatement = Assert.Single(captured!, s => s.sql == RoleVsMenuQB.DELETE_ROLEVSMENU_BY_IDS); var ids = (IEnumerable)deleteStatement.param.GetType().GetProperty("Ids")!.GetValue(deleteStatement.param)!; Assert.Equal(new[] { 101 }, ids); var updateStatement = Assert.Single(captured!, s => s.sql == RoleVsMenuQB.UPDATE_ROLEVSMENU); Assert.Equal(100, ((RoleVsMenuDTO)updateStatement.param).RoleVsMenuId); Assert.DoesNotContain(captured!, s => s.sql == RoleVsMenuQB.SAVE_ROLEVSMENU); } [Fact] public async Task Save_ExistingMenu_ReusesSameRoleVsMenuId_InsteadOfDeleteAndReinsert() { // The bug this fix targets: the old Save() reassigned a brand-new ROLEVSMENUID to // every row on every save, destroying CREATEDON/audit history even for a row whose // (RoleId, MenuId) didn't change. Confirms the update path keeps the original id. var existing = new List { new() { RoleVsMenuId = 777, RoleVsMenuMenuId = 501 } }; var dal = new Mock(); dal.Setup(x => x.GetExistingForModule(9, 3, It.IsAny())).ReturnsAsync(existing); List<(string sql, object param)>? captured = null; var qe = new Mock(); qe.Setup(x => x.ExecuteInTransactionAsync(It.IsAny(), It.IsAny>())) .Callback>((_, statements) => captured = statements.ToList()) .Returns(Task.CompletedTask); var bll = new RoleVsMenuBLL(dal.Object, null!, qe.Object); var incoming = new List { new() { RoleVsMenuId = 0, RoleVsMenuRoleId = 9, RoleVsMenuMenuId = 501, RoleVsMenuSlNo = 5, RoleVsMenuAllow = "000000" }, }; await bll.Save(ModuleId: 3, PageId: -1, incoming, Login()); Assert.Equal(777, incoming[0].RoleVsMenuId); Assert.DoesNotContain(captured!, s => s.sql == RoleVsMenuQB.DELETE_ROLEVSMENU_BY_IDS); } }