feat: Update DEVELOPMENT_PLAN with deliverables and acceptance criteria; add integration tests for update flow
This commit is contained in:
parent
50311139bf
commit
eb7ffe9969
2 changed files with 228 additions and 25 deletions
|
|
@ -784,20 +784,20 @@ AUTO_UPDATE_NOTIFY=true
|
||||||
- Security: HTTPS-only, checksum verification
|
- Security: HTTPS-only, checksum verification
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] `src/webdrop_bridge/core/updater.py` - Update manager
|
- [x] `src/webdrop_bridge/core/updater.py` - Update manager (COMPLETE)
|
||||||
- [ ] Menu item for manual update check
|
- [x] Unit tests for update checking and downloading (20 tests passing)
|
||||||
- [ ] Update notification dialog
|
- [x] Integration with Forgejo API (async queries working)
|
||||||
- [ ] Unit tests for update checking and downloading
|
- [ ] Menu item for manual update check (TODO: Priority 2)
|
||||||
- [ ] Integration with Forgejo API
|
- [ ] Update notification dialog (TODO: Priority 2)
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
- Can query Forgejo releases API
|
- [x] Can query Forgejo releases API
|
||||||
- Detects new versions correctly
|
- [x] Detects new versions correctly
|
||||||
- Downloads and verifies checksums
|
- [x] Downloads and verifies checksums
|
||||||
- Prompts user for restart
|
- [x] Gracefully handles network errors
|
||||||
- Manual check works from menu
|
- [x] Version comparison uses semantic versioning
|
||||||
- Gracefully handles network errors
|
- [ ] Manual check works from menu (TODO: Priority 2)
|
||||||
- Version comparison uses semantic versioning
|
- [ ] Prompts user for restart (TODO: Priority 2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -872,21 +872,21 @@ Help Menu
|
||||||
- Cancel-safe download handling
|
- Cancel-safe download handling
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] `src/webdrop_bridge/ui/update_manager_ui.py` - UI dialogs
|
- [x] `src/webdrop_bridge/ui/update_manager_ui.py` - UI dialogs (skeleton complete)
|
||||||
- [ ] Update menu item integration
|
- [x] Status bar update indicator (COMPLETE - emoji + status text)
|
||||||
- [ ] Status bar update indicator
|
- [ ] Update menu item integration (TODO: Priority 2)
|
||||||
- [ ] All dialogs with error handling
|
- [ ] All dialogs with signal hookups (TODO: Priority 2)
|
||||||
- [ ] Tests for UI interactions
|
- [ ] Tests for UI interactions (TODO: Priority 2)
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
- Menu item works and triggers check
|
- [x] Status bar updates in real-time (DONE)
|
||||||
- All dialogs display correctly
|
- [x] No blocking operations on main thread (async/await)
|
||||||
- Progress shown during download
|
- [x] Network errors handled gracefully (try/except with logging)
|
||||||
- Restart options work
|
- [ ] Menu item works and triggers check (TODO: Priority 2)
|
||||||
- Network errors handled gracefully
|
- [ ] All dialogs display correctly (TODO: Priority 2)
|
||||||
- Cancel operations work safely
|
- [ ] Progress shown during download (TODO: Priority 2)
|
||||||
- Status bar updates in real-time
|
- [ ] Restart options work (TODO: Priority 2)
|
||||||
- No blocking operations on main thread
|
- [ ] Cancel operations work safely (TODO: Priority 2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
203
tests/integration/test_update_flow.py
Normal file
203
tests/integration/test_update_flow.py
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
"""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.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.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 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.body == mock_forgejo_response["body"]
|
||||||
|
assert "Bug Fixes" in release.body
|
||||||
Loading…
Add table
Add a link
Reference in a new issue