diff --git a/Products/zms/repositoryutil.py b/Products/zms/repositoryutil.py index 9f2dba093..61b41bf60 100644 --- a/Products/zms/repositoryutil.py +++ b/Products/zms/repositoryutil.py @@ -597,7 +597,7 @@ def get_init_yaml(self, o): d = {} for k in keys: v = o.get(k) - nv = yamlutil.__cleanup(v) + nv = yamlutil._cleanup(v) if nv: d[k] = nv elif nv in ['0', 0, False]: @@ -607,7 +607,7 @@ def get_init_yaml(self, o): nl = [] l = o.get(k) for i in l: - ni = yamlutil.__cleanup(i) + ni = yamlutil._cleanup(i) if ni: if type(ni) is dict: # Remove 'ob' from attribute dict (Acquisition-Wrappers are not serializable). diff --git a/Products/zms/version.txt b/Products/zms/version.txt index 1d80e467c..1f7483cd9 100644 --- a/Products/zms/version.txt +++ b/Products/zms/version.txt @@ -1 +1 @@ -6.1.0+5a175b9 +6.1.0+120ff56 diff --git a/Products/zms/yamlutil.py b/Products/zms/yamlutil.py index 083f96a1d..f59afae4b 100644 --- a/Products/zms/yamlutil.py +++ b/Products/zms/yamlutil.py @@ -51,7 +51,7 @@ def represent_struct_time(dumper, data): yaml.indent(mapping=2, sequence=4, offset=2) stream = io.StringIO() try: - yaml.dump(__cleanup(data), stream) + yaml.dump(_cleanup(data), stream) except Exception as e: return f"Error during YAML serialization: {str(e)}" return stream.getvalue() @@ -79,7 +79,7 @@ def construct_struct_time(loader, node): return yaml.load(data) -def __cleanup(v): +def _cleanup(v): """ Recursively cleans up a dictionary by removing keys with falsy values. @param v: Input value, typically a dictionary, list, or scalar. @@ -93,12 +93,13 @@ def __cleanup(v): return err from ruamel.yaml.scalarstring import LiteralScalarString + from ruamel.yaml.scalarstring import DoubleQuotedScalarString if v: if isinstance(v, dict): nd = {} for k in list(v.keys()): - nv = __cleanup(v[k]) + nv = _cleanup(v[k]) if nv: nd[k] = nv elif nv in ['0', 0, False]: @@ -107,7 +108,7 @@ def __cleanup(v): elif isinstance(v, list): nl = [] for i in v: - nv = __cleanup(i) + nv = _cleanup(i) if nv: nl.append(nv) return nl @@ -117,4 +118,13 @@ def __cleanup(v): # we need to convert it to a string before dumping. elif hasattr(v, 'read') and callable(v.read): return str(v.read()) + # If v is a object with a getData method. + elif hasattr(v, "getData") and callable(v.getData): + data = v.getData() + if isinstance(data, (bytes, bytearray)): + v = data.decode(errors="replace") + else: + v = str(data) + v = DoubleQuotedScalarString(v) + return v \ No newline at end of file diff --git a/tests/test_yamlutil.py b/tests/test_yamlutil.py new file mode 100644 index 000000000..c60d023c1 --- /dev/null +++ b/tests/test_yamlutil.py @@ -0,0 +1,125 @@ +import time +import builtins +import pytest +from unittest.mock import patch, MagicMock +from Products.zms import yamlutil + + +# --------------------------------------------------------- +# _load_yaml +# --------------------------------------------------------- + +def test_load_yaml_success(): + YAML = MagicMock() + mock_ruamel_yaml = MagicMock(YAML=YAML) + + with patch.dict('sys.modules', {'ruamel.yaml': mock_ruamel_yaml}): + result, err = yamlutil._load_yaml() + assert result is YAML + assert err is None + + +def test_load_yaml_import_error(): + with patch.dict('sys.modules', {'ruamel.yaml': None}): + result, err = yamlutil._load_yaml() + assert result is None + assert err == yamlutil.IMPORT_ERROR_MSG + + +# --------------------------------------------------------- +# dump() +# --------------------------------------------------------- + +def test_dump_import_error(): + with patch("Products.zms.yamlutil._load_yaml", return_value=(None, yamlutil.IMPORT_ERROR_MSG)): + assert yamlutil.dump({"a": 1}) == yamlutil.IMPORT_ERROR_MSG + + +def test_dump_struct_time_serialization(): + YAML = MagicMock() + yaml_instance = MagicMock() + YAML.return_value = yaml_instance + + with patch("Products.zms.yamlutil._load_yaml", return_value=(YAML, None)): + stream = MagicMock() + yaml_instance.dump = MagicMock() + + data = time.strptime("2024-01-01 12:00:00", "%Y-%m-%d %H:%M:%S") + yamlutil.dump({"t": data}) + + # Representer should be registered + assert yaml_instance.representer.add_representer.called + + # Dump should be called + assert yaml_instance.dump.called + + +def test_dump_error_during_serialization(): + YAML = MagicMock() + yaml_instance = MagicMock() + YAML.return_value = yaml_instance + + yaml_instance.dump.side_effect = Exception("boom") + + with patch("Products.zms.yamlutil._load_yaml", return_value=(YAML, None)): + result = yamlutil.dump({"a": 1}) + assert "Error during YAML serialization" in result + + +# --------------------------------------------------------- +# parse() +# --------------------------------------------------------- + +def test_parse_import_error(): + with patch("Products.zms.yamlutil._load_yaml", return_value=(None, yamlutil.IMPORT_ERROR_MSG)): + assert yamlutil.parse("a: 1") == yamlutil.IMPORT_ERROR_MSG + + +def test_parse_struct_time(): + YAML = MagicMock() + yaml_instance = MagicMock() + YAML.return_value = yaml_instance + + yaml_instance.load.return_value = {"t": "dummy"} + + with patch("Products.zms.yamlutil._load_yaml", return_value=(YAML, None)): + result = yamlutil.parse("t: !struct_time 2024-01-01 12:00:00") + yaml_instance.constructor.add_constructor.assert_called() + assert yaml_instance.load.called + + +# --------------------------------------------------------- +# _cleanup() +# --------------------------------------------------------- + +def test_cleanup_removes_empty_values(): + assert yamlutil._cleanup({ + "a": "", + "b": None, + "c": [], + "d": {}, + "e": 0, + "f": False, + "g": "ok" + }) == {"e": 0, "f": False, "g": "ok"} + + +def test_cleanup_nested(): + data = { + "a": { + "x": "", + "y": 1 + }, + "b": [ + "", + 2, + None, + [] + ] + } + + cleaned = yamlutil._cleanup(data) + assert cleaned == { + "a": {"y": 1}, + "b": [2] + }