Reflect the current year in all TechnoLibre license headers across script/, test/, and docker/. Generated by Claude Code 2.1.74 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
262 lines
8.6 KiB
Python
262 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from script.config.config_file import ConfigFile
|
|
|
|
|
|
class TestDeepMergeWithLists(unittest.TestCase):
|
|
def setUp(self):
|
|
self.cfg = ConfigFile()
|
|
|
|
def test_empty_dicts(self):
|
|
result = self.cfg.deep_merge_with_lists({}, {})
|
|
self.assertEqual(result, {})
|
|
|
|
def test_dest_only(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": 1, "b": 2}, {}
|
|
)
|
|
self.assertEqual(result, {"a": 1, "b": 2})
|
|
|
|
def test_src_only(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{}, {"a": 1, "b": 2}
|
|
)
|
|
self.assertEqual(result, {"a": 1, "b": 2})
|
|
|
|
def test_simple_merge(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": 1}, {"b": 2}
|
|
)
|
|
self.assertEqual(result, {"a": 1, "b": 2})
|
|
|
|
def test_src_overrides_dest_string(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": "old"}, {"a": "new"}
|
|
)
|
|
self.assertEqual(result, {"a": "new"})
|
|
|
|
def test_empty_src_string_keeps_dest(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": "old"}, {"a": ""}
|
|
)
|
|
self.assertEqual(result, {"a": "old"})
|
|
|
|
def test_nested_dict_merge(self):
|
|
dest = {"a": {"x": 1, "y": 2}}
|
|
src = {"a": {"y": 3, "z": 4}}
|
|
result = self.cfg.deep_merge_with_lists(dest, src)
|
|
self.assertEqual(result, {"a": {"x": 1, "y": 3, "z": 4}})
|
|
|
|
def test_deeply_nested_dict_merge(self):
|
|
dest = {"a": {"b": {"c": 1}}}
|
|
src = {"a": {"b": {"d": 2}}}
|
|
result = self.cfg.deep_merge_with_lists(dest, src)
|
|
self.assertEqual(result, {"a": {"b": {"c": 1, "d": 2}}})
|
|
|
|
def test_list_replace_strategy(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": [1, 2]}, {"a": [3, 4]}, list_strategy="replace"
|
|
)
|
|
self.assertEqual(result, {"a": [3, 4]})
|
|
|
|
def test_list_extend_strategy(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": [1, 2]}, {"a": [3, 4]}, list_strategy="extend"
|
|
)
|
|
self.assertEqual(result, {"a": [1, 2, 3, 4]})
|
|
|
|
def test_dest_dict_not_mutated(self):
|
|
dest = {"a": {"x": 1}}
|
|
src = {"a": {"y": 2}}
|
|
self.cfg.deep_merge_with_lists(dest, src)
|
|
self.assertEqual(dest, {"a": {"x": 1}})
|
|
|
|
def test_src_overrides_non_string_non_dict_non_list(self):
|
|
result = self.cfg.deep_merge_with_lists(
|
|
{"a": 1}, {"a": 2}
|
|
)
|
|
self.assertEqual(result, {"a": 2})
|
|
|
|
|
|
class TestGetConfig(unittest.TestCase):
|
|
def setUp(self):
|
|
self.cfg = ConfigFile()
|
|
self.tmpdir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
for f in os.listdir(self.tmpdir):
|
|
os.remove(os.path.join(self.tmpdir, f))
|
|
os.rmdir(self.tmpdir)
|
|
|
|
def _write_json(self, filename, data):
|
|
path = os.path.join(self.tmpdir, filename)
|
|
with open(path, "w") as f:
|
|
json.dump(data, f)
|
|
return path
|
|
|
|
def test_get_config_base_only(self):
|
|
base_path = self._write_json(
|
|
"base.json", {"instance": [{"name": "test"}]}
|
|
)
|
|
with patch(
|
|
"script.config.config_file.CONFIG_FILE", base_path
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
|
os.path.join(self.tmpdir, "nonexistent1.json"),
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
|
os.path.join(self.tmpdir, "nonexistent2.json"),
|
|
):
|
|
result = self.cfg.get_config("instance")
|
|
self.assertEqual(result, [{"name": "test"}])
|
|
|
|
def test_get_config_returns_none_for_missing_key(self):
|
|
base_path = self._write_json("base.json", {"a": 1})
|
|
with patch(
|
|
"script.config.config_file.CONFIG_FILE", base_path
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
|
os.path.join(self.tmpdir, "nonexistent1.json"),
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
|
os.path.join(self.tmpdir, "nonexistent2.json"),
|
|
):
|
|
result = self.cfg.get_config("missing")
|
|
self.assertIsNone(result)
|
|
|
|
def test_get_config_override_merges(self):
|
|
base_path = self._write_json(
|
|
"base.json",
|
|
{"instance": [{"name": "base"}]},
|
|
)
|
|
override_path = self._write_json(
|
|
"override.json",
|
|
{"instance": [{"name": "override"}]},
|
|
)
|
|
with patch(
|
|
"script.config.config_file.CONFIG_FILE", base_path
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
|
override_path,
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
|
os.path.join(self.tmpdir, "nonexistent.json"),
|
|
):
|
|
result = self.cfg.get_config("instance")
|
|
# Lists with extend: base + override
|
|
self.assertEqual(
|
|
result,
|
|
[{"name": "base"}, {"name": "override"}],
|
|
)
|
|
|
|
def test_get_config_private_merges(self):
|
|
base_path = self._write_json(
|
|
"base.json",
|
|
{"data": {"key": "base_val"}},
|
|
)
|
|
private_path = self._write_json(
|
|
"private.json",
|
|
{"data": {"key": "private_val"}},
|
|
)
|
|
with patch(
|
|
"script.config.config_file.CONFIG_FILE", base_path
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
|
os.path.join(self.tmpdir, "nonexistent.json"),
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
|
private_path,
|
|
):
|
|
result = self.cfg.get_config("data")
|
|
self.assertEqual(result, {"key": "private_val"})
|
|
|
|
def test_get_config_all_three_files(self):
|
|
base_path = self._write_json(
|
|
"base.json",
|
|
{"items": [1], "meta": {"a": "base"}},
|
|
)
|
|
override_path = self._write_json(
|
|
"override.json",
|
|
{"items": [2], "meta": {"b": "override"}},
|
|
)
|
|
private_path = self._write_json(
|
|
"private.json",
|
|
{"items": [3], "meta": {"a": "private"}},
|
|
)
|
|
with patch(
|
|
"script.config.config_file.CONFIG_FILE", base_path
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
|
override_path,
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
|
private_path,
|
|
):
|
|
result_items = self.cfg.get_config("items")
|
|
result_meta = self.cfg.get_config("meta")
|
|
# Merge order: base + private, then + override
|
|
# Lists extend: [1] + [3] = [1,3], then [1,3] + [2] = [1,3,2]
|
|
self.assertEqual(result_items, [1, 3, 2])
|
|
# Dict merge: {a: base} + {a: private} = {a: private}
|
|
# then {a: private} + {b: override} = {a: private, b: override}
|
|
self.assertEqual(
|
|
result_meta, {"a": "private", "b": "override"}
|
|
)
|
|
|
|
def test_no_config_files_exist(self):
|
|
with patch(
|
|
"script.config.config_file.CONFIG_FILE",
|
|
os.path.join(self.tmpdir, "nope1.json"),
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_FILE",
|
|
os.path.join(self.tmpdir, "nope2.json"),
|
|
), patch(
|
|
"script.config.config_file.CONFIG_OVERRIDE_PRIVATE_FILE",
|
|
os.path.join(self.tmpdir, "nope3.json"),
|
|
):
|
|
result = self.cfg.get_config("anything")
|
|
self.assertIsNone(result)
|
|
|
|
|
|
class TestGetConfigValue(unittest.TestCase):
|
|
def setUp(self):
|
|
self.cfg = ConfigFile()
|
|
|
|
def test_nested_value(self):
|
|
with patch.object(
|
|
self.cfg,
|
|
"get_config",
|
|
return_value={"level1": {"level2": "found"}},
|
|
):
|
|
result = self.cfg.get_config_value(
|
|
["root", "level1", "level2"]
|
|
)
|
|
self.assertEqual(result, "found")
|
|
|
|
def test_single_key(self):
|
|
with patch.object(
|
|
self.cfg,
|
|
"get_config",
|
|
return_value={"key": "value"},
|
|
):
|
|
result = self.cfg.get_config_value(["root"])
|
|
self.assertEqual(result, {"key": "value"})
|
|
|
|
|
|
class TestGetLogoAsciiFilePath(unittest.TestCase):
|
|
def test_returns_logo_path(self):
|
|
cfg = ConfigFile()
|
|
result = cfg.get_logo_ascii_file_path()
|
|
self.assertEqual(result, "./script/todo/logo_ascii.txt")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|