- 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.
254 lines
8.9 KiB
Python
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"
|