using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.MenuGroup; using FrameworkBLL.MenuSet; using FrameworkDAL.CustomCode.MenuSet; using FrameworkDAL.DTO.MenuSet; using FrameworkDAL.Query.MenuSet; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using Moq; using Xunit; namespace FrameworkTests; /// /// MenuSetBLL.SaveMenuSetDetail closes the gap MenuGroupBLL.ValidateMenuGroupVisibility was /// originally built with no caller — this is the one place a MENUGROUPID re-filing override /// is actually enforced. AutoNumber/KeyInvalidate are passed null! in the rejection test since /// that path throws before either is touched (fail-fast, before any DAL write). /// public class MenuSetBLLVisibilityTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 }; [Fact] public async Task SaveMenuSetDetail_MenuGroupIdTargetsAnotherSetsPrivateGroup_RejectsBeforeAnyDalWrite() { var dal = new Mock(); var menuGroupBLL = new Mock(); menuGroupBLL .Setup(x => x.ValidateMenuGroupVisibility(10, 55, It.IsAny(), It.IsAny())) .ThrowsAsync(new ArgumentException("MenuGroup 10 is private to another MenuSet and is not assignable here.")); var bll = new MenuSetBLL(dal.Object, menuGroupBLL.Object, null!, null!); var details = new List { new() { MenuSetDetailId = 1, MenuId = 501, MenuGroupId = 10 } }; await Assert.ThrowsAsync(() => bll.SaveMenuSetDetail(55, details, Login(), CancellationToken.None)); dal.Verify(x => x.SaveMenuSetDetail(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Never); } [Fact] public async Task SaveMenuSetDetail_NoMenuGroupIdOverrides_SkipsVisibilityCheckEntirely() { var dal = new Mock(); var menuGroupBLL = new Mock(); var bll = new MenuSetBLL(dal.Object, menuGroupBLL.Object, null!, null!); // No MenuGroupId set on any row — SaveMenuSetDetail would NPE on _keyInvalidate if it // reached the cache-invalidation call, so this test only proves the validation loop // is correctly skipped when there's nothing to validate (asserted via Verify(Never) // before the DAL/cache call would otherwise be reached). var details = new List { new() { MenuSetDetailId = 1, MenuId = 501 } }; // Exercise just the validation-loop portion of the contract via a DAL that throws, // to prove control reaches the DAL call without any visibility check firing first. dal.Setup(x => x.SaveMenuSetDetail(55, It.IsAny>(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("stop-here-marker")); var ex = await Assert.ThrowsAsync( () => bll.SaveMenuSetDetail(55, details, Login(), CancellationToken.None)); Assert.Equal("stop-here-marker", ex.Message); menuGroupBLL.Verify(x => x.ValidateMenuGroupVisibility( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } } /// /// MenuSetDAL.SaveMenuSetDetail ports the same diff-save shape as /// MenuGroupDAL.SaveMenuGroupDetail for MMENUSETDETAIL, via a mocked IQueryExecutor. /// public class MenuSetDetailDaoDiffSaveTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 }; [Fact] public async Task SaveMenuSetDetail_MixOfExistingAndOmitted_BuildsDeleteNotInPlusUpsertStatements() { 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 dal = new MenuSetDAL(qe.Object); // MenuSetDetailId 30 survives (update); 40 is omitted from this save (must be deleted). var details = new List { new() { MenuSetDetailId = 30, MenuId = 501 } }; await dal.SaveMenuSetDetail(menuSetId: 55, details, Login()); Assert.NotNull(captured); var deleteStatement = Assert.Single(captured!, s => s.sql == MenuSetQB.DELETE_MENUSETDETAIL_NOT_IN); var survivingIds = ((System.Collections.IEnumerable)deleteStatement.param.GetType().GetProperty("SurvivingIds")!.GetValue(deleteStatement.param)!) .Cast().ToList(); Assert.Equal(new[] { 30 }, survivingIds); Assert.Contains(captured!, s => s.sql == MenuSetQB.UPDATE_MENUSETDETAIL); } [Fact] public async Task SaveMenuSetDetail_EmptyList_DeletesAllExistingOverrides() { 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 dal = new MenuSetDAL(qe.Object); await dal.SaveMenuSetDetail(menuSetId: 55, new List(), Login()); var only = Assert.Single(captured!); Assert.Equal(MenuSetQB.DELETE_MENUSETDETAIL_BY_SET, only.sql); } }