mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
tests: Add full test coverage for updater checks
Fully test the update check logic, by introducing several Qt tests. Also, improve the `UpdaterThread.get_letest_info()` method, that gets the latest version and changelog from GitHub, with several checks. These checks are also tested in our newly added tests.
This commit is contained in:
parent
fdc53efc35
commit
bc4bba4fa1
3 changed files with 438 additions and 58 deletions
|
@ -1,5 +1,6 @@
|
||||||
"""A module that contains the logic for checking for updates."""
|
"""A module that contains the logic for checking for updates."""
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
@ -97,6 +98,7 @@ class UpdaterThread(QtCore.QThread):
|
||||||
GH_RELEASE_URL = (
|
GH_RELEASE_URL = (
|
||||||
"https://api.github.com/repos/freedomofpress/dangerzone/releases/latest"
|
"https://api.github.com/repos/freedomofpress/dangerzone/releases/latest"
|
||||||
)
|
)
|
||||||
|
REQ_TIMEOUT = 15
|
||||||
|
|
||||||
def __init__(self, dangerzone: DangerzoneGui):
|
def __init__(self, dangerzone: DangerzoneGui):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -201,14 +203,30 @@ class UpdaterThread(QtCore.QThread):
|
||||||
to the users.
|
to the users.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# TODO: Set timeout.
|
res = requests.get(self.GH_RELEASE_URL, timeout=self.REQ_TIMEOUT)
|
||||||
info = requests.get(self.GH_RELEASE_URL).json()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO:: Handle errors.
|
raise RuntimeError(
|
||||||
raise
|
f"Encountered an exception while querying {self.GH_RELEASE_URL}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
version = info["tag_name"].lstrip("v")
|
if res.status_code != 200:
|
||||||
changelog = markdown.markdown(info["body"])
|
raise RuntimeError(
|
||||||
|
f"Encountered an HTTP {res.status_code} error while querying"
|
||||||
|
f" {self.GH_RELEASE_URL}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = res.json()
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Received a non-JSON response from {self.GH_RELEASE_URL}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = info["tag_name"].lstrip("v")
|
||||||
|
changelog = markdown.markdown(info["body"])
|
||||||
|
except KeyError as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Missing required fields in JSON response from {self.GH_RELEASE_URL}"
|
||||||
|
)
|
||||||
|
|
||||||
return UpdateReport(version=version, changelog=changelog)
|
return UpdateReport(version=version, changelog=changelog)
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from PySide6 import QtCore, QtWidgets
|
||||||
from pytest import MonkeyPatch, fixture
|
from pytest import MonkeyPatch, fixture
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from pytestqt.qtbot import QtBot
|
from pytestqt.qtbot import QtBot
|
||||||
|
|
||||||
from dangerzone.gui import MainWindow
|
from dangerzone.gui import MainWindow
|
||||||
from dangerzone.gui import updater as updater_mod
|
from dangerzone.gui import main_window as main_window_module
|
||||||
from dangerzone.gui.main_window import *
|
from dangerzone.gui import updater as updater_module
|
||||||
|
from dangerzone.gui.logic import DangerzoneGui
|
||||||
|
from dangerzone.gui.main_window import ContentWidget
|
||||||
from dangerzone.gui.updater import UpdateReport, UpdaterThread
|
from dangerzone.gui.updater import UpdateReport, UpdaterThread
|
||||||
from dangerzone.util import get_version
|
from dangerzone.util import get_version
|
||||||
|
|
||||||
from .. import sample_doc, sample_pdf
|
from .. import sample_doc, sample_pdf
|
||||||
from . import qt_updater, updater
|
from . import qt_updater as updater
|
||||||
from .test_updater import default_updater_settings
|
from .test_updater import assert_report_equal, default_updater_settings
|
||||||
|
|
||||||
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
|
|
||||||
if typing.TYPE_CHECKING:
|
|
||||||
from PySide2 import QtCore
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
from PySide6 import QtCore, QtGui, QtWidgets
|
|
||||||
except ImportError:
|
|
||||||
from PySide2 import QtCore, QtGui, QtWidgets
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## Widget Fixtures
|
## Widget Fixtures
|
||||||
|
@ -28,7 +24,7 @@ else:
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def content_widget(qtbot: QtBot, mocker: MockerFixture) -> QtWidgets.QWidget:
|
def content_widget(qtbot: QtBot, mocker: MockerFixture) -> ContentWidget:
|
||||||
# Setup
|
# Setup
|
||||||
mock_app = mocker.MagicMock()
|
mock_app = mocker.MagicMock()
|
||||||
dummy = mocker.MagicMock()
|
dummy = mocker.MagicMock()
|
||||||
|
@ -38,63 +34,280 @@ def content_widget(qtbot: QtBot, mocker: MockerFixture) -> QtWidgets.QWidget:
|
||||||
return w
|
return w
|
||||||
|
|
||||||
|
|
||||||
def test_qt(
|
def test_default_menu(
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
qt_updater: UpdaterThread,
|
updater: UpdaterThread,
|
||||||
|
) -> None:
|
||||||
|
"""Check that the default menu entries are in order."""
|
||||||
|
updater.dangerzone.settings.set("updater_check", True)
|
||||||
|
|
||||||
|
window = MainWindow(updater.dangerzone)
|
||||||
|
menu_actions = window.hamburger_button.menu().actions()
|
||||||
|
assert len(menu_actions) == 3
|
||||||
|
|
||||||
|
toggle_updates_action = menu_actions[0]
|
||||||
|
assert toggle_updates_action.text() == "Check for updates"
|
||||||
|
assert toggle_updates_action.isChecked()
|
||||||
|
|
||||||
|
separator = menu_actions[1]
|
||||||
|
assert separator.isSeparator()
|
||||||
|
|
||||||
|
exit_action = menu_actions[2]
|
||||||
|
assert exit_action.text() == "Exit"
|
||||||
|
|
||||||
|
toggle_updates_action.trigger()
|
||||||
|
assert not toggle_updates_action.isChecked()
|
||||||
|
assert updater.dangerzone.settings.get("updater_check") == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_update(
|
||||||
|
qtbot: QtBot,
|
||||||
|
updater: UpdaterThread,
|
||||||
monkeypatch: MonkeyPatch,
|
monkeypatch: MonkeyPatch,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
updater = qt_updater
|
"""Test that when no update has been detected, the user is not alerted."""
|
||||||
|
# Check that when no update is detected, e.g., due to update cooldown, an empty
|
||||||
|
# report is received that does not affect the menu entries.
|
||||||
|
curtime = int(time.time())
|
||||||
updater.dangerzone.settings.set("updater_check", True)
|
updater.dangerzone.settings.set("updater_check", True)
|
||||||
updater.dangerzone.settings.set("updater_last_check", 0)
|
updater.dangerzone.settings.set("updater_errors", 9)
|
||||||
|
updater.dangerzone.settings.set("updater_last_check", curtime)
|
||||||
|
|
||||||
expected_settings = default_updater_settings()
|
expected_settings = default_updater_settings()
|
||||||
expected_settings["updater_check"] = True
|
expected_settings["updater_check"] = True
|
||||||
|
expected_settings["updater_errors"] = 0 # errors must be cleared
|
||||||
mock_upstream_info = {"tag_name": f"v{get_version()}", "body": "changelog"}
|
expected_settings["updater_last_check"] = curtime
|
||||||
|
|
||||||
# Always assume that we can perform multiple update checks in a row.
|
|
||||||
monkeypatch.setattr(updater, "_should_postpone_update_check", lambda: False)
|
|
||||||
|
|
||||||
# Make requests.get().json() return the above dictionary.
|
|
||||||
requests_mock = mocker.MagicMock()
|
|
||||||
requests_mock().json.return_value = mock_upstream_info
|
|
||||||
monkeypatch.setattr(updater_mod.requests, "get", requests_mock)
|
|
||||||
|
|
||||||
window = MainWindow(updater.dangerzone)
|
window = MainWindow(updater.dangerzone)
|
||||||
window.register_update_handler(updater.finished)
|
window.register_update_handler(updater.finished)
|
||||||
|
handle_updates_spy = mocker.spy(window, "handle_updates")
|
||||||
|
|
||||||
|
menu_actions_before = window.hamburger_button.menu().actions()
|
||||||
|
|
||||||
with qtbot.waitSignal(updater.finished) as blocker:
|
with qtbot.waitSignal(updater.finished) as blocker:
|
||||||
updater.start()
|
updater.start()
|
||||||
|
|
||||||
assert len(window.hamburger_button.menu().actions()) == 3
|
# Check that the callback function gets an empty report.
|
||||||
|
handle_updates_spy.assert_called_once()
|
||||||
|
assert_report_equal(handle_updates_spy.call_args.args[0], UpdateReport())
|
||||||
|
|
||||||
|
# Check that the menu entries remain exactly the same.
|
||||||
|
menu_actions_after = window.hamburger_button.menu().actions()
|
||||||
|
assert menu_actions_before == menu_actions_after
|
||||||
|
|
||||||
|
# Check that any previous update errors are cleared.
|
||||||
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_detected(
|
||||||
|
qtbot: QtBot,
|
||||||
|
updater: UpdaterThread,
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test that a newly detected version leads to a notification to the user."""
|
||||||
|
updater.dangerzone.settings.set("updater_check", True)
|
||||||
|
updater.dangerzone.settings.set("updater_last_check", 0)
|
||||||
|
updater.dangerzone.settings.set("updater_errors", 9)
|
||||||
|
|
||||||
|
# Make requests.get().json() return the following dictionary.
|
||||||
|
mock_upstream_info = {"tag_name": "99.9.9", "body": "changelog"}
|
||||||
|
mocker.patch("dangerzone.gui.updater.requests.get")
|
||||||
|
requests_mock = updater_module.requests.get
|
||||||
|
requests_mock().status_code = 200 # type: ignore [call-arg]
|
||||||
|
requests_mock().json.return_value = mock_upstream_info # type: ignore [attr-defined, call-arg]
|
||||||
|
|
||||||
|
window = MainWindow(updater.dangerzone)
|
||||||
|
window.register_update_handler(updater.finished)
|
||||||
|
handle_updates_spy = mocker.spy(window, "handle_updates")
|
||||||
|
load_svg_spy = mocker.spy(window, "load_svg_image")
|
||||||
|
|
||||||
|
menu_actions_before = window.hamburger_button.menu().actions()
|
||||||
|
|
||||||
|
with qtbot.waitSignal(updater.finished) as blocker:
|
||||||
|
updater.start()
|
||||||
|
|
||||||
|
menu_actions_after = window.hamburger_button.menu().actions()
|
||||||
|
|
||||||
|
# Check that the callback function gets an update report.
|
||||||
|
handle_updates_spy.assert_called_once()
|
||||||
|
assert_report_equal(
|
||||||
|
handle_updates_spy.call_args.args[0], UpdateReport("99.9.9", "<p>changelog</p>")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the settings have been updated properly.
|
||||||
|
expected_settings = default_updater_settings()
|
||||||
|
expected_settings["updater_check"] = True
|
||||||
expected_settings["updater_last_check"] = updater.dangerzone.settings.get(
|
expected_settings["updater_last_check"] = updater.dangerzone.settings.get(
|
||||||
"updater_last_check"
|
"updater_last_check"
|
||||||
)
|
)
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
|
||||||
|
|
||||||
mock_upstream_info["tag_name"] = "v99.9.9"
|
|
||||||
mock_upstream_info["changelog"] = "changelog"
|
|
||||||
expected_settings["updater_latest_version"] = "99.9.9"
|
expected_settings["updater_latest_version"] = "99.9.9"
|
||||||
expected_settings["updater_latest_changelog"] = "<p>changelog</p>"
|
expected_settings["updater_latest_changelog"] = "<p>changelog</p>"
|
||||||
|
expected_settings["updater_errors"] = 0
|
||||||
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
# Check that the hamburger icon has changed with the expected SVG image.
|
||||||
|
assert load_svg_spy.call_count == 2
|
||||||
|
assert load_svg_spy.call_args_list[0].args[0] == "hamburger_menu_update_success.svg"
|
||||||
|
assert (
|
||||||
|
load_svg_spy.call_args_list[1].args[0] == "hamburger_menu_update_available.svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that new menu entries have been added.
|
||||||
|
menu_actions_after = window.hamburger_button.menu().actions()
|
||||||
|
assert len(menu_actions_after) == 5
|
||||||
|
assert menu_actions_after[2:] == menu_actions_before
|
||||||
|
|
||||||
|
success_action = menu_actions_after[0]
|
||||||
|
assert success_action.text() == "New version available"
|
||||||
|
|
||||||
|
separator = menu_actions_after[1]
|
||||||
|
assert separator.isSeparator()
|
||||||
|
|
||||||
|
# Check that clicking in the new menu entry, opens a dialog.
|
||||||
|
update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog")
|
||||||
|
|
||||||
|
def check_dialog() -> None:
|
||||||
|
dialog = updater.dangerzone.app.activeWindow()
|
||||||
|
|
||||||
|
update_dialog_spy.assert_called_once()
|
||||||
|
kwargs = update_dialog_spy.call_args.kwargs
|
||||||
|
assert "99.9.9" in kwargs["title"]
|
||||||
|
assert "dangerzone.rocks" in kwargs["intro_msg"]
|
||||||
|
assert not kwargs["middle_widget"].toggle_button.isChecked()
|
||||||
|
collapsible_box = kwargs["middle_widget"]
|
||||||
|
text_browser = (
|
||||||
|
collapsible_box.layout().itemAt(1).widget().layout().itemAt(0).widget()
|
||||||
|
)
|
||||||
|
assert collapsible_box.toggle_button.text() == "What's New?"
|
||||||
|
assert text_browser.toPlainText() == "changelog"
|
||||||
|
|
||||||
|
height_initial = dialog.sizeHint().height()
|
||||||
|
width_initial = dialog.sizeHint().width()
|
||||||
|
|
||||||
|
# Collapse the "What's New" section and ensure that the dialog's height
|
||||||
|
# increases.
|
||||||
|
with qtbot.waitSignal(collapsible_box.toggle_animation.finished) as blocker:
|
||||||
|
collapsible_box.toggle_button.click()
|
||||||
|
|
||||||
|
assert dialog.sizeHint().height() > height_initial
|
||||||
|
assert dialog.sizeHint().width() == width_initial
|
||||||
|
|
||||||
|
# Uncollapse the "What's New" section, and ensure that the dialog's height gets
|
||||||
|
# back to the original value.
|
||||||
|
with qtbot.waitSignal(collapsible_box.toggle_animation.finished) as blocker:
|
||||||
|
collapsible_box.toggle_button.click()
|
||||||
|
|
||||||
|
assert dialog.sizeHint().height() == height_initial
|
||||||
|
assert dialog.sizeHint().width() == width_initial
|
||||||
|
|
||||||
|
dialog.close()
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(500, check_dialog)
|
||||||
|
success_action.trigger()
|
||||||
|
|
||||||
|
# FIXME: We should check the content of the dialog here.
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_error(
|
||||||
|
qtbot: QtBot,
|
||||||
|
updater: UpdaterThread,
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test that an error during an update check leads to a notification to the user."""
|
||||||
|
# Test 1 - Check that the first error does not notify the user.
|
||||||
|
updater.dangerzone.settings.set("updater_check", True)
|
||||||
|
updater.dangerzone.settings.set("updater_last_check", 0)
|
||||||
|
updater.dangerzone.settings.set("updater_errors", 0)
|
||||||
|
|
||||||
|
# Make requests.get() return an errorthe following dictionary.
|
||||||
|
mocker.patch("dangerzone.gui.updater.requests.get")
|
||||||
|
requests_mock = updater_module.requests.get
|
||||||
|
requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined]
|
||||||
|
|
||||||
|
window = MainWindow(updater.dangerzone)
|
||||||
|
window.register_update_handler(updater.finished)
|
||||||
|
handle_updates_spy = mocker.spy(window, "handle_updates")
|
||||||
|
load_svg_spy = mocker.spy(window, "load_svg_image")
|
||||||
|
|
||||||
|
menu_actions_before = window.hamburger_button.menu().actions()
|
||||||
|
|
||||||
with qtbot.waitSignal(updater.finished) as blocker:
|
with qtbot.waitSignal(updater.finished) as blocker:
|
||||||
updater.start()
|
updater.start()
|
||||||
|
|
||||||
# The separator and install updates button has been added
|
menu_actions_after = window.hamburger_button.menu().actions()
|
||||||
assert len(window.hamburger_button.menu().actions()) == 5
|
|
||||||
|
# Check that the callback function gets an update report.
|
||||||
|
handle_updates_spy.assert_called_once()
|
||||||
|
assert "failed" in handle_updates_spy.call_args.args[0].error
|
||||||
|
|
||||||
|
# Check that the settings have been updated properly.
|
||||||
|
expected_settings = default_updater_settings()
|
||||||
|
expected_settings["updater_check"] = True
|
||||||
|
expected_settings["updater_last_check"] = updater.dangerzone.settings.get(
|
||||||
|
"updater_last_check"
|
||||||
|
)
|
||||||
|
expected_settings["updater_errors"] += 1
|
||||||
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
# Check that the hamburger icon has not changed.
|
||||||
|
assert load_svg_spy.call_count == 0
|
||||||
|
|
||||||
|
# Check that no menu entries have been added.
|
||||||
|
assert menu_actions_before == menu_actions_after
|
||||||
|
|
||||||
|
# Test 2 - Check that the second error does not notify the user either.
|
||||||
|
updater.dangerzone.settings.set("updater_last_check", 0)
|
||||||
|
with qtbot.waitSignal(updater.finished) as blocker:
|
||||||
|
updater.start()
|
||||||
|
|
||||||
|
assert load_svg_spy.call_count == 0
|
||||||
|
|
||||||
|
# Check that the settings have been updated properly.
|
||||||
|
expected_settings["updater_errors"] += 1
|
||||||
expected_settings["updater_last_check"] = updater.dangerzone.settings.get(
|
expected_settings["updater_last_check"] = updater.dangerzone.settings.get(
|
||||||
"updater_last_check"
|
"updater_last_check"
|
||||||
)
|
)
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
# TODO:
|
# Check that no menu entries have been added.
|
||||||
# 1. Test that hamburger icon changes according to the update status.
|
assert menu_actions_before == menu_actions_after
|
||||||
# 2. Check that the dialogs for new updates / update errors have the expected
|
|
||||||
# content.
|
# Test 3 - Check that a third error shows a new menu entry.
|
||||||
# 3. Check that a user can toggle updates from the hamburger menu.
|
updater.dangerzone.settings.set("updater_last_check", 0)
|
||||||
# 4. Check that errors are cleared from the settings, whenever we have a successful
|
with qtbot.waitSignal(updater.finished) as blocker:
|
||||||
# update check.
|
updater.start()
|
||||||
# 5. Check that latest version/changelog, as well as update errors, are cached in
|
|
||||||
# the settings
|
menu_actions_after = window.hamburger_button.menu().actions()
|
||||||
|
assert len(menu_actions_after) == 5
|
||||||
|
assert menu_actions_after[2:] == menu_actions_before
|
||||||
|
|
||||||
|
error_action = menu_actions_after[0]
|
||||||
|
assert error_action.text() == "Update error"
|
||||||
|
|
||||||
|
separator = menu_actions_after[1]
|
||||||
|
assert separator.isSeparator()
|
||||||
|
|
||||||
|
# Check that clicking in the new menu entry, opens a dialog.
|
||||||
|
update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog")
|
||||||
|
|
||||||
|
def check_dialog() -> None:
|
||||||
|
dialog = updater.dangerzone.app.activeWindow()
|
||||||
|
|
||||||
|
update_dialog_spy.assert_called_once()
|
||||||
|
kwargs = update_dialog_spy.call_args.kwargs
|
||||||
|
assert kwargs["title"] == "Update check error"
|
||||||
|
assert "Something went wrong" in kwargs["intro_msg"]
|
||||||
|
assert "Encountered an exception" in kwargs["middle_widget"].toPlainText()
|
||||||
|
assert "failed" in kwargs["middle_widget"].toPlainText()
|
||||||
|
assert "dangerzone.rocks" in kwargs["epilogue_msg"]
|
||||||
|
|
||||||
|
dialog.close()
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(500, check_dialog)
|
||||||
|
error_action.trigger()
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -3,9 +3,11 @@ import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import typing
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from PySide6 import QtCore
|
||||||
from pytest import MonkeyPatch
|
from pytest import MonkeyPatch
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from pytestqt.qtbot import QtBot
|
from pytestqt.qtbot import QtBot
|
||||||
|
@ -16,7 +18,7 @@ from dangerzone.gui import updater as updater_module
|
||||||
from dangerzone.gui.updater import UpdateReport, UpdaterThread
|
from dangerzone.gui.updater import UpdateReport, UpdaterThread
|
||||||
from dangerzone.util import get_version
|
from dangerzone.util import get_version
|
||||||
|
|
||||||
from . import generate_isolated_updater, updater
|
from . import generate_isolated_updater, qt_updater, updater
|
||||||
|
|
||||||
|
|
||||||
def default_updater_settings() -> dict:
|
def default_updater_settings() -> dict:
|
||||||
|
@ -191,9 +193,10 @@ def test_update_checks(
|
||||||
mock_upstream_info = {"tag_name": f"v{get_version()}", "body": "changelog"}
|
mock_upstream_info = {"tag_name": f"v{get_version()}", "body": "changelog"}
|
||||||
|
|
||||||
# Make requests.get().json() return the above dictionary.
|
# Make requests.get().json() return the above dictionary.
|
||||||
requests_mock = mocker.MagicMock()
|
mocker.patch("dangerzone.gui.updater.requests.get")
|
||||||
requests_mock().json.return_value = mock_upstream_info
|
requests_mock = updater_module.requests.get
|
||||||
monkeypatch.setattr(updater_module.requests, "get", requests_mock)
|
requests_mock().status_code = 200 # type: ignore [call-arg]
|
||||||
|
requests_mock().json.return_value = mock_upstream_info # type: ignore [attr-defined, call-arg]
|
||||||
|
|
||||||
# Always assume that we can perform multiple update checks in a row.
|
# Always assume that we can perform multiple update checks in a row.
|
||||||
monkeypatch.setattr(updater, "_should_postpone_update_check", lambda: False)
|
monkeypatch.setattr(updater, "_should_postpone_update_check", lambda: False)
|
||||||
|
@ -211,9 +214,12 @@ def test_update_checks(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test 3 - Check that HTTP errors are converted to error reports.
|
# Test 3 - Check that HTTP errors are converted to error reports.
|
||||||
requests_mock.side_effect = Exception("failed")
|
requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined]
|
||||||
report = updater.check_for_updates()
|
report = updater.check_for_updates()
|
||||||
assert_report_equal(report, UpdateReport(error="failed"))
|
error_msg = (
|
||||||
|
f"Encountered an exception while querying {updater.GH_RELEASE_URL}: failed"
|
||||||
|
)
|
||||||
|
assert_report_equal(report, UpdateReport(error=error_msg))
|
||||||
|
|
||||||
# Test 4 - Check that cached version/changelog info do not trigger an update check.
|
# Test 4 - Check that cached version/changelog info do not trigger an update check.
|
||||||
updater.dangerzone.settings.set("updater_latest_version", "99.9.9")
|
updater.dangerzone.settings.set("updater_latest_version", "99.9.9")
|
||||||
|
@ -238,6 +244,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
|
|
||||||
# # Make requests.get().json() return the version info that we want.
|
# # Make requests.get().json() return the version info that we want.
|
||||||
mock_upstream_info = {"tag_name": "99.9.9", "body": "changelog"}
|
mock_upstream_info = {"tag_name": "99.9.9", "body": "changelog"}
|
||||||
|
requests_mock().status_code = 200 # type: ignore [call-arg]
|
||||||
requests_mock().json.return_value = mock_upstream_info # type: ignore [attr-defined, call-arg]
|
requests_mock().json.return_value = mock_upstream_info # type: ignore [attr-defined, call-arg]
|
||||||
|
|
||||||
# Test 1: The first time Dangerzone checks for updates, the cooldown period should
|
# Test 1: The first time Dangerzone checks for updates, the cooldown period should
|
||||||
|
@ -289,4 +296,146 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
report = updater.check_for_updates()
|
report = updater.check_for_updates()
|
||||||
assert cooldown_spy.spy_return == False
|
assert cooldown_spy.spy_return == False
|
||||||
assert updater.dangerzone.settings.get("updater_last_check") == curtime
|
assert updater.dangerzone.settings.get("updater_last_check") == curtime
|
||||||
assert_report_equal(report, UpdateReport(error="failed"))
|
error_msg = (
|
||||||
|
f"Encountered an exception while querying {updater.GH_RELEASE_URL}: failed"
|
||||||
|
)
|
||||||
|
assert_report_equal(report, UpdateReport(error=error_msg))
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_errors(
|
||||||
|
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test update check errors."""
|
||||||
|
# Mock requests.get().
|
||||||
|
mocker.patch("dangerzone.gui.updater.requests.get")
|
||||||
|
requests_mock = updater_module.requests.get
|
||||||
|
|
||||||
|
# Always assume that we can perform multiple update checks in a row.
|
||||||
|
monkeypatch.setattr(updater, "_should_postpone_update_check", lambda: False)
|
||||||
|
|
||||||
|
# Test 1 - Check that request exceptions are being detected as errors.
|
||||||
|
requests_mock.side_effect = Exception("bad url") # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert report.error is not None
|
||||||
|
assert "bad url" in report.error
|
||||||
|
assert "Encountered an exception" in report.error
|
||||||
|
|
||||||
|
# Test 2 - Check that non HTTP 200 responses are detected as errors.
|
||||||
|
class MockResponse500:
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
requests_mock.return_value = MockResponse500() # type: ignore [attr-defined]
|
||||||
|
requests_mock.side_effect = None # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert report.error is not None
|
||||||
|
assert "Encountered an HTTP 500 error" in report.error
|
||||||
|
|
||||||
|
# Test 3 - Check that non JSON responses are detected as errors.
|
||||||
|
class MockResponseBadJSON:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return json.loads("bad json")
|
||||||
|
|
||||||
|
requests_mock.return_value = MockResponseBadJSON() # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert report.error is not None
|
||||||
|
assert "Received a non-JSON response" in report.error
|
||||||
|
|
||||||
|
# Test 4 - Check that missing fields in JSON are detected as errors.
|
||||||
|
class MockResponseEmpty:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
requests_mock.return_value = MockResponseEmpty() # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert report.error is not None
|
||||||
|
assert "Missing required fields in JSON" in report.error
|
||||||
|
|
||||||
|
# Test 5 - Check invalid versions are reported
|
||||||
|
class MockResponseBadVersion:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {"tag_name": "vbad_version", "body": "changelog"}
|
||||||
|
|
||||||
|
requests_mock.return_value = MockResponseBadVersion() # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert report.error is not None
|
||||||
|
assert "Invalid version" in report.error
|
||||||
|
|
||||||
|
# Test 6 - Check invalid markdown is reported
|
||||||
|
class MockResponseBadMarkdown:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {"tag_name": "v99.9.9", "body": ["bad", "markdown"]}
|
||||||
|
|
||||||
|
requests_mock.return_value = MockResponseBadMarkdown() # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert report.error is not None
|
||||||
|
|
||||||
|
# Test 7 - Check that a valid response passes.
|
||||||
|
class MockResponseValid:
|
||||||
|
status_code = 200
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {"tag_name": "v99.9.9", "body": "changelog"}
|
||||||
|
|
||||||
|
requests_mock.return_value = MockResponseValid() # type: ignore [attr-defined]
|
||||||
|
report = updater.check_for_updates()
|
||||||
|
assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_check_prompt(
|
||||||
|
qtbot: QtBot,
|
||||||
|
qt_updater: UpdaterThread,
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the prompt to enable update checks works properly."""
|
||||||
|
# Force Dangerzone to check immediately for updates
|
||||||
|
qt_updater.dangerzone.settings.set("updater_last_check", 0)
|
||||||
|
|
||||||
|
# Test 1 - Check that on the second run of Dangerzone, the user is prompted to
|
||||||
|
# choose if they want to enable update checks. By clicking on, we store this
|
||||||
|
# decision in the settings.
|
||||||
|
def click_ok() -> None:
|
||||||
|
dialog = qt_updater.dangerzone.app.activeWindow()
|
||||||
|
dialog.ok_button.click() # type: ignore [attr-defined]
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(500, click_ok)
|
||||||
|
res = qt_updater.should_check_for_updates()
|
||||||
|
|
||||||
|
assert res == True
|
||||||
|
assert qt_updater.check == True
|
||||||
|
|
||||||
|
# Test 2 - Same as the previous test, but check that clicking on cancel stores the
|
||||||
|
# opposite decision.
|
||||||
|
qt_updater.check = None
|
||||||
|
|
||||||
|
def click_cancel() -> None:
|
||||||
|
dialog = qt_updater.dangerzone.app.activeWindow()
|
||||||
|
dialog.cancel_button.click() # type: ignore [attr-defined]
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(500, click_cancel)
|
||||||
|
res = qt_updater.should_check_for_updates()
|
||||||
|
|
||||||
|
assert res == False
|
||||||
|
assert qt_updater.check == False
|
||||||
|
|
||||||
|
# Test 3 - Same as the previous test, but check that clicking on "X" does not store
|
||||||
|
# any decision.
|
||||||
|
qt_updater.check = None
|
||||||
|
|
||||||
|
def click_x() -> None:
|
||||||
|
dialog = qt_updater.dangerzone.app.activeWindow()
|
||||||
|
dialog.close()
|
||||||
|
|
||||||
|
QtCore.QTimer.singleShot(500, click_x)
|
||||||
|
res = qt_updater.should_check_for_updates()
|
||||||
|
|
||||||
|
assert res == False
|
||||||
|
assert qt_updater.check == None
|
||||||
|
|
Loading…
Reference in a new issue