using System.Collections.Generic; using System.Linq; using GB5Shared.Tree; using FrameworkDAL.DTO.MenuGroup; using Xunit; namespace FrameworkTests; /// /// Covers the -1-sentinel-to-null-Parent translation MenuGroupBLL.GetMenuGroupTreeForSet /// applies before calling TreeHelper.ConvertToTree (mirroring MenuBLL's existing MMENU /// handling), plus the shared/private node-visibility mix and orphaned-parent handling /// called out in the plan's Phase 4 test list. Pure C# — no DB involved, since /// TreeHelper itself is DB-agnostic. /// public class MenuGroupTreeTests { private static MenuGroupDTO Node(int id, string code, int parentGroupId, int? menuSetId = null) => new() { MenuGroupId = id, GroupCode = code, GroupName = code, ParentGroupId = parentGroupId, MenuSetId = menuSetId, }; private static void ApplySentinelTranslation(IEnumerable flat) { foreach (var node in flat) node.Parent = node.ParentGroupId == -1 ? null : new MenuGroupDTO { MenuGroupId = node.ParentGroupId }; } [Fact] public void ConvertToTree_TranslatesMinusOneSentinelToRoot() { var flat = new List { Node(1, "TOP", parentGroupId: -1), Node(2, "CHILD", parentGroupId: 1), }; ApplySentinelTranslation(flat); var forest = flat.ConvertToTree(); var top = Assert.Single(forest); Assert.Equal(1, top.MenuGroupId); var child = Assert.Single(top.children); Assert.Equal(2, child.MenuGroupId); } [Fact] public void ConvertToTree_SharedAndPrivateNodesCoexistInOneForest() { // MENUSETID IS NULL = shared bundle node; MENUSETID = 42 = private to that one set. var flat = new List { Node(1, "SHARED_TOP", parentGroupId: -1, menuSetId: null), Node(2, "SHARED_CHILD", parentGroupId: 1, menuSetId: null), Node(3, "PRIVATE_CHILD", parentGroupId: 1, menuSetId: 42), }; ApplySentinelTranslation(flat); var forest = flat.ConvertToTree(); var top = forest.FindTreeNode(1)!; Assert.Equal(2, top.children.Count); Assert.Contains(top.children, c => c.MenuGroupId == 2); Assert.Contains(top.children, c => c.MenuGroupId == 3 && c.MenuSetId == 42); } [Fact] public void ConvertToTree_OrphanedParent_NodeIsSilentlyDropped() { // Node 2's parent (99) never made it into the flat list — e.g. a private group whose // parent was deleted, or simply not visible to this MenuSetId's filter. TreeHelper's // documented behavior is to drop the orphan rather than throw. var flat = new List { Node(1, "TOP", parentGroupId: -1), Node(2, "ORPHAN", parentGroupId: 99), }; ApplySentinelTranslation(flat); var forest = flat.ConvertToTree(); Assert.Single(forest); Assert.Null(forest.FindTreeNode(2)); } [Fact] public void FindTreeNode_ReturnsConfiguredRoot_AndDepthFirstTraversalCollectsAllDescendantIds() { var flat = new List { Node(1, "ROOT", parentGroupId: -1), Node(2, "CHILD_A", parentGroupId: 1), Node(3, "CHILD_B", parentGroupId: 1), Node(4, "GRANDCHILD", parentGroupId: 2), }; ApplySentinelTranslation(flat); var forest = flat.ConvertToTree(); var root = forest.FindTreeNode(1); Assert.NotNull(root); var ids = root!.DepthFirstTraversal().Select(n => n.MenuGroupId).OrderBy(i => i).ToList(); Assert.Equal(new[] { 1, 2, 3, 4 }, ids); } [Fact] public void FindTreeNode_MisconfiguredRoot_ReturnsNull() { // MMENUSET.MenuGroupId pointing at a group id that doesn't exist in the flat list // (e.g. a private node scoped to a different MenuSetId) must be detectable by the // BLL as "not found", not silently treated as an empty tree. var flat = new List { Node(1, "TOP", parentGroupId: -1) }; ApplySentinelTranslation(flat); var forest = flat.ConvertToTree(); Assert.Null(forest.FindTreeNode(404)); } }