"""Integration tests for the complete update flow.""" import asyncio import json from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from webdrop_bridge.config import Config from webdrop_bridge.core.updater import Release, UpdateManager @pytest.fixture def config(tmp_path): """Create test config.""" return Config( app_name="Test WebDrop", app_version="0.0.1", log_level="INFO", log_file=None, allowed_roots=[tmp_path], allowed_urls=[], webapp_url="file:///./webapp/index.html", window_width=800, window_height=600, enable_logging=False, ) @pytest.fixture def mock_forgejo_response(): """Mock Forgejo API response - formatted as returned by _fetch_release.""" return { "tag_name": "v0.0.2", "name": "WebDropBridge v0.0.2", "version": "0.0.2", # _fetch_release adds this "body": "## Bug Fixes\n- Fixed drag and drop on macOS", "assets": [ { "name": "WebDropBridge.exe", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.0.2/WebDropBridge.exe", }, { "name": "WebDropBridge.exe.sha256", "browser_download_url": "https://git.him-tools.de/HIM-public/webdrop-bridge/releases/download/v0.0.2/WebDropBridge.exe.sha256", }, ], "published_at": "2026-01-29T10:00:00Z", } class TestUpdateFlowIntegration: """Integration tests for the complete update check flow.""" @pytest.mark.asyncio async def test_full_update_check_flow(self, config, mock_forgejo_response, tmp_path): """Test complete flow: API query -> version check -> signal.""" manager = UpdateManager( current_version=config.app_version, config_dir=tmp_path ) # Mock the API fetch with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.return_value = mock_forgejo_response # Run check release = await manager.check_for_updates() # Verify API was called mock_fetch.assert_called_once() # Verify we got a release assert release is not None assert release.version == "0.0.2" assert release.tag_name == "v0.0.2" assert len(release.assets) == 2 @pytest.mark.asyncio async def test_update_check_with_cache(self, config, mock_forgejo_response, tmp_path): """Test that cache is used on second call.""" manager = UpdateManager( current_version=config.app_version, config_dir=tmp_path ) with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.return_value = mock_forgejo_response # First call - should fetch from API release1 = await manager.check_for_updates() assert mock_fetch.call_count == 1 # Second call - should use cache release2 = await manager.check_for_updates() assert mock_fetch.call_count == 1 # Still 1, cache used # Verify both got same result assert release1 is not None assert release2 is not None assert release1.version == release2.version @pytest.mark.asyncio async def test_update_check_no_newer_version(self, config, tmp_path): """Test that no update available when latest is same version.""" manager = UpdateManager( current_version="0.0.2", config_dir=tmp_path ) response = { "tag_name": "v0.0.2", "name": "WebDropBridge v0.0.2", "body": "", "assets": [], "published_at": "2026-01-29T10:00:00Z", } with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.return_value = response release = await manager.check_for_updates() # Should return None since version is not newer assert release is None @pytest.mark.asyncio async def test_update_check_network_error(self, config, tmp_path): """Test graceful handling of network errors.""" manager = UpdateManager( current_version=config.app_version, config_dir=tmp_path ) # Mock network error with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.side_effect = Exception("Connection timeout") release = await manager.check_for_updates() # Should return None on error assert release is None @pytest.mark.asyncio async def test_version_parsing_in_api_response(self, config, tmp_path): """Test that version is correctly extracted from tag_name.""" manager = UpdateManager( current_version=config.app_version, config_dir=tmp_path ) # API returns version with 'v' prefix - but _fetch_release processes it response = { "tag_name": "v1.2.3", "name": "Release", "version": "1.2.3", # _fetch_release adds this "body": "", "assets": [], "published_at": "2026-01-29T10:00:00Z", } with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.return_value = response release = await manager.check_for_updates() # Version should be extracted correctly (without 'v') assert release is not None assert release.version == "1.2.3" @pytest.mark.asyncio async def test_asset_parsing_in_release(self, config, mock_forgejo_response, tmp_path): """Test that release assets are correctly parsed.""" manager = UpdateManager( current_version=config.app_version, config_dir=tmp_path ) with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.return_value = mock_forgejo_response release = await manager.check_for_updates() # Should have both exe and checksum assert release is not None assert len(release.assets) == 2 asset_names = [a["name"] for a in release.assets] assert "WebDropBridge.exe" in asset_names assert "WebDropBridge.exe.sha256" in asset_names @pytest.mark.asyncio async def test_changelog_preserved(self, config, mock_forgejo_response, tmp_path): """Test that release notes/changelog are preserved.""" manager = UpdateManager( current_version=config.app_version, config_dir=tmp_path ) with patch.object(manager, "_fetch_release") as mock_fetch: mock_fetch.return_value = mock_forgejo_response release = await manager.check_for_updates() # Changelog should be available assert release is not None assert release.body == mock_forgejo_response["body"] assert "Bug Fixes" in release.body