using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.Menu; using FrameworkBLL.MenuGroup; using FrameworkDAL.CustomCode.MenuGroup; using FrameworkDAL.DTO.MenuGroup; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace FrameworkTests; /// /// Covers the two structural guarantees MenuGroupBLL.GetMenuGroupTreeForSet must hold: /// PRESENTATIONTYPE = 0 delegates unchanged to IMenuBLL (the byte-for-byte legacy /// compatibility contract, tested here as "no MenuGroup code runs, the call is forwarded /// verbatim" since a real end-to-end byte comparison belongs in an integration test against /// live data, not a unit test), and a misconfigured PRESENTATIONTYPE<>0 set (still at the /// -1 MenuGroupId default) fails loudly instead of silently rendering an empty tree. /// Also covers ValidateMenuGroupVisibility's shared/private boundary. /// AutoNumber/IQueryExecutor/KeyInvalidate are passed null! — none of the paths under test /// here touch them (only Save/Delete/SaveMenuGroupDetail do). /// public class MenuGroupBLLTests { private static LoginDTO Login(int userId = 7) => new() { ClientId = -1, UserId = userId }; [Fact] public async Task GetMenuGroupTreeForSet_PresentationTypeZero_DelegatesToMenuBLL_Unchanged() { var dal = new Mock(); dal.Setup(x => x.GetMenuSetPresentationInfo(55, It.IsAny(), It.IsAny())) .ReturnsAsync(new MenuSetPresentationInfoDTO { MenuSetId = 55, ModuleId = 3, PresentationType = 0, MenuGroupId = -1 }); var menuBLL = new Mock(); menuBLL.Setup(x => x.GetMenuForUserModule(7, 3, It.IsAny())) .ReturnsAsync("LEGACY_JSON_UNCHANGED"); var bll = new MenuGroupBLL(dal.Object, menuBLL.Object, null!, null!, null!); var result = await bll.GetMenuGroupTreeForSet(55, Login(), CancellationToken.None); Assert.Equal("LEGACY_JSON_UNCHANGED", result); menuBLL.Verify(x => x.GetMenuForUserModule(7, 3, It.IsAny()), Times.Once); // No MenuGroup flat-list/leaf-resolution call for a legacy set — confirms no new // code executes on this path, not just that the output happens to match. dal.Verify(x => x.GetMenuGroupFlatList(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task GetMenuGroupTreeForSet_NonLegacySetStillAtDefaultMenuGroupId_ThrowsRatherThanRenderingEmptyTree() { var dal = new Mock(); dal.Setup(x => x.GetMenuSetPresentationInfo(55, It.IsAny(), It.IsAny())) .ReturnsAsync(new MenuSetPresentationInfoDTO { MenuSetId = 55, ModuleId = 3, PresentationType = 1, MenuGroupId = -1 }); var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!); await Assert.ThrowsAsync(() => bll.GetMenuGroupTreeForSet(55, Login(), CancellationToken.None)); } [Fact] public async Task GetMenuGroupTreeForSet_UnknownMenuSetId_Throws() { var dal = new Mock(); dal.Setup(x => x.GetMenuSetPresentationInfo(999, It.IsAny(), It.IsAny())) .ReturnsAsync((MenuSetPresentationInfoDTO?)null); var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!); await Assert.ThrowsAsync(() => bll.GetMenuGroupTreeForSet(999, Login(), CancellationToken.None)); } [Fact] public async Task ValidateMenuGroupVisibility_SharedGroup_IsAssignableFromAnyMenuSet() { var dal = new Mock(); dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = null }); var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!); await bll.ValidateMenuGroupVisibility(10, callerMenuSetId: 999, Login(), CancellationToken.None); // No throw = pass. } [Fact] public async Task ValidateMenuGroupVisibility_PrivateGroup_OwnSet_IsAllowed() { var dal = new Mock(); dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = 55 }); var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!); await bll.ValidateMenuGroupVisibility(10, callerMenuSetId: 55, Login(), CancellationToken.None); } [Fact] public async Task ValidateMenuGroupVisibility_PrivateGroup_AnotherSet_IsRejected() { var dal = new Mock(); dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = 55 }); var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!); await Assert.ThrowsAsync( () => bll.ValidateMenuGroupVisibility(10, callerMenuSetId: 999, Login(), CancellationToken.None)); } [Fact] public async Task SaveMenuGroupDetail_MultipleNewLeaves_EachGetsADistinctSequentialId() { // Regression test for a real bug caught while wiring MenuSetBLL.SaveMenuSetDetail: // SaveMenuGroupDetail never actually called AutoNumber for new leaves, so every new // leaf kept MenuGroupDetailId == 0 (a PK collision the moment there was more than one // new leaf in a save). AutoNumber is a concrete class, but both of its own // dependencies (IQueryExecutor, IOptionsSnapshot) are interfaces, so a // real AutoNumber can be constructed here instead of needing null!. var dal = new Mock(); dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny())) .ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = null }); var qe = new Mock(); // AutoNumber.GetNumberAsync reads results[0] (a long — AutoId's SQL type) as the // post-increment AUTOID and derives firstNumber = newAutoId - noOfId (+ // ServerConfigOffset, 0 here) — reserving 2 ids starting at 100 means the // UPDATE ... SET AUTOID = AUTOID + 2 returns 102. qe.Setup(x => x.QueryAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { 102 }); var autoNumber = new AutoNumber(qe.Object, Mock.Of>( o => o.Value == new Gb5SystemDTO { DataBaseType = 0 })); // KeyInvalidate is passed null! — SaveMenuGroupDetail's cache-invalidation tail runs // after the id assignment under test, so a NullReferenceException there (KeyInvalidate // needs a real DaprClient this test isn't wiring up) is caught and ignored; the ids // are already assigned by the time that call happens. var bll = new MenuGroupBLL(dal.Object, Mock.Of(), autoNumber, qe.Object, null!); var leaves = new List { new() { MenuGroupDetailId = 0, MenuId = 501 }, new() { MenuGroupDetailId = 0, MenuId = 502 }, }; try { await bll.SaveMenuGroupDetail(10, leaves, Login(), CancellationToken.None); } catch (NullReferenceException) { } var ids = leaves.Select(l => l.MenuGroupDetailId).OrderBy(i => i).ToList(); Assert.Equal(new[] { 100, 101 }, ids); Assert.Equal(2, ids.Distinct().Count()); } }