# Copyright (c) 2026 PowerOn AG # All rights reserved. """Unit tests for the generic UDB tree builder (`_buildTree.py`). Most node-level behavior moved into the polymorphic class hierarchy (`udbNodes.py`) and has its own dedicated tests in `test_udbNodes.py`. This file covers the orchestrator (`getChildrenForParents`) and the remaining lookup helpers. """ from __future__ import annotations import asyncio import unittest from unittest.mock import MagicMock, patch from modules.serviceCenter.services.serviceKnowledge import _buildTree class TestKeyCoding(unittest.TestCase): def test_encode_decode_roundtrip(self): key = _buildTree._encode("ds", "conn-1", "sharepointFolder", "/sites/x") kind, parts = _buildTree._decode(key) self.assertEqual(kind, "ds") self.assertEqual(parts, ["conn-1", "sharepointFolder", "/sites/x"]) def test_top_level_kinds(self): self.assertEqual(_buildTree._decode("conn|abc")[0], "conn") self.assertEqual(_buildTree._decode("mgrp|m1")[0], "mgrp") self.assertEqual(_buildTree._decode("feat|m1|trustee|fi-1")[1], ["m1", "trustee", "fi-1"]) class TestRecordLookup(unittest.TestCase): def test_finds_ds_record_by_normalised_path(self): rec = {"id": "x", "connectionId": "c", "sourceType": "msft", "path": "/folder"} self.assertEqual(_buildTree._findDsRecord([rec], "c", "msft", "/folder/").get("id"), "x") self.assertIsNone(_buildTree._findDsRecord([rec], "c", "msft", "/other")) def test_finds_fds_record_with_matching_filter(self): rec = {"id": "f", "featureInstanceId": "fi1", "tableName": "Pos", "recordFilter": {"id": "5"}} self.assertEqual(_buildTree._findFdsRecord([rec], "fi1", "Pos", {"id": "5"}).get("id"), "f") self.assertIsNone(_buildTree._findFdsRecord([rec], "fi1", "Pos", {"id": "99"})) def test_fds_record_with_none_filter_matches_only_none(self): rec = {"id": "f", "featureInstanceId": "fi1", "tableName": "*", "recordFilter": None} self.assertEqual(_buildTree._findFdsRecord([rec], "fi1", "*", None).get("id"), "f") self.assertIsNone(_buildTree._findFdsRecord([rec], "fi1", "*", {"id": "1"})) class TestWiredTableFieldAggregation(unittest.TestCase): """`_wireTableFieldsAsLogicalChildren` rewraps `FdsTableNode.getEffectiveFlag` so the table aggregates with its declared field children. The aggregate must respect the new FdsField inheritance: if all fields inherit from the table (no list entries), the table stays non-mixed.""" def _buildTableWithFields(self, *, tableNeutralize, neutralizeFields, fieldNames): from modules.serviceCenter.services.serviceKnowledge.udbNodes import ( FdsTableNode, FdsFieldNode, ) rec = { "id": "fds-tbl", "featureInstanceId": "fi1", "tableName": "Pos", "recordFilter": None, "neutralize": tableNeutralize, "neutralizeFields": neutralizeFields, } tableNode = FdsTableNode("fi1", "trustee", "Pos", "key", "Positions", "feat|m1|trustee|fi1", rec=rec, hasFields=True) fields = [ FdsFieldNode("fi1", "Pos", name, tableNode.key, tableRec=rec, featureCode="trustee") for name in fieldNames ] tableNode._logicalFieldChildren = fields # type: ignore[attr-defined] _buildTree._wireTableFieldsAsLogicalChildren(tableNode) return tableNode, [rec] def test_table_true_no_overrides_stays_true(self): """Regression: toggling a table to True must NOT leave it 'mixed' because the declared field children should inherit the table value.""" tableNode, allFds = self._buildTableWithFields( tableNeutralize=True, neutralizeFields=None, fieldNames=["amount", "currency"], ) self.assertTrue(tableNode.getEffectiveFlag("neutralize", [], allFds, "aggregate")) def test_table_false_with_override_is_mixed(self): tableNode, allFds = self._buildTableWithFields( tableNeutralize=False, neutralizeFields=["amount"], fieldNames=["amount", "currency"], ) self.assertEqual( tableNode.getEffectiveFlag("neutralize", [], allFds, "aggregate"), "mixed", ) def test_table_inherit_no_overrides_walks_default(self): """Implicit table + no overrides + no workspace -> default False.""" tableNode, allFds = self._buildTableWithFields( tableNeutralize=None, neutralizeFields=None, fieldNames=["amount", "currency"], ) self.assertFalse(tableNode.getEffectiveFlag("neutralize", [], allFds, "aggregate")) class TestGetChildrenForParents(unittest.TestCase): """End-to-end orchestrator tests with mocked dependencies. The orchestrator returns serialised node dicts produced by `UdbNode.toDict(...)`, so the keys/kinds/parentKey wiring is what matters here -- not the inheritance arithmetic (covered in test_udbNodes.py).""" def _runAsync(self, coro): return asyncio.run(coro) def test_unknown_parent_key_returns_empty_list(self): with patch("modules.interfaces.interfaceDbApp.getRootInterface") as mockRoot: rootIf = MagicMock() rootIf.db.getRecordset.return_value = [] mockRoot.return_value = rootIf ctx = MagicMock() ctx.user.id = "u1" ctx.mandateId = "m1" result = self._runAsync( _buildTree.getChildrenForParents(["bogus|key"], ctx) ) self.assertEqual(result["bogus|key"], []) def test_top_level_emits_personal_root_first(self): with patch("modules.interfaces.interfaceDbApp.getRootInterface") as mockRoot, \ patch("modules.serviceCenter.services.serviceKnowledge._buildTree._personalRootChildrenNodes", return_value=[]), \ patch("modules.security.rbacCatalog.getCatalogService"): rootIf = MagicMock() rootIf.db.getRecordset.return_value = [] rootIf.getUserMandates.return_value = [] mockRoot.return_value = rootIf ctx = MagicMock() ctx.user.id = "u1" ctx.mandateId = "m1" result = self._runAsync( _buildTree.getChildrenForParents([None], ctx) ) children = result["__root__"] self.assertGreaterEqual(len(children), 1) personalRoot = children[0] self.assertEqual(personalRoot["key"], "personalRoot") self.assertEqual(personalRoot["kind"], "synthRoot") self.assertIsNone(personalRoot["parentKey"]) self.assertTrue(personalRoot["hasChildren"]) self.assertTrue(personalRoot["defaultExpanded"]) def test_top_level_emits_mandate_groups_inline(self): with patch("modules.interfaces.interfaceDbApp.getRootInterface") as mockRoot, \ patch("modules.security.rbacCatalog.getCatalogService") as mockCatalog, \ patch("modules.serviceCenter.services.serviceKnowledge._buildTree._personalRootChildrenNodes", return_value=[]): rootIf = MagicMock() rootIf.db.getRecordset.return_value = [] userMandate = MagicMock() userMandate.mandateId = "m1" rootIf.getUserMandates.return_value = [userMandate] featureInst = MagicMock() featureInst.id = "fi-1" featureInst.featureCode = "trustee" featureInst.enabled = True rootIf.getFeatureInstancesByMandate.return_value = [featureInst] featureAccess = MagicMock(enabled=True) rootIf.getFeatureAccess.return_value = featureAccess mockRoot.return_value = rootIf catalog = MagicMock() catalog.getFeaturesWithDataObjects.return_value = ["trustee"] mockCatalog.return_value = catalog ctx = MagicMock() ctx.user.id = "u1" ctx.mandateId = None result = self._runAsync(_buildTree.getChildrenForParents([None], ctx)) byKey = {c["key"]: c for c in result["__root__"]} self.assertIn("personalRoot", byKey) self.assertIn("mgrp|m1", byKey) mgroup = byKey["mgrp|m1"] self.assertEqual(mgroup["kind"], "mandateGroup") self.assertIsNone(mgroup["parentKey"]) self.assertEqual(mgroup["mandateId"], "m1") self.assertTrue(mgroup["defaultExpanded"]) if __name__ == "__main__": unittest.main()