"""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"