diff --git a/server/config.py b/server/config.py index 7643aba..8362c45 100644 --- a/server/config.py +++ b/server/config.py @@ -8,7 +8,7 @@ def get_config() -> dict: """This is to keep logic of accessing config in one place""" global _config with open(settings.CONSERVER_CONFIG_FILE) as file: - _config = yaml.safe_load(file) + _config = yaml.safe_load(file) or {} return _config diff --git a/server/main.py b/server/main.py index 0ec4815..56f5387 100644 --- a/server/main.py +++ b/server/main.py @@ -642,7 +642,12 @@ def worker_loop(worker_id: int) -> None: ingress_chain_map = get_ingress_chain_map() all_ingress_lists = list(ingress_chain_map.keys()) logger.debug("[%s] Monitoring ingress lists: %s", worker_name, all_ingress_lists) - + + if not all_ingress_lists: + logger.warning("[%s] No ingress lists configured, retrying in 15s", worker_name) + time.sleep(15) + continue + logger.debug("[%s] Waiting for vCon on ingress lists (timeout: 15s)", worker_name) popped_item = r.blpop(all_ingress_lists, timeout=15) if not popped_item: diff --git a/server/tests/test_config_null.py b/server/tests/test_config_null.py new file mode 100644 index 0000000..10bdb98 --- /dev/null +++ b/server/tests/test_config_null.py @@ -0,0 +1,68 @@ +"""Tests for get_config() handling of null/empty config files (CON-510).""" + +from unittest.mock import patch, mock_open +from config import get_config + + +class TestGetConfigNullHandling: + """Test that get_config() returns {} when yaml.safe_load returns None.""" + + def test_get_config_returns_empty_dict_when_yaml_is_null(self): + """All-comment YAML makes yaml.safe_load return None; get_config must return {}.""" + # yaml.safe_load returns None for a file with only comments + null_yaml = "# just a comment\n" + with patch("builtins.open", mock_open(read_data=null_yaml)): + result = get_config() + + assert result == {}, f"Expected {{}} but got {result!r}" + + def test_get_config_returns_empty_dict_when_file_is_empty(self): + """Completely empty file also makes yaml.safe_load return None.""" + with patch("builtins.open", mock_open(read_data="")): + result = get_config() + + assert result == {}, f"Expected {{}} but got {result!r}" + + def test_get_config_returns_dict_when_valid(self): + """Normal config file should be returned as-is.""" + valid_yaml = "chains:\n my_chain:\n ingress_lists:\n - my_queue\n" + with patch("builtins.open", mock_open(read_data=valid_yaml)): + result = get_config() + + assert result == {"chains": {"my_chain": {"ingress_lists": ["my_queue"]}}} + + +class TestGetIngressChainMapNullConfig: + """Test that get_ingress_chain_map() does not crash when config is null.""" + + def test_get_ingress_chain_map_logic_with_null_config(self): + """Simulate get_ingress_chain_map() logic when config is {} (null yaml result). + + The real function does: + chains = config.get("chains", {}) + for chain_name, chain_config in chains.items(): ... + This must not crash when config is {}. + """ + null_yaml = "# only comments\n" + with patch("builtins.open", mock_open(read_data=null_yaml)): + cfg = get_config() + + # Replicate get_ingress_chain_map logic + chains = cfg.get("chains", {}) + ingress_details = {} + for chain_name, chain_config in chains.items(): + for ingress_list in chain_config.get("ingress_lists", []): + ingress_details[ingress_list] = {"name": chain_name, **chain_config} + + assert ingress_details == {} + + def test_worker_loop_does_not_crash_on_null_config(self): + """Simulate the worker_loop config reload path with a null config.""" + # Verify that calling .get() on the result of get_config() doesn't raise + null_yaml = "# nothing here\n" + with patch("builtins.open", mock_open(read_data=null_yaml)): + cfg = get_config() + + # This is exactly what get_ingress_chain_map() does — must not raise + chains = cfg.get("chains", {}) + assert chains == {}