.json`` in
-``resources/translations/`` and optionally add an entry to
-:attr:`Translator.BUILTIN_LANGUAGES` for a nicer display name.
-"""
-
-import json
-import logging
-import sys
-from pathlib import Path
-from typing import Dict, Optional
-
-logger = logging.getLogger(__name__)
-
-
-class Translator:
- """Manages translations for the application.
-
- Loads translations from UTF-8 JSON files that use dot-notation string keys.
- Falls back to the English translation (and ultimately to the bare key) when
- a translation is missing.
-
- Attributes:
- BUILTIN_LANGUAGES: Mapping of language code → display name for languages
- that ship with the application. Add entries here when including new
- translation files.
- """
-
- #: Human-readable display names for supported language codes.
- #: Unknown codes fall back to their uppercase code string.
- BUILTIN_LANGUAGES: Dict[str, str] = {
- "en": "English",
- "de": "Deutsch",
- "fr": "Français",
- "it": "Italiano",
- "ru": "Русский",
- "zh": "中文",
- }
-
- def __init__(self) -> None:
- self._language: str = "en"
- self._translations: Dict[str, str] = {}
- self._fallback: Dict[str, str] = {}
- self._translations_dir: Optional[Path] = None
-
- def initialize(self, language: str, translations_dir: Path) -> None:
- """Initialize the translator with a language and translations directory.
-
- Args:
- language: Language code (e.g. ``"en"``, ``"de"``, ``"fr"``) or
- ``"auto"`` to detect from the system locale.
- translations_dir: Directory containing the ``.json`` translation files.
- """
- self._translations_dir = translations_dir
-
- # Resolve "auto" to system locale
- if language == "auto":
- language = self._detect_system_language()
- logger.debug(f"Auto-detected language: {language}")
-
- # Load English as fallback first
- en_path = translations_dir / "en.json"
- if en_path.exists():
- self._fallback = self._load_file(en_path)
- logger.debug(f"Loaded English fallback translations ({len(self._fallback)} keys)")
- else:
- logger.warning(f"English translation file not found at {en_path}")
-
- # Load requested language
- self._language = language
- if language != "en":
- lang_path = translations_dir / f"{language}.json"
- if lang_path.exists():
- self._translations = self._load_file(lang_path)
- logger.debug(f"Loaded '{language}' translations ({len(self._translations)} keys)")
- else:
- logger.warning(
- f"Translation file not found for language '{language}', "
- "falling back to English"
- )
- self._translations = {}
- else:
- self._translations = self._fallback
-
- def tr(self, key: str, **kwargs: str) -> str:
- """Get translated string for the given key.
-
- Args:
- key: Translation key using dot-notation (e.g. ``"toolbar.home"``).
- **kwargs: Named format arguments applied to the translated string.
-
- Returns:
- Translated and formatted string. Returns the *key* itself when no
- translation is found, so missing keys are always visible.
- """
- text = self._translations.get(key) or self._fallback.get(key) or key
- if kwargs:
- try:
- text = text.format(**kwargs)
- except (KeyError, ValueError) as e:
- logger.debug(f"Translation format error for key '{key}': {e}")
- return text
-
- def get_current_language(self) -> str:
- """Get the currently active language code (e.g. ``"de"``)."""
- return self._language
-
- def get_available_languages(self) -> Dict[str, str]:
- """Return available languages as ``{code: display_name}``.
-
- Discovers language files at runtime so newly added JSON files are
- automatically included without code changes.
-
- Returns:
- Ordered dict mapping language code → human-readable display name.
- """
- if self._translations_dir is None:
- return {"en": "English"}
-
- languages: Dict[str, str] = {}
- for lang_file in sorted(self._translations_dir.glob("*.json")):
- code = lang_file.stem
- name = self.BUILTIN_LANGUAGES.get(code, code.upper())
- languages[code] = name
- return languages
-
- # ------------------------------------------------------------------
- # Private helpers
- # ------------------------------------------------------------------
-
- def _load_file(self, path: Path) -> Dict[str, str]:
- """Load a JSON translation file.
-
- Args:
- path: Path to the UTF-8 encoded JSON translation file.
-
- Returns:
- Dictionary of translation keys to translated strings, or an empty
- dict when the file cannot be read or parsed.
- """
- try:
- with open(path, "r", encoding="utf-8") as f:
- return json.load(f)
- except (json.JSONDecodeError, IOError) as e:
- logger.error(f"Failed to load translation file {path}: {e}")
- return {}
-
- def _detect_system_language(self) -> str:
- """Detect system language from locale or platform settings.
-
- On Windows, attempts to read the UI language via the WinAPI before
- falling back to the ``locale`` module.
-
- Returns:
- Best-matching supported language code, or ``"en"`` as fallback.
- """
- import locale
-
- try:
- lang_code: Optional[str] = None
-
- if sys.platform.startswith("win"):
- # Windows: use GetUserDefaultUILanguage for accuracy
- try:
- import ctypes
-
- lcid = ctypes.windll.kernel32.GetUserDefaultUILanguage() # type: ignore[attr-defined]
- # Subset of LCID → ISO 639-1 mappings
- lcid_map: Dict[int, str] = {
- 0x0407: "de", # German (Germany)
- 0x0C07: "de", # German (Austria)
- 0x0807: "de", # German (Switzerland)
- 0x040C: "fr", # French (France)
- 0x080C: "fr", # French (Belgium)
- 0x0C0C: "fr", # French (Canada)
- 0x100C: "fr", # French (Switzerland)
- 0x0410: "it", # Italian (Italy)
- 0x0810: "it", # Italian (Switzerland)
- 0x0419: "ru", # Russian
- 0x0804: "zh", # Chinese Simplified
- 0x0404: "zh", # Chinese Traditional
- 0x0409: "en", # English (US)
- 0x0809: "en", # English (UK)
- }
- lang_code = lcid_map.get(lcid)
- except Exception:
- pass
-
- if not lang_code:
- raw = locale.getdefaultlocale()[0] or ""
- lang_code = raw.split("_")[0].lower() if raw else None
-
- if lang_code and lang_code in self.BUILTIN_LANGUAGES:
- return lang_code
-
- except Exception as e:
- logger.debug(f"Language auto-detection failed: {e}")
-
- return "en"
-
-
-# ---------------------------------------------------------------------------
-# Module-level singleton and public API
-# ---------------------------------------------------------------------------
-
-_translator = Translator()
-
-
-def _ensure_initialized() -> None:
- """Initialize translator lazily with default settings if needed."""
- if _translator._translations_dir is not None: # type: ignore[attr-defined]
- return
- _translator.initialize("en", get_translations_dir())
-
-
-def initialize(language: str, translations_dir: Path) -> None:
- """Initialize the global translator.
-
- Should be called **once at application startup**, before any UI is shown.
-
- Args:
- language: Language code (e.g. ``"de"``) or ``"auto"`` for system
- locale detection.
- translations_dir: Directory containing the ``.json`` translation files.
- """
- _translator.initialize(language, translations_dir)
-
-
-def tr(key: str, **kwargs: str) -> str:
- """Translate a string by key.
-
- Args:
- key: Translation key (e.g. ``"toolbar.home"``).
- **kwargs: Named format arguments (e.g. ``name="file.pdf"``).
-
- Returns:
- Translated string with any format substitutions applied.
- """
- _ensure_initialized()
- text = _translator.tr(key, **kwargs)
-
- # If lookup failed and translator points to a non-default directory (e.g. tests
- # overriding translator state), retry from default bundled translations.
- if text == key:
- default_dir = get_translations_dir()
- current_dir = _translator._translations_dir # type: ignore[attr-defined]
- if current_dir != default_dir:
- _translator.initialize("en", default_dir)
- text = _translator.tr(key, **kwargs)
-
- return text
-
-
-def get_current_language() -> str:
- """Return the currently active language code (e.g. ``"de"``)."""
- return _translator.get_current_language()
-
-
-def get_available_languages() -> Dict[str, str]:
- """Return all available languages as ``{code: display_name}``."""
- _ensure_initialized()
- return _translator.get_available_languages()
-
-
-def get_translations_dir() -> Path:
- """Resolve the translations directory for the current runtime context.
-
- Handles development mode, PyInstaller bundles, and MSI installations
- by searching the known candidate paths in order.
-
- Returns:
- Path to the ``resources/translations`` directory.
- """
- if hasattr(sys, "_MEIPASS"):
- # PyInstaller bundle
- return Path(sys._MEIPASS) / "resources" / "translations" # type: ignore[attr-defined]
- # Development mode or installed Python package
- return Path(__file__).parent.parent.parent.parent / "resources" / "translations"
diff --git a/tests/unit/test_brand_config.py b/tests/unit/test_brand_config.py
deleted file mode 100644
index 9016fef..0000000
--- a/tests/unit/test_brand_config.py
+++ /dev/null
@@ -1,147 +0,0 @@
-"""Tests for brand-aware build configuration helpers."""
-
-import json
-import sys
-from pathlib import Path
-
-BUILD_SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "build" / "scripts"
-if str(BUILD_SCRIPTS_DIR) not in sys.path:
- sys.path.insert(0, str(BUILD_SCRIPTS_DIR))
-
-from brand_config import (
- DEFAULT_BRAND_ID,
- collect_local_release_data,
- generate_release_manifest,
- load_brand_config,
- merge_release_manifests,
-)
-
-
-def test_load_agravity_brand_config():
- """Test loading the Agravity brand manifest."""
- brand = load_brand_config("agravity")
-
- assert brand.brand_id == "agravity"
- assert brand.display_name == "Agravity Bridge"
- assert brand.asset_prefix == "AgravityBridge"
- assert brand.exe_name == "AgravityBridge"
- assert brand.toolbar_icon_home == "resources/icons/home.ico"
- assert brand.toolbar_icon_reload == "resources/icons/reload.ico"
- assert brand.toolbar_icon_open == "resources/icons/open.ico"
- assert brand.toolbar_icon_openwith == "resources/icons/openwith.ico"
- assert brand.windows_installer_name("0.8.4") == "AgravityBridge-0.8.4-win-x64.msi"
-
-
-def test_generate_release_manifest_for_agravity(tmp_path):
- """Test generating a shared release manifest from local artifacts."""
- project_root = tmp_path
- (project_root / "build" / "brands").mkdir(parents=True)
- (project_root / "build" / "dist" / "windows" / "agravity").mkdir(parents=True)
- (project_root / "build" / "dist" / "macos" / "agravity").mkdir(parents=True)
-
- source_manifest = Path(__file__).resolve().parents[2] / "build" / "brands" / "agravity.json"
- (project_root / "build" / "brands" / "agravity.json").write_text(
- source_manifest.read_text(encoding="utf-8"),
- encoding="utf-8",
- )
-
- win_installer = (
- project_root
- / "build"
- / "dist"
- / "windows"
- / "agravity"
- / "AgravityBridge-0.8.4-win-x64.msi"
- )
- win_installer.write_bytes(b"msi")
- (win_installer.parent / f"{win_installer.name}.sha256").write_text("abc", encoding="utf-8")
-
- mac_installer = (
- project_root
- / "build"
- / "dist"
- / "macos"
- / "agravity"
- / "AgravityBridge-0.8.4-macos-universal.dmg"
- )
- mac_installer.write_bytes(b"dmg")
- (mac_installer.parent / f"{mac_installer.name}.sha256").write_text("def", encoding="utf-8")
-
- output_path = project_root / "build" / "dist" / "release-manifest.json"
- generate_release_manifest(
- "0.8.4",
- ["agravity"],
- output_path=output_path,
- root=project_root,
- )
-
- manifest = json.loads(output_path.read_text(encoding="utf-8"))
- assert manifest["version"] == "0.8.4"
- assert (
- manifest["brands"]["agravity"]["windows-x64"]["installer"]
- == "AgravityBridge-0.8.4-win-x64.msi"
- )
- assert (
- manifest["brands"]["agravity"]["macos-universal"]["installer"]
- == "AgravityBridge-0.8.4-macos-universal.dmg"
- )
-
-
-def test_collect_local_release_data_includes_default_brand(tmp_path):
- """Test discovering local artifacts for the default Windows build."""
- project_root = tmp_path
- installer_dir = project_root / "build" / "dist" / "windows" / DEFAULT_BRAND_ID
- installer_dir.mkdir(parents=True)
-
- installer = installer_dir / "WebDropBridge-0.8.4-win-x64.msi"
- installer.write_bytes(b"msi")
- checksum = installer_dir / f"{installer.name}.sha256"
- checksum.write_text("abc", encoding="utf-8")
-
- data = collect_local_release_data("0.8.4", platform="windows", root=project_root)
-
- assert data["brands"] == [DEFAULT_BRAND_ID]
- assert str(installer) in data["artifacts"]
- assert str(checksum) in data["artifacts"]
- assert (
- data["manifest"]["brands"][DEFAULT_BRAND_ID]["windows-x64"]["installer"] == installer.name
- )
-
-
-def test_merge_release_manifests_preserves_existing_platforms():
- """Test merging platform-specific manifest entries from separate upload runs."""
- base_manifest = {
- "version": "0.8.4",
- "channel": "stable",
- "brands": {
- "agravity": {
- "windows-x64": {
- "installer": "AgravityBridge-0.8.4-win-x64.msi",
- "checksum": "AgravityBridge-0.8.4-win-x64.msi.sha256",
- }
- }
- },
- }
- overlay_manifest = {
- "version": "0.8.4",
- "channel": "stable",
- "brands": {
- "agravity": {
- "macos-universal": {
- "installer": "AgravityBridge-0.8.4-macos-universal.dmg",
- "checksum": "AgravityBridge-0.8.4-macos-universal.dmg.sha256",
- }
- }
- },
- }
-
- merged = merge_release_manifests(base_manifest, overlay_manifest)
-
- assert (
- merged["brands"]["agravity"]["windows-x64"]["installer"]
- == "AgravityBridge-0.8.4-win-x64.msi"
- )
- assert (
- merged["brands"]["agravity"]["macos-universal"]["installer"]
- == "AgravityBridge-0.8.4-macos-universal.dmg"
- )
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
index 09e4d6d..fdeda3d 100644
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -1,7 +1,6 @@
"""Unit tests for configuration system."""
import os
-import sys
import pytest
@@ -13,26 +12,14 @@ 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",
- )
- ):
+ if key.startswith(('APP_', 'LOG_', 'ALLOWED_', 'WEBAPP_', 'WINDOW_', 'ENABLE_')):
del os.environ[key]
-
+
yield
-
+
# Restore env (cleanup)
os.environ.clear()
os.environ.update(saved_env)
@@ -77,28 +64,6 @@ class TestConfigFromEnv:
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
@@ -108,11 +73,8 @@ class TestConfigFromEnv:
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
@@ -225,30 +187,3 @@ class TestConfigValidation:
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"
diff --git a/tests/unit/test_i18n.py b/tests/unit/test_i18n.py
deleted file mode 100644
index b52da9d..0000000
--- a/tests/unit/test_i18n.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Unit tests for i18n translation helper."""
-
-import json
-from pathlib import Path
-
-from webdrop_bridge.utils import i18n
-
-
-class TestI18n:
- """Tests for translation lookup and fallback behavior."""
-
- def test_tr_lazy_initialization_uses_english_defaults(self):
- """Translator should lazily initialize and resolve known keys."""
- # Force a fresh singleton state for this test.
- i18n._translator = i18n.Translator() # type: ignore[attr-defined]
-
- assert i18n.tr("settings.title") == "Settings"
-
- def test_initialize_with_language_falls_back_to_english(self, tmp_path: Path):
- """Missing keys in selected language should fall back to English."""
- translations = tmp_path / "translations"
- translations.mkdir(parents=True, exist_ok=True)
-
- (translations / "en.json").write_text(
- json.dumps(
- {
- "greeting": "Hello {name}",
- "settings.title": "Settings",
- }
- ),
- encoding="utf-8",
- )
- (translations / "de.json").write_text(
- json.dumps(
- {
- "settings.title": "Einstellungen",
- }
- ),
- encoding="utf-8",
- )
-
- i18n._translator = i18n.Translator() # type: ignore[attr-defined]
- i18n.initialize("de", translations)
-
- assert i18n.tr("settings.title") == "Einstellungen"
- assert i18n.tr("greeting", name="Alex") == "Hello Alex"
-
- def test_get_available_languages_reads_translation_files(self, tmp_path: Path):
- """Available languages should be discovered from JSON files."""
- translations = tmp_path / "translations"
- translations.mkdir(parents=True, exist_ok=True)
- (translations / "en.json").write_text("{}", encoding="utf-8")
- (translations / "fr.json").write_text("{}", encoding="utf-8")
-
- i18n._translator = i18n.Translator() # type: ignore[attr-defined]
- i18n.initialize("en", translations)
-
- available = i18n.get_available_languages()
- assert "en" in available
- assert "fr" in available
diff --git a/tests/unit/test_settings_dialog.py b/tests/unit/test_settings_dialog.py
index 06d78d0..51fd02d 100644
--- a/tests/unit/test_settings_dialog.py
+++ b/tests/unit/test_settings_dialog.py
@@ -34,7 +34,7 @@ class TestSettingsDialogInitialization:
"""Test dialog can be created."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog is not None
assert dialog.windowTitle() == "Settings"
@@ -42,58 +42,51 @@ class TestSettingsDialogInitialization:
"""Test dialog has all required tabs."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.tabs is not None
- assert dialog.tabs.count() == 7 # General + previous 6 tabs
-
- def test_dialog_has_general_tab(self, qtbot, sample_config):
- """Test General tab exists."""
- dialog = SettingsDialog(sample_config)
- qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(0) == "General"
+ assert dialog.tabs.count() == 6 # Web Source, Paths, URLs, Logging, Window, Profiles
def test_dialog_has_web_source_tab(self, qtbot, sample_config):
"""Test Web Source tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(1) == "Web Source"
+
+ assert dialog.tabs.tabText(0) == "Web Source"
def test_dialog_has_paths_tab(self, qtbot, sample_config):
"""Test Paths tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(2) == "Paths"
+
+ assert dialog.tabs.tabText(1) == "Paths"
def test_dialog_has_urls_tab(self, qtbot, sample_config):
"""Test URLs tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(3) == "URLs"
+
+ assert dialog.tabs.tabText(2) == "URLs"
def test_dialog_has_logging_tab(self, qtbot, sample_config):
"""Test Logging tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(4) == "Logging"
+
+ assert dialog.tabs.tabText(3) == "Logging"
def test_dialog_has_window_tab(self, qtbot, sample_config):
"""Test Window tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(5) == "Window"
+
+ assert dialog.tabs.tabText(4) == "Window"
def test_dialog_has_profiles_tab(self, qtbot, sample_config):
"""Test Profiles tab exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
- assert dialog.tabs.tabText(6) == "Profiles"
+
+ assert dialog.tabs.tabText(5) == "Profiles"
class TestPathsTab:
@@ -103,7 +96,7 @@ class TestPathsTab:
"""Test paths are loaded from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
items = [dialog.paths_list.item(i).text() for i in range(dialog.paths_list.count())]
assert len(items) == 2
# Paths are normalized (backslashes on Windows)
@@ -114,7 +107,7 @@ class TestPathsTab:
"""Test Add Path button exists."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.tabs.currentWidget() is not None
@@ -125,7 +118,7 @@ class TestURLsTab:
"""Test URLs are loaded from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
items = [dialog.urls_list.item(i).text() for i in range(dialog.urls_list.count())]
assert len(items) == 2
assert "http://example.com" in items
@@ -139,14 +132,14 @@ class TestLoggingTab:
"""Test log level is set from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.log_level_combo.currentText() == "INFO"
def test_log_levels_available(self, qtbot, sample_config):
"""Test all log levels are available."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
levels = [dialog.log_level_combo.itemText(i) for i in range(dialog.log_level_combo.count())]
assert "DEBUG" in levels
assert "INFO" in levels
@@ -162,21 +155,21 @@ class TestWindowTab:
"""Test window width is set from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.width_spin.value() == 800
def test_window_height_set_from_config(self, qtbot, sample_config):
"""Test window height is set from configuration."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.height_spin.value() == 600
def test_window_width_has_min_max(self, qtbot, sample_config):
"""Test window width spinbox has min/max."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.width_spin.minimum() == 400
assert dialog.width_spin.maximum() == 5000
@@ -184,7 +177,7 @@ class TestWindowTab:
"""Test window height spinbox has min/max."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.height_spin.minimum() == 300
assert dialog.height_spin.maximum() == 5000
@@ -196,7 +189,7 @@ class TestProfilesTab:
"""Test profiles list is initialized."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
assert dialog.profiles_list is not None
@@ -207,9 +200,9 @@ class TestConfigDataRetrieval:
"""Test retrieving configuration data from dialog."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
config_data = dialog.get_config_data()
-
+
assert config_data["app_name"] == "WebDrop Bridge"
assert config_data["log_level"] == "INFO"
assert config_data["window_width"] == 800
@@ -219,7 +212,7 @@ class TestConfigDataRetrieval:
"""Test get_config_data returns valid configuration data."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
# All default values are valid
config_data = dialog.get_config_data()
assert config_data is not None
@@ -229,14 +222,14 @@ class TestConfigDataRetrieval:
"""Test get_config_data returns modified values."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
# Modify values
dialog.width_spin.setValue(1024)
dialog.height_spin.setValue(768)
dialog.log_level_combo.setCurrentText("DEBUG")
-
+
config_data = dialog.get_config_data()
-
+
assert config_data["window_width"] == 1024
assert config_data["window_height"] == 768
assert config_data["log_level"] == "DEBUG"
@@ -249,7 +242,7 @@ class TestApplyConfigData:
"""Test applying config data updates paths."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
new_config = {
"app_name": "Test",
"app_version": "1.0.0",
@@ -262,9 +255,9 @@ class TestApplyConfigData:
"window_height": 600,
"enable_logging": True,
}
-
+
dialog._apply_config_data(new_config)
-
+
items = [dialog.paths_list.item(i).text() for i in range(dialog.paths_list.count())]
assert "/new/path" in items
assert "/another/path" in items
@@ -273,7 +266,7 @@ class TestApplyConfigData:
"""Test applying config data updates URLs."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
new_config = {
"app_name": "Test",
"app_version": "1.0.0",
@@ -286,9 +279,9 @@ class TestApplyConfigData:
"window_height": 600,
"enable_logging": True,
}
-
+
dialog._apply_config_data(new_config)
-
+
items = [dialog.urls_list.item(i).text() for i in range(dialog.urls_list.count())]
assert "http://new.com" in items
assert "http://test.org" in items
@@ -297,7 +290,7 @@ class TestApplyConfigData:
"""Test applying config data updates window size."""
dialog = SettingsDialog(sample_config)
qtbot.addWidget(dialog)
-
+
new_config = {
"app_name": "Test",
"app_version": "1.0.0",
@@ -310,8 +303,8 @@ class TestApplyConfigData:
"window_height": 1024,
"enable_logging": True,
}
-
+
dialog._apply_config_data(new_config)
-
+
assert dialog.width_spin.value() == 1280
assert dialog.height_spin.value() == 1024
diff --git a/tests/unit/test_updater.py b/tests/unit/test_updater.py
index db57ebc..1685f20 100644
--- a/tests/unit/test_updater.py
+++ b/tests/unit/test_updater.py
@@ -16,17 +16,6 @@ def update_manager(tmp_path):
return UpdateManager(current_version="0.0.1", config_dir=tmp_path)
-@pytest.fixture
-def agravity_update_manager(tmp_path):
- """Create a brand-aware UpdateManager instance for Agravity Bridge."""
- return UpdateManager(
- current_version="0.0.1",
- config_dir=tmp_path,
- brand_id="agravity",
- update_channel="stable",
- )
-
-
@pytest.fixture
def sample_release():
"""Sample release data from API."""
@@ -263,143 +252,6 @@ class TestDownloading:
assert result is None
- @pytest.mark.asyncio
- async def test_download_update_uses_release_manifest(self, agravity_update_manager, tmp_path):
- """Test branded download selection from a shared release manifest."""
- release = Release(
- tag_name="v0.0.2",
- name="WebDropBridge v0.0.2",
- version="0.0.2",
- body="Release notes",
- assets=[
- {
- "name": "AgravityBridge-0.0.2-win-x64.msi",
- "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi",
- },
- {
- "name": "AgravityBridge-0.0.2-win-x64.msi.sha256",
- "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi.sha256",
- },
- {
- "name": "OtherBridge-0.0.2-win-x64.msi",
- "browser_download_url": "https://example.com/OtherBridge-0.0.2-win-x64.msi",
- },
- {
- "name": "release-manifest.json",
- "browser_download_url": "https://example.com/release-manifest.json",
- },
- ],
- published_at="2026-01-29T10:00:00Z",
- )
-
- manifest = {
- "version": "0.0.2",
- "channel": "stable",
- "brands": {
- "agravity": {
- "windows-x64": {
- "installer": "AgravityBridge-0.0.2-win-x64.msi",
- "checksum": "AgravityBridge-0.0.2-win-x64.msi.sha256",
- }
- }
- },
- }
-
- with (
- patch.object(UpdateManager, "_download_json_asset", return_value=manifest),
- patch.object(UpdateManager, "_download_file", return_value=True) as mock_download,
- ):
- result = await agravity_update_manager.download_update(release, tmp_path)
-
- assert result is not None
- assert result.name == "AgravityBridge-0.0.2-win-x64.msi"
- mock_download.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_download_update_falls_back_to_brand_prefix_without_manifest(
- self, agravity_update_manager, tmp_path
- ):
- """Test branded download selection still works when the manifest is unavailable."""
- release = Release(
- tag_name="v0.0.2",
- name="WebDropBridge v0.0.2",
- version="0.0.2",
- body="Release notes",
- assets=[
- {
- "name": "WebDropBridge-0.0.2-win-x64.msi",
- "browser_download_url": "https://example.com/WebDropBridge-0.0.2-win-x64.msi",
- },
- {
- "name": "AgravityBridge-0.0.2-win-x64.msi",
- "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi",
- },
- {
- "name": "AgravityBridge-0.0.2-win-x64.msi.sha256",
- "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi.sha256",
- },
- ],
- published_at="2026-01-29T10:00:00Z",
- )
-
- with patch.object(UpdateManager, "_download_file", return_value=True) as mock_download:
- result = await agravity_update_manager.download_update(release, tmp_path)
-
- assert result is not None
- assert result.name == "AgravityBridge-0.0.2-win-x64.msi"
- mock_download.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_verify_checksum_uses_release_manifest(self, agravity_update_manager, tmp_path):
- """Test branded checksum selection from a shared release manifest."""
- test_file = tmp_path / "AgravityBridge-0.0.2-win-x64.msi"
- test_file.write_bytes(b"test content")
-
- import hashlib
-
- checksum = hashlib.sha256(b"test content").hexdigest()
- release = Release(
- tag_name="v0.0.2",
- name="WebDropBridge v0.0.2",
- version="0.0.2",
- body="Release notes",
- assets=[
- {
- "name": "AgravityBridge-0.0.2-win-x64.msi",
- "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi",
- },
- {
- "name": "AgravityBridge-0.0.2-win-x64.msi.sha256",
- "browser_download_url": "https://example.com/AgravityBridge-0.0.2-win-x64.msi.sha256",
- },
- {
- "name": "release-manifest.json",
- "browser_download_url": "https://example.com/release-manifest.json",
- },
- ],
- published_at="2026-01-29T10:00:00Z",
- )
- manifest = {
- "version": "0.0.2",
- "channel": "stable",
- "brands": {
- "agravity": {
- "windows-x64": {
- "installer": "AgravityBridge-0.0.2-win-x64.msi",
- "checksum": "AgravityBridge-0.0.2-win-x64.msi.sha256",
- }
- }
- },
- }
-
- with (
- patch.object(UpdateManager, "_download_json_asset", return_value=manifest),
- patch.object(UpdateManager, "_download_checksum", return_value=checksum),
- ):
- result = await agravity_update_manager.verify_checksum(test_file, release)
-
- assert result is True
-
class TestChecksumVerification:
"""Test checksum verification."""