bouquin/tests/test_version_check.py
Miguel Jacq 9435800910
Some checks failed
CI / test (push) Has been cancelled
Lint / test (push) Successful in 32s
Trivy / test (push) Successful in 25s
More tests
2025-11-26 17:12:58 +11:00

534 lines
18 KiB
Python

import pytest
from unittest.mock import Mock, patch
import subprocess
from bouquin.version_check import VersionChecker
from PySide6.QtWidgets import QMessageBox, QWidget
from PySide6.QtGui import QPixmap
def test_version_checker_init(app):
"""Test VersionChecker initialization."""
parent = QWidget()
checker = VersionChecker(parent)
assert checker._parent is parent
def test_version_checker_init_no_parent(app):
"""Test VersionChecker initialization without parent."""
checker = VersionChecker()
assert checker._parent is None
def test_current_version_returns_version(app):
"""Test getting current version."""
checker = VersionChecker()
with patch("importlib.metadata.version", return_value="1.2.3"):
version = checker.current_version()
assert version == "1.2.3"
def test_current_version_fallback_on_error(app):
"""Test current version fallback when package not found."""
checker = VersionChecker()
import importlib.metadata
with patch(
"importlib.metadata.version",
side_effect=importlib.metadata.PackageNotFoundError("Not found"),
):
version = checker.current_version()
assert version == "0.0.0"
def test_parse_version_simple(app):
"""Test parsing simple version string."""
result = VersionChecker._parse_version("1.2.3")
assert result == (1, 2, 3)
def test_parse_version_complex(app):
"""Test parsing complex version string with extra text."""
result = VersionChecker._parse_version("v1.2.3-beta")
assert result == (1, 2, 3)
def test_parse_version_no_numbers(app):
"""Test parsing version string with no numbers."""
result = VersionChecker._parse_version("invalid")
assert result == (0,)
def test_parse_version_single_number(app):
"""Test parsing version with single number."""
result = VersionChecker._parse_version("5")
assert result == (5,)
def test_is_newer_version_true(app):
"""Test detecting newer version."""
checker = VersionChecker()
assert checker._is_newer_version("1.2.3", "1.2.2") is True
assert checker._is_newer_version("2.0.0", "1.9.9") is True
assert checker._is_newer_version("1.3.0", "1.2.9") is True
def test_is_newer_version_false(app):
"""Test detecting same or older version."""
checker = VersionChecker()
assert checker._is_newer_version("1.2.3", "1.2.3") is False
assert checker._is_newer_version("1.2.2", "1.2.3") is False
assert checker._is_newer_version("0.9.9", "1.0.0") is False
def test_logo_pixmap(app):
"""Test generating logo pixmap."""
checker = VersionChecker()
pixmap = checker._logo_pixmap(96)
assert isinstance(pixmap, QPixmap)
assert not pixmap.isNull()
def test_logo_pixmap_different_sizes(app):
"""Test generating logo pixmap with different sizes."""
checker = VersionChecker()
pixmap_small = checker._logo_pixmap(48)
pixmap_large = checker._logo_pixmap(128)
assert not pixmap_small.isNull()
assert not pixmap_large.isNull()
def test_show_version_dialog(qtbot, app):
"""Test showing version dialog."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
with patch.object(QMessageBox, "exec") as mock_exec:
with patch("importlib.metadata.version", return_value="1.0.0"):
checker.show_version_dialog()
# Dialog should have been shown
assert mock_exec.called
def test_check_for_updates_network_error(qtbot, app):
"""Test check for updates when network request fails."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
with patch("requests.get", side_effect=Exception("Network error")):
with patch.object(QMessageBox, "warning") as mock_warning:
checker.check_for_updates()
# Should show warning
assert mock_warning.called
def test_check_for_updates_empty_response(qtbot, app):
"""Test check for updates with empty version string."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_response = Mock()
mock_response.text = " "
mock_response.raise_for_status = Mock()
with patch("requests.get", return_value=mock_response):
with patch.object(QMessageBox, "warning") as mock_warning:
checker.check_for_updates()
# Should show warning about empty version
assert mock_warning.called
def test_check_for_updates_already_latest(qtbot, app):
"""Test check for updates when already on latest version."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_response = Mock()
mock_response.text = "1.0.0"
mock_response.raise_for_status = Mock()
with patch("requests.get", return_value=mock_response):
with patch("importlib.metadata.version", return_value="1.0.0"):
with patch.object(QMessageBox, "information") as mock_info:
checker.check_for_updates()
# Should show info that we're on latest
assert mock_info.called
def test_check_for_updates_new_version_available_declined(qtbot, app):
"""Test check for updates when new version is available but user declines."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_response = Mock()
mock_response.text = "2.0.0"
mock_response.raise_for_status = Mock()
with patch("requests.get", return_value=mock_response):
with patch("importlib.metadata.version", return_value="1.0.0"):
with patch.object(QMessageBox, "question", return_value=QMessageBox.No):
# Should not proceed to download
checker.check_for_updates()
def test_check_for_updates_new_version_available_accepted(qtbot, app):
"""Test check for updates when new version is available and user accepts."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_response = Mock()
mock_response.text = "2.0.0"
mock_response.raise_for_status = Mock()
with patch("requests.get", return_value=mock_response):
with patch("importlib.metadata.version", return_value="1.0.0"):
with patch.object(QMessageBox, "question", return_value=QMessageBox.Yes):
with patch.object(
checker, "_download_and_verify_appimage"
) as mock_download:
checker.check_for_updates()
# Should call download
mock_download.assert_called_once_with("2.0.0")
def test_download_file_success(qtbot, app, tmp_path):
"""Test downloading a file successfully."""
checker = VersionChecker()
mock_response = Mock()
mock_response.headers = {"Content-Length": "1000"}
mock_response.iter_content = Mock(return_value=[b"data" * 25]) # 100 bytes
mock_response.raise_for_status = Mock()
dest_path = tmp_path / "test_file.bin"
with patch("requests.get", return_value=mock_response):
checker._download_file("http://example.com/file", dest_path)
assert dest_path.exists()
def test_download_file_with_progress(qtbot, app, tmp_path):
"""Test downloading a file with progress dialog."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_response = Mock()
mock_response.headers = {"Content-Length": "1000"}
mock_response.iter_content = Mock(return_value=[b"x" * 100, b"y" * 100])
mock_response.raise_for_status = Mock()
dest_path = tmp_path / "test_file.bin"
from PySide6.QtWidgets import QProgressDialog
mock_progress = Mock(spec=QProgressDialog)
mock_progress.wasCanceled = Mock(return_value=False)
mock_progress.value = Mock(return_value=0)
with patch("requests.get", return_value=mock_response):
checker._download_file(
"http://example.com/file", dest_path, progress=mock_progress
)
# Progress should have been updated
assert mock_progress.setValue.called
def test_download_file_cancelled(qtbot, app, tmp_path):
"""Test cancelling a file download."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_response = Mock()
mock_response.headers = {"Content-Length": "1000"}
mock_response.iter_content = Mock(return_value=[b"x" * 100])
mock_response.raise_for_status = Mock()
dest_path = tmp_path / "test_file.bin"
from PySide6.QtWidgets import QProgressDialog
mock_progress = Mock(spec=QProgressDialog)
mock_progress.wasCanceled = Mock(return_value=True)
mock_progress.value = Mock(return_value=0)
with patch("requests.get", return_value=mock_response):
with pytest.raises(RuntimeError):
checker._download_file(
"http://example.com/file", dest_path, progress=mock_progress
)
def test_download_file_no_content_length(qtbot, app, tmp_path):
"""Test downloading file without Content-Length header."""
checker = VersionChecker()
mock_response = Mock()
mock_response.headers = {}
mock_response.iter_content = Mock(return_value=[b"data"])
mock_response.raise_for_status = Mock()
dest_path = tmp_path / "test_file.bin"
with patch("requests.get", return_value=mock_response):
checker._download_file("http://example.com/file", dest_path)
assert dest_path.exists()
def test_download_and_verify_appimage_download_cancelled(qtbot, app, tmp_path):
"""Test AppImage download when user cancels."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
with patch(
"bouquin.version_check.QStandardPaths.writableLocation",
return_value=str(tmp_path),
):
with patch.object(
checker, "_download_file", side_effect=RuntimeError("Download cancelled")
):
with patch.object(QMessageBox, "information") as mock_info:
checker._download_and_verify_appimage("2.0.0")
# Should show cancellation message
assert mock_info.called
def test_download_and_verify_appimage_download_error(qtbot, app, tmp_path):
"""Test AppImage download when download fails."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
with patch(
"bouquin.version_check.QStandardPaths.writableLocation",
return_value=str(tmp_path),
):
with patch.object(
checker, "_download_file", side_effect=Exception("Network error")
):
with patch.object(QMessageBox, "critical") as mock_critical:
checker._download_and_verify_appimage("2.0.0")
# Should show error message
assert mock_critical.called
def test_download_and_verify_appimage_gpg_key_error(qtbot, app, tmp_path):
"""Test AppImage verification when GPG key cannot be read."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
with patch(
"bouquin.version_check.QStandardPaths.writableLocation",
return_value=str(tmp_path),
):
with patch.object(checker, "_download_file"):
with patch(
"importlib.resources.files", side_effect=Exception("Key not found")
):
with patch.object(QMessageBox, "critical") as mock_critical:
checker._download_and_verify_appimage("2.0.0")
# Should show error about GPG key
assert mock_critical.called
def test_download_and_verify_appimage_gpg_not_found(qtbot, app, tmp_path):
"""Test AppImage verification when GPG is not installed."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_files = Mock()
mock_files.read_bytes = Mock(return_value=b"fake key data")
with patch(
"bouquin.version_check.QStandardPaths.writableLocation",
return_value=str(tmp_path),
):
with patch.object(checker, "_download_file"):
with patch("importlib.resources.files", return_value=mock_files):
with patch(
"subprocess.run", side_effect=FileNotFoundError("gpg not found")
):
with patch.object(QMessageBox, "critical") as mock_critical:
checker._download_and_verify_appimage("2.0.0")
# Should show error about GPG not found
assert mock_critical.called
def test_download_and_verify_appimage_verification_failed(qtbot, app, tmp_path):
"""Test AppImage verification when signature verification fails."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_files = Mock()
mock_files.read_bytes = Mock(return_value=b"fake key data")
with patch(
"bouquin.version_check.QStandardPaths.writableLocation",
return_value=str(tmp_path),
):
with patch.object(checker, "_download_file"):
with patch("importlib.resources.files", return_value=mock_files):
# First subprocess call (import) succeeds, second (verify) fails
mock_error = subprocess.CalledProcessError(1, "gpg")
mock_error.stderr = b"Verification failed"
with patch("subprocess.run", side_effect=[None, mock_error]):
with patch.object(QMessageBox, "critical") as mock_critical:
checker._download_and_verify_appimage("2.0.0")
# Should show error about verification
assert mock_critical.called
def test_download_and_verify_appimage_success(qtbot, app, tmp_path):
"""Test successful AppImage download and verification."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_files = Mock()
mock_files.read_bytes = Mock(return_value=b"fake key data")
with patch(
"bouquin.version_check.QStandardPaths.writableLocation",
return_value=str(tmp_path),
):
with patch.object(checker, "_download_file"):
with patch("importlib.resources.files", return_value=mock_files):
with patch("subprocess.run"): # Both calls succeed
with patch.object(QMessageBox, "information") as mock_info:
checker._download_and_verify_appimage("2.0.0")
# Should show success message
assert mock_info.called
def test_version_comparison_edge_cases(app):
"""Test version comparison with edge cases."""
checker = VersionChecker()
# Different lengths
assert checker._is_newer_version("1.0.0.1", "1.0.0") is True
assert checker._is_newer_version("1.0", "1.0.0") is False
# Large numbers
assert checker._is_newer_version("10.0.0", "9.9.9") is True
assert checker._is_newer_version("1.100.0", "1.99.0") is True
def test_download_file_creates_parent_directory(qtbot, app, tmp_path):
"""Test that download creates parent directory if needed."""
checker = VersionChecker()
mock_response = Mock()
mock_response.headers = {}
mock_response.iter_content = Mock(return_value=[b"data"])
mock_response.raise_for_status = Mock()
dest_path = tmp_path / "subdir" / "nested" / "test_file.bin"
with patch("requests.get", return_value=mock_response):
checker._download_file("http://example.com/file", dest_path)
assert dest_path.exists()
assert dest_path.parent.exists()
def test_show_version_dialog_check_button_clicked(qtbot, app):
"""Test clicking 'Check for updates' button in version dialog."""
parent = QWidget()
qtbot.addWidget(parent)
checker = VersionChecker(parent)
mock_box = Mock(spec=QMessageBox)
check_button = Mock()
mock_box.clickedButton = Mock(return_value=check_button)
mock_box.addButton = Mock(return_value=check_button)
with patch("importlib.metadata.version", return_value="1.0.0"):
with patch("bouquin.version_check.QMessageBox", return_value=mock_box):
with patch.object(checker, "check_for_updates") as mock_check:
checker.show_version_dialog()
# check_for_updates should be called when button is clicked
if mock_box.clickedButton() is check_button:
assert mock_check.called
def test_parse_version_with_letters(app):
"""Test parsing version strings with letters."""
result = VersionChecker._parse_version("1.2.3rc1")
assert 1 in result
assert 2 in result
assert 3 in result
def test_download_file_invalid_content_length(qtbot, app, tmp_path):
"""Test downloading file with invalid Content-Length header."""
checker = VersionChecker()
mock_response = Mock()
mock_response.headers = {"Content-Length": "invalid"}
mock_response.iter_content = Mock(return_value=[b"data"])
mock_response.raise_for_status = Mock()
dest_path = tmp_path / "test_file.bin"
with patch("requests.get", return_value=mock_response):
# Should handle gracefully
checker._download_file("http://example.com/file", dest_path)
assert dest_path.exists()
def test_version_checker_creation(qtbot):
"""Test creating a VersionChecker instance."""
widget = QWidget()
qtbot.addWidget(widget)
checker = VersionChecker(widget)
assert checker is not None
def test_current_version(qtbot):
"""Test getting the current version."""
widget = QWidget()
qtbot.addWidget(widget)
checker = VersionChecker(widget)
version = checker.current_version()
# Version should be a string
assert isinstance(version, str)
assert len(version) > 0