webdrop-bridge/tests/unit/test_config.py
claudi eab1009d8c Add brand-specific update channel and environment configuration
- Updated `brand_config.py` to include `WEBDROP_UPDATE_CHANNEL` in the environment variables.
- Enhanced `build_macos.sh` to create a bundled `.env` file with brand-specific defaults, including the update channel.
- Implemented a method in `build_windows.py` to create a bundled `.env` file for Windows builds, incorporating brand-specific runtime defaults.
- Modified `config.py` to ensure the application can locate the `.env` file in various installation scenarios.
- Added unit tests in `test_config.py` to verify the loading of the bootstrap `.env` from the PyInstaller runtime directory.
- Generated new WiX object and script files for the Windows installer, including application shortcuts and registry entries.
2026-03-12 09:04:27 +01:00

254 lines
8.9 KiB
Python

"""Unit tests for configuration system."""
import os
import sys
import pytest
from webdrop_bridge.config import Config, ConfigurationError
@pytest.fixture(autouse=True)
def clear_env():
"""Clear environment variables before each test to avoid persistence."""
# Save current env
saved_env = os.environ.copy()
# Clear relevant variables
for key in list(os.environ.keys()):
if key.startswith(
(
"APP_",
"LOG_",
"ALLOWED_",
"WEBAPP_",
"WINDOW_",
"ENABLE_",
"BRAND_",
"UPDATE_",
"LANGUAGE",
)
):
del os.environ[key]
yield
# Restore env (cleanup)
os.environ.clear()
os.environ.update(saved_env)
class TestConfigFromEnv:
"""Test Config.from_env() loading from environment."""
def test_from_env_with_all_values(self, tmp_path):
"""Test loading config with all environment variables set."""
# Create .env file
env_file = tmp_path / ".env"
root1 = tmp_path / "root1"
root2 = tmp_path / "root2"
root1.mkdir()
root2.mkdir()
env_file.write_text(
f"APP_NAME=TestApp\n"
f"APP_VERSION=2.0.0\n"
f"LOG_LEVEL=DEBUG\n"
f"LOG_FILE={tmp_path / 'test.log'}\n"
f"ALLOWED_ROOTS={root1},{root2}\n"
f"ALLOWED_URLS=example.com,*.test.org\n"
f"WEBAPP_URL=http://localhost:8000\n"
f"WINDOW_WIDTH=1200\n"
f"WINDOW_HEIGHT=800\n"
)
# Load config (env vars from file, not system)
config = Config.from_env(str(env_file))
# Version always comes from package __init__.py, not from APP_VERSION env var
from webdrop_bridge import __version__
assert config.app_name == "TestApp"
assert config.app_version == __version__ # Uses package version, not env var
assert config.log_level == "DEBUG"
assert config.allowed_roots == [root1.resolve(), root2.resolve()]
assert config.allowed_urls == ["example.com", "*.test.org"]
assert config.webapp_url == "http://localhost:8000"
assert config.window_width == 1200
assert config.window_height == 800
def test_from_env_with_branding_values(self, tmp_path):
"""Test loading branding and update metadata from environment."""
env_file = tmp_path / ".env"
root1 = tmp_path / "root1"
root1.mkdir()
env_file.write_text(
f"BRAND_ID=agravity\n"
f"APP_CONFIG_DIR_NAME=agravity_bridge\n"
f"UPDATE_REPO=HIM-public/webdrop-bridge\n"
f"UPDATE_CHANNEL=stable\n"
f"UPDATE_MANIFEST_NAME=release-manifest.json\n"
f"ALLOWED_ROOTS={root1}\n"
)
config = Config.from_env(str(env_file))
assert config.brand_id == "agravity"
assert config.config_dir_name == "agravity_bridge"
assert config.update_repo == "HIM-public/webdrop-bridge"
assert config.update_channel == "stable"
assert config.update_manifest_name == "release-manifest.json"
def test_from_env_with_defaults(self, tmp_path):
"""Test loading config uses defaults when env vars not set."""
# Create empty .env file
env_file = tmp_path / ".env"
env_file.write_text("")
config = Config.from_env(str(env_file))
assert config.app_name == "WebDrop Bridge"
assert config.brand_id == "webdrop_bridge"
assert config.config_dir_name == "webdrop_bridge"
# Version should come from __init__.py (dynamic, not hardcoded)
from webdrop_bridge import __version__
assert config.app_version == __version__
assert config.log_level == "INFO"
assert config.window_width == 1024
assert config.window_height == 768
def test_from_env_invalid_log_level(self, tmp_path):
"""Test that invalid log level raises ConfigurationError."""
env_file = tmp_path / ".env"
root1 = tmp_path / "root1"
root1.mkdir()
env_file.write_text(f"LOG_LEVEL=INVALID\nALLOWED_ROOTS={root1}\n")
with pytest.raises(ConfigurationError, match="Invalid LOG_LEVEL"):
Config.from_env(str(env_file))
def test_from_env_invalid_window_dimension(self, tmp_path):
"""Test that negative window dimensions raise ConfigurationError."""
env_file = tmp_path / ".env"
root1 = tmp_path / "root1"
root1.mkdir()
env_file.write_text(f"WINDOW_WIDTH=-100\nALLOWED_ROOTS={root1}\n")
with pytest.raises(ConfigurationError, match="Window dimensions"):
Config.from_env(str(env_file))
def test_from_env_invalid_root_path(self, tmp_path):
"""Test that non-existent root paths are logged as warning but don't raise error."""
env_file = tmp_path / ".env"
env_file.write_text("ALLOWED_ROOTS=/nonexistent/path/that/does/not/exist\n")
# Should not raise - just logs warning and returns empty allowed_roots
config = Config.from_env(str(env_file))
assert config.allowed_roots == [] # Non-existent roots are skipped
def test_from_env_empty_webapp_url(self, tmp_path):
"""Test that empty webapp URL raises ConfigurationError."""
env_file = tmp_path / ".env"
root1 = tmp_path / "root1"
root1.mkdir()
env_file.write_text(f"WEBAPP_URL=\nALLOWED_ROOTS={root1}\n")
with pytest.raises(ConfigurationError, match="WEBAPP_URL"):
Config.from_env(str(env_file))
class TestConfigValidation:
"""Test Config field validation."""
def test_root_path_resolution(self, tmp_path):
"""Test that root paths are resolved to absolute paths."""
env_file = tmp_path / ".env"
root_dir = tmp_path / "allowed"
root_dir.mkdir()
env_file.write_text(f"ALLOWED_ROOTS={root_dir}\n")
config = Config.from_env(str(env_file))
# Should be resolved to absolute path
assert config.allowed_roots[0].is_absolute()
def test_multiple_root_paths(self, tmp_path):
"""Test loading multiple root paths."""
dir1 = tmp_path / "dir1"
dir2 = tmp_path / "dir2"
dir1.mkdir()
dir2.mkdir()
env_file = tmp_path / ".env"
env_file.write_text(f"ALLOWED_ROOTS={dir1},{dir2}\n")
config = Config.from_env(str(env_file))
assert len(config.allowed_roots) == 2
assert config.allowed_roots[0] == dir1.resolve()
assert config.allowed_roots[1] == dir2.resolve()
def test_allowed_urls_empty(self, tmp_path):
"""Test that empty ALLOWED_URLS means no URL restriction."""
env_file = tmp_path / ".env"
env_file.write_text("ALLOWED_URLS=\n")
config = Config.from_env(str(env_file))
assert config.allowed_urls == []
def test_allowed_urls_single(self, tmp_path):
"""Test loading single allowed URL."""
env_file = tmp_path / ".env"
env_file.write_text("ALLOWED_URLS=example.com\n")
config = Config.from_env(str(env_file))
assert config.allowed_urls == ["example.com"]
def test_allowed_urls_multiple(self, tmp_path):
"""Test loading multiple allowed URLs."""
env_file = tmp_path / ".env"
env_file.write_text("ALLOWED_URLS=example.com,*.test.org,localhost\n")
config = Config.from_env(str(env_file))
assert config.allowed_urls == ["example.com", "*.test.org", "localhost"]
def test_allowed_urls_with_whitespace(self, tmp_path):
"""Test that whitespace is trimmed from allowed URLs."""
env_file = tmp_path / ".env"
env_file.write_text("ALLOWED_URLS= example.com , test.org \n")
config = Config.from_env(str(env_file))
assert config.allowed_urls == ["example.com", "test.org"]
def test_brand_specific_default_paths(self):
"""Test brand-specific config and log directories."""
config_path = Config.get_default_config_path("agravity_bridge")
log_path = Config.get_default_log_path("agravity_bridge")
assert config_path.parts[-2:] == ("agravity_bridge", "config.json")
assert log_path.parts[-2:] == ("logs", "agravity_bridge.log")
class TestBootstrapEnvLoading:
"""Test bootstrap .env loading behavior for packaged builds."""
def test_load_bootstrap_env_reads_meipass_dotenv(self, tmp_path, monkeypatch):
"""Packaged app should load .env from PyInstaller runtime directory."""
meipass_dir = tmp_path / "runtime"
meipass_dir.mkdir(parents=True)
env_path = meipass_dir / ".env"
env_path.write_text("APP_NAME=Agravity Bridge\n", encoding="utf-8")
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "_MEIPASS", str(meipass_dir), raising=False)
loaded_path = Config.load_bootstrap_env()
assert loaded_path == env_path
assert os.getenv("APP_NAME") == "Agravity Bridge"