Files
potato-mesh/tests/test_config_unit.py
T
l5y de1ccc5a2e release: v0.6.0 — remove deprecated env var aliases (#704)
* chore: bump version to 0.6.0 and remove deprecated env var aliases

BREAKING CHANGES:
- POTATOMESH_INSTANCE removed — use INSTANCE_DOMAIN
- PROVIDER removed — use PROTOCOL
- MESH_SERIAL removed — use CONNECTION
- PORT config alias removed — use CONNECTION

The _ConfigModule proxy class (which kept PROTOCOL/PROVIDER and
CONNECTION/PORT in sync) is deleted. docker-compose.yml now defaults
INSTANCE_DOMAIN to http://web:41447 so deployments without an explicit
value continue to work.

* tests: run black

* address review comments
2026-04-05 16:49:10 +02:00

212 lines
8.2 KiB
Python

# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for :mod:`data.mesh_ingestor.config`."""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
import data.mesh_ingestor.config as config
# ---------------------------------------------------------------------------
# _parse_channel_names
# ---------------------------------------------------------------------------
class TestParseChannelNames:
"""Tests for :func:`config._parse_channel_names`."""
def test_none_returns_empty(self):
"""None input returns empty tuple."""
assert config._parse_channel_names(None) == ()
def test_empty_string_returns_empty(self):
"""Empty string returns empty tuple."""
assert config._parse_channel_names("") == ()
def test_single_name(self):
"""Single channel name is returned as a one-element tuple."""
assert config._parse_channel_names("LongFast") == ("LongFast",)
def test_comma_separated(self):
"""Comma-separated names are split and returned."""
result = config._parse_channel_names("LongFast,Chat")
assert result == ("LongFast", "Chat")
def test_strips_whitespace(self):
"""Leading/trailing whitespace around names is stripped."""
result = config._parse_channel_names(" LongFast , Chat ")
assert result == ("LongFast", "Chat")
def test_deduplicates_case_insensitively(self):
"""Duplicate names (case-insensitively) are deduplicated."""
result = config._parse_channel_names("LongFast,longfast,LONGFAST")
assert result == ("LongFast",)
def test_preserves_order(self):
"""Original order is preserved, first occurrence kept on dedup."""
result = config._parse_channel_names("B,A,B,C")
assert result == ("B", "A", "C")
def test_empty_segments_skipped(self):
"""Empty segments from consecutive commas are skipped."""
result = config._parse_channel_names("A,,B,,,C")
assert result == ("A", "B", "C")
# ---------------------------------------------------------------------------
# _parse_hidden_channels
# ---------------------------------------------------------------------------
class TestParseHiddenChannels:
"""Tests for :func:`config._parse_hidden_channels`."""
def test_delegates_to_parse_channel_names(self):
"""_parse_hidden_channels delegates to _parse_channel_names."""
assert config._parse_hidden_channels(
"Chat,Admin"
) == config._parse_channel_names("Chat,Admin")
def test_none_returns_empty(self):
"""None input returns empty tuple."""
assert config._parse_hidden_channels(None) == ()
# ---------------------------------------------------------------------------
# _resolve_instance_domain
# ---------------------------------------------------------------------------
class TestResolveInstanceDomain:
"""Tests for :func:`config._resolve_instance_domain`."""
def test_returns_instance_domain_when_set(self, monkeypatch):
"""Uses INSTANCE_DOMAIN when set."""
monkeypatch.setenv("INSTANCE_DOMAIN", "mesh.example.com")
result = config._resolve_instance_domain()
assert result == "https://mesh.example.com"
def test_adds_https_when_no_scheme(self, monkeypatch):
"""Adds https:// prefix when no scheme is present."""
monkeypatch.setenv("INSTANCE_DOMAIN", "example.com")
assert config._resolve_instance_domain() == "https://example.com"
def test_preserves_existing_scheme(self, monkeypatch):
"""Leaves existing http:// scheme intact."""
monkeypatch.setenv("INSTANCE_DOMAIN", "http://example.com")
assert config._resolve_instance_domain() == "http://example.com"
def test_strips_trailing_slash(self, monkeypatch):
"""Strips trailing slash from instance domain."""
monkeypatch.setenv("INSTANCE_DOMAIN", "https://example.com/")
assert config._resolve_instance_domain() == "https://example.com"
def test_returns_empty_when_not_set(self, monkeypatch):
"""Returns empty string when INSTANCE_DOMAIN is unset."""
monkeypatch.delenv("INSTANCE_DOMAIN", raising=False)
assert config._resolve_instance_domain() == ""
# ---------------------------------------------------------------------------
# _debug_log
# ---------------------------------------------------------------------------
class TestDebugLog:
"""Tests for :func:`config._debug_log`."""
def test_suppressed_when_debug_false(self, monkeypatch, capsys):
"""Nothing is printed when DEBUG is False and severity is debug."""
monkeypatch.setattr(config, "DEBUG", False)
config._debug_log("silent", severity="debug")
assert capsys.readouterr().out == ""
def test_prints_when_debug_true(self, monkeypatch, capsys):
"""Message is printed when DEBUG is True."""
monkeypatch.setattr(config, "DEBUG", True)
config._debug_log("hello world")
out = capsys.readouterr().out
assert "hello world" in out
def test_always_flag_bypasses_debug_guard(self, monkeypatch, capsys):
"""always=True forces output even when DEBUG is False."""
monkeypatch.setattr(config, "DEBUG", False)
config._debug_log("force print", always=True)
out = capsys.readouterr().out
assert "force print" in out
def test_context_included_in_output(self, monkeypatch, capsys):
"""Context label is included in log output."""
monkeypatch.setattr(config, "DEBUG", True)
config._debug_log("msg", context="test.ctx")
out = capsys.readouterr().out
assert "context=test.ctx" in out
def test_severity_included_in_output(self, monkeypatch, capsys):
"""Severity level is included in log output."""
monkeypatch.setattr(config, "DEBUG", True)
config._debug_log("msg", severity="warn")
out = capsys.readouterr().out
assert "[warn]" in out
def test_metadata_included_in_output(self, monkeypatch, capsys):
"""Additional metadata key=value pairs are included in output."""
monkeypatch.setattr(config, "DEBUG", True)
config._debug_log("msg", node_id="!aabb1234")
out = capsys.readouterr().out
assert "node_id=" in out
def test_warn_severity_printed_even_when_debug_false(self, monkeypatch, capsys):
"""Non-debug severity is printed regardless of DEBUG flag."""
monkeypatch.setattr(config, "DEBUG", False)
config._debug_log("warn msg", severity="warn")
out = capsys.readouterr().out
assert "warn msg" in out
# ---------------------------------------------------------------------------
# PROTOCOL validation
# ---------------------------------------------------------------------------
class TestProtocolValidation:
"""Tests for PROTOCOL environment validation at import time."""
def test_valid_protocol_does_not_raise(self, monkeypatch):
"""Importing config with a valid PROTOCOL succeeds."""
import importlib
monkeypatch.setenv("PROTOCOL", "meshtastic")
# Re-importing should not raise
importlib.reload(config)
def test_invalid_protocol_raises_value_error(self, monkeypatch):
"""An invalid PROTOCOL value raises ValueError at module load."""
import importlib
monkeypatch.setenv("PROTOCOL", "bogus_protocol_xyz")
with pytest.raises(ValueError, match="Unknown PROTOCOL"):
importlib.reload(config)
# Restore to valid value so subsequent tests work
monkeypatch.setenv("PROTOCOL", "meshtastic")
importlib.reload(config)