mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-01 19:22:23 +02:00
Check for new container image releases when checking for updates
There is now a new setting that is updated when a container upgrade needs to be applied. The `UpdaterReport` has been extended to support this scenario, and is now a python `dataclass`.
This commit is contained in:
parent
fc1f91f32d
commit
27aa2b05a1
9 changed files with 230 additions and 81 deletions
|
@ -117,7 +117,7 @@ def cli_main(
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Ensure container is installed
|
# Ensure container is installed
|
||||||
should_upgrade = bool(settings.get("updater_check_all"))
|
should_upgrade = bool(settings.get("updater_container_needs_update"))
|
||||||
dangerzone.isolation_provider.install(should_upgrade)
|
dangerzone.isolation_provider.install(should_upgrade)
|
||||||
|
|
||||||
# Convert the document
|
# Convert the document
|
||||||
|
|
|
@ -29,7 +29,7 @@ else:
|
||||||
from .. import errors
|
from .. import errors
|
||||||
from ..document import SAFE_EXTENSION, Document
|
from ..document import SAFE_EXTENSION, Document
|
||||||
from ..isolation_provider.qubes import is_qubes_native_conversion
|
from ..isolation_provider.qubes import is_qubes_native_conversion
|
||||||
from ..updater.releases import UpdateReport
|
from ..updater.releases import UpdaterReport
|
||||||
from ..util import format_exception, get_resource_path, get_version
|
from ..util import format_exception, get_resource_path, get_version
|
||||||
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
|
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
|
||||||
|
|
||||||
|
@ -327,14 +327,14 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_updates(self, report: UpdateReport) -> None:
|
def handle_updates(self, report: UpdaterReport) -> None:
|
||||||
"""Handle update reports from the update checker thread.
|
"""Handle update reports from the update checker thread.
|
||||||
|
|
||||||
See Updater.check_for_updates() to find the different types of reports that it
|
See UpdaterReport to find the different types of reports that it
|
||||||
may send back, depending on the outcome of an update check.
|
may send back, depending on the outcome of an update check.
|
||||||
"""
|
"""
|
||||||
# If there are no new updates, reset the error counter (if any) and return.
|
# If there are no new updates, reset the error counter (if any) and return.
|
||||||
if report.empty():
|
if report.is_empty:
|
||||||
self.dangerzone.settings.set("updater_errors", 0, autosave=True)
|
self.dangerzone.settings.set("updater_errors", 0, autosave=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -376,14 +376,13 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
hamburger_menu.insertAction(sep, error_action)
|
hamburger_menu.insertAction(sep, error_action)
|
||||||
else:
|
else:
|
||||||
log.debug(f"Handling new version: {report.version}")
|
log.debug(f"Handling new version: {report.version}")
|
||||||
self.dangerzone.settings.set("updater_latest_version", report.version)
|
|
||||||
self.dangerzone.settings.set("updater_latest_changelog", report.changelog)
|
|
||||||
self.dangerzone.settings.set("updater_errors", 0)
|
self.dangerzone.settings.set("updater_errors", 0)
|
||||||
|
if report.new_github_release:
|
||||||
# FIXME: Save the settings to the filesystem only when they have really changed,
|
log.debug(f"New Dangerzone release: {report.version}")
|
||||||
# maybe with a dirty bit.
|
self.dangerzone.settings.set("updater_latest_version", report.version)
|
||||||
self.dangerzone.settings.save()
|
self.dangerzone.settings.set(
|
||||||
|
"updater_latest_changelog", report.changelog
|
||||||
|
)
|
||||||
self.hamburger_button.setIcon(
|
self.hamburger_button.setIcon(
|
||||||
QtGui.QIcon(
|
QtGui.QIcon(
|
||||||
load_svg_image(
|
load_svg_image(
|
||||||
|
@ -397,13 +396,26 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
success_action.setIcon(
|
success_action.setIcon(
|
||||||
QtGui.QIcon(
|
QtGui.QIcon(
|
||||||
load_svg_image(
|
load_svg_image(
|
||||||
"hamburger_menu_update_dot_available.svg", width=64, height=64
|
"hamburger_menu_update_dot_available.svg",
|
||||||
|
width=64,
|
||||||
|
height=64,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
success_action.triggered.connect(self.show_update_success)
|
success_action.triggered.connect(self.show_update_success)
|
||||||
hamburger_menu.insertAction(sep, success_action)
|
hamburger_menu.insertAction(sep, success_action)
|
||||||
|
|
||||||
|
if report.new_container_release:
|
||||||
|
log.debug(f"New container image release available")
|
||||||
|
self.dangerzone.settings.set(
|
||||||
|
"updater_container_needs_update",
|
||||||
|
report.container_needs_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME: Save the settings to the filesystem only when they have really changed,
|
||||||
|
# maybe with a dirty bit.
|
||||||
|
self.dangerzone.settings.save()
|
||||||
|
|
||||||
def register_update_handler(self, signal: QtCore.SignalInstance) -> None:
|
def register_update_handler(self, signal: QtCore.SignalInstance) -> None:
|
||||||
signal.connect(self.handle_updates)
|
signal.connect(self.handle_updates)
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class UpdaterThread(QtCore.QThread):
|
||||||
When finished, this thread triggers a signal with the results.
|
When finished, this thread triggers a signal with the results.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
finished = QtCore.Signal(releases.UpdateReport)
|
finished = QtCore.Signal(releases.UpdaterReport)
|
||||||
|
|
||||||
def __init__(self, dangerzone: DangerzoneGui):
|
def __init__(self, dangerzone: DangerzoneGui):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -99,7 +99,7 @@ class UpdaterThread(QtCore.QThread):
|
||||||
|
|
||||||
def should_check_for_updates(self) -> bool:
|
def should_check_for_updates(self) -> bool:
|
||||||
try:
|
try:
|
||||||
should_check: Optional[bool] = releases.should_check_for_releases(
|
should_check: Optional[bool] = releases.should_check_for_updates(
|
||||||
self.dangerzone.settings
|
self.dangerzone.settings
|
||||||
)
|
)
|
||||||
except errors.NeedUserInput:
|
except errors.NeedUserInput:
|
||||||
|
|
|
@ -9,6 +9,7 @@ from typing import Callable, List, Optional, Tuple
|
||||||
from .. import container_utils, errors
|
from .. import container_utils, errors
|
||||||
from ..container_utils import Runtime
|
from ..container_utils import Runtime
|
||||||
from ..document import Document
|
from ..document import Document
|
||||||
|
from ..settings import Settings
|
||||||
from ..updater import (
|
from ..updater import (
|
||||||
DEFAULT_PUBKEY_LOCATION,
|
DEFAULT_PUBKEY_LOCATION,
|
||||||
UpdaterError,
|
UpdaterError,
|
||||||
|
@ -135,6 +136,9 @@ class Container(IsolationProvider):
|
||||||
if update_available and image_digest:
|
if update_available and image_digest:
|
||||||
log.debug("Upgrading container image to %s", image_digest)
|
log.debug("Upgrading container image to %s", image_digest)
|
||||||
upgrade_container_image(image_digest, callback=callback)
|
upgrade_container_image(image_digest, callback=callback)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
settings.set("updater_container_needs_update", False, autosave=True)
|
||||||
else:
|
else:
|
||||||
log.debug("No update available for the container.")
|
log.debug("No update available for the container.")
|
||||||
if not installed_tags:
|
if not installed_tags:
|
||||||
|
|
28
dangerzone/isolation_provider/report.py
Normal file
28
dangerzone/isolation_provider/report.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Report:
|
||||||
|
gh_version: str | None = None
|
||||||
|
gh_changelog: str | None = None
|
||||||
|
container_needs_upgrade: bool = False
|
||||||
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def do_something_for_me() -> Report:
|
||||||
|
# I want to report the following:
|
||||||
|
# 1. There were an error
|
||||||
|
raise Exception("Something happened")
|
||||||
|
report = Report()
|
||||||
|
report.gh_version = something
|
||||||
|
report.gh_changelog = changelog
|
||||||
|
report.container_needs_upgrade = True
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
report = do_something_for_me()
|
||||||
|
except ReportException:
|
||||||
|
pass
|
|
@ -38,6 +38,7 @@ class Settings:
|
||||||
# FIXME: How to invalidate those if they change upstream?
|
# FIXME: How to invalidate those if they change upstream?
|
||||||
"updater_latest_version": get_version(),
|
"updater_latest_version": get_version(),
|
||||||
"updater_latest_changelog": "",
|
"updater_latest_changelog": "",
|
||||||
|
"updater_container_needs_update": False,
|
||||||
"updater_errors": 0,
|
"updater_errors": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,22 @@ import json
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
import requests
|
import requests
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
from .. import util
|
from .. import container_utils, util
|
||||||
from ..settings import Settings
|
from ..settings import Settings
|
||||||
from . import errors, log
|
from . import errors, log
|
||||||
|
from .signatures import (
|
||||||
|
DEFAULT_PUBKEY_LOCATION,
|
||||||
|
)
|
||||||
|
from .signatures import (
|
||||||
|
is_update_available as is_container_update_available,
|
||||||
|
)
|
||||||
|
|
||||||
# Check for updates at most every 12 hours.
|
# Check for updates at most every 12 hours.
|
||||||
UPDATE_CHECK_COOLDOWN_SECS = 60 * 60 * 12
|
UPDATE_CHECK_COOLDOWN_SECS = 60 * 60 * 12
|
||||||
|
@ -21,22 +28,31 @@ GH_RELEASE_URL = (
|
||||||
REQ_TIMEOUT = 15
|
REQ_TIMEOUT = 15
|
||||||
|
|
||||||
|
|
||||||
class UpdateReport:
|
@dataclass
|
||||||
|
class UpdaterReport:
|
||||||
"""A report for an update check."""
|
"""A report for an update check."""
|
||||||
|
|
||||||
def __init__(
|
version: Optional[str] = None
|
||||||
self,
|
changelog: Optional[str] = None
|
||||||
version: Optional[str] = None,
|
container_needs_update: Optional[bool] = None
|
||||||
changelog: Optional[str] = None,
|
error: Optional[str] = None
|
||||||
error: Optional[str] = None,
|
|
||||||
):
|
|
||||||
self.version = version
|
|
||||||
self.changelog = changelog
|
|
||||||
self.error = error
|
|
||||||
|
|
||||||
def empty(self) -> bool:
|
@property
|
||||||
|
def new_github_release(self) -> bool:
|
||||||
|
return self.version is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def new_container_release(self) -> bool:
|
||||||
|
return self.container_needs_update is True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_empty(self) -> bool:
|
||||||
return self.version is None and self.changelog is None and self.error is None
|
return self.version is None and self.changelog is None and self.error is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_error(self) -> bool:
|
||||||
|
return self.error is not None
|
||||||
|
|
||||||
|
|
||||||
def _get_now_timestamp() -> int:
|
def _get_now_timestamp() -> int:
|
||||||
return int(time.time())
|
return int(time.time())
|
||||||
|
@ -61,18 +77,20 @@ def ensure_sane_update(cur_version: str, latest_version: str) -> bool:
|
||||||
if version.parse(cur_version) == version.parse(latest_version):
|
if version.parse(cur_version) == version.parse(latest_version):
|
||||||
return False
|
return False
|
||||||
elif version.parse(cur_version) > version.parse(latest_version):
|
elif version.parse(cur_version) > version.parse(latest_version):
|
||||||
# FIXME: This is a sanity check, but we should improve its wording.
|
raise Exception(
|
||||||
raise Exception("Received version is older than the latest version")
|
"The version received from Github Releases is older than the latest known version"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def fetch_release_info() -> UpdateReport:
|
def fetch_github_release_info() -> Tuple[str, str]:
|
||||||
"""Get the latest release info from GitHub.
|
"""Get the latest release info from GitHub.
|
||||||
|
|
||||||
Also, render the changelog from Markdown format to HTML, so that we can show it
|
Also, render the changelog from Markdown format to HTML, so that we can show it
|
||||||
to the users.
|
to the users.
|
||||||
"""
|
"""
|
||||||
|
log.debug("Checking the latest GitHub release")
|
||||||
try:
|
try:
|
||||||
res = requests.get(GH_RELEASE_URL, timeout=REQ_TIMEOUT)
|
res = requests.get(GH_RELEASE_URL, timeout=REQ_TIMEOUT)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -99,10 +117,11 @@ def fetch_release_info() -> UpdateReport:
|
||||||
f"Missing required fields in JSON response from {GH_RELEASE_URL}"
|
f"Missing required fields in JSON response from {GH_RELEASE_URL}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return UpdateReport(version=version, changelog=changelog)
|
log.debug(f"Latest version in GitHub is {version}")
|
||||||
|
return version, changelog
|
||||||
|
|
||||||
|
|
||||||
def should_check_for_releases(settings: Settings) -> bool:
|
def should_check_for_updates(settings: Settings) -> bool:
|
||||||
"""Determine if we can check for release updates based on settings and user prefs.
|
"""Determine if we can check for release updates based on settings and user prefs.
|
||||||
|
|
||||||
Note that this method only checks if the user has expressed an interest for
|
Note that this method only checks if the user has expressed an interest for
|
||||||
|
@ -141,7 +160,7 @@ def should_check_for_releases(settings: Settings) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_for_updates(settings: Settings) -> UpdateReport:
|
def check_for_updates(settings: Settings) -> UpdaterReport:
|
||||||
"""Check for updates locally and remotely.
|
"""Check for updates locally and remotely.
|
||||||
|
|
||||||
Check for updates (locally and remotely) and return a report with the findings:
|
Check for updates (locally and remotely) and return a report with the findings:
|
||||||
|
@ -156,36 +175,56 @@ def check_for_updates(settings: Settings) -> UpdateReport:
|
||||||
message.
|
message.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
log.debug("Checking for Dangerzone updates")
|
log.debug("Checking for new DZ releases and container updates")
|
||||||
latest_version = settings.get("updater_latest_version")
|
latest_version = settings.get("updater_latest_version")
|
||||||
if version.parse(util.get_version()) < version.parse(latest_version):
|
new_gh_version = version.parse(util.get_version()) < version.parse(
|
||||||
log.debug("Determined that there is an update due to cached results")
|
latest_version
|
||||||
return UpdateReport(
|
|
||||||
version=latest_version,
|
|
||||||
changelog=settings.get("updater_latest_changelog"),
|
|
||||||
)
|
)
|
||||||
|
new_container_update = settings.get("updater_container_needs_update")
|
||||||
|
|
||||||
|
report = UpdaterReport()
|
||||||
|
|
||||||
|
if new_gh_version:
|
||||||
|
report.version = latest_version
|
||||||
|
report.changelog = settings.get("updater_latest_changelog")
|
||||||
|
|
||||||
|
if new_container_update:
|
||||||
|
log.debug("Determined that there is an update due to cached results")
|
||||||
|
report.container_needs_update = new_container_update
|
||||||
|
|
||||||
|
if not report.is_empty:
|
||||||
|
return report
|
||||||
|
|
||||||
# If the previous check happened before the cooldown period expires, do not
|
# If the previous check happened before the cooldown period expires, do not
|
||||||
# check again. Else, bump the last check timestamp, before making the actual
|
# check again. Else, bump the last check timestamp, before making the actual
|
||||||
# check. This is to ensure that even failed update checks respect the cooldown
|
# check. This is to ensure that even failed update checks respect the cooldown
|
||||||
# period.
|
# period.
|
||||||
if _should_postpone_update_check(settings):
|
if _should_postpone_update_check(settings):
|
||||||
return UpdateReport()
|
return UpdaterReport()
|
||||||
else:
|
else:
|
||||||
settings.set("updater_last_check", _get_now_timestamp(), autosave=True)
|
settings.set("updater_last_check", _get_now_timestamp(), autosave=True)
|
||||||
|
|
||||||
log.debug("Checking the latest GitHub release")
|
report = UpdaterReport()
|
||||||
report = fetch_release_info()
|
gh_version, gh_changelog = fetch_github_release_info()
|
||||||
log.debug(f"Latest version in GitHub is {report.version}")
|
if gh_version and ensure_sane_update(latest_version, gh_version):
|
||||||
if report.version and ensure_sane_update(latest_version, report.version):
|
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Determined that there is an update due to a new GitHub version:"
|
f"Determined that there is an update due to a new GitHub version:"
|
||||||
f" {latest_version} < {report.version}"
|
f" {latest_version} < {gh_version}"
|
||||||
|
)
|
||||||
|
report.version = gh_version
|
||||||
|
report.changelog = gh_changelog
|
||||||
|
|
||||||
|
container_name = container_utils.expected_image_name()
|
||||||
|
container_needs_update, _ = is_container_update_available(
|
||||||
|
container_name, DEFAULT_PUBKEY_LOCATION
|
||||||
|
)
|
||||||
|
report.container_needs_update = container_needs_update
|
||||||
|
|
||||||
|
settings.set(
|
||||||
|
"updater_container_needs_update", container_needs_update, autosave=True
|
||||||
)
|
)
|
||||||
return report
|
return report
|
||||||
|
|
||||||
log.debug("No need to update")
|
|
||||||
return UpdateReport()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("Encountered an error while checking for upgrades")
|
log.exception("Encountered an error while checking for upgrades")
|
||||||
return UpdateReport(error=str(e))
|
return UpdaterReport(error=str(e))
|
||||||
|
|
|
@ -118,24 +118,26 @@ def test_default_menu(
|
||||||
assert updater.dangerzone.settings.get("updater_check_all") is False
|
assert updater.dangerzone.settings.get("updater_check_all") is False
|
||||||
|
|
||||||
|
|
||||||
def test_no_update(
|
def test_no_new_release(
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
updater: UpdaterThread,
|
updater: UpdaterThread,
|
||||||
monkeypatch: MonkeyPatch,
|
monkeypatch: MonkeyPatch,
|
||||||
mocker: MockerFixture,
|
mocker: MockerFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that when no update has been detected, the user is not alerted."""
|
"""Test that when no new release has been detected, the user is not alerted."""
|
||||||
# Check that when no update is detected, e.g., due to update cooldown, an empty
|
# 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.
|
# report is received that does not affect the menu entries.
|
||||||
curtime = int(time.time())
|
curtime = int(time.time())
|
||||||
updater.dangerzone.settings.set("updater_check_all", True)
|
updater.dangerzone.settings.set("updater_check_all", True)
|
||||||
updater.dangerzone.settings.set("updater_errors", 9)
|
updater.dangerzone.settings.set("updater_errors", 9)
|
||||||
updater.dangerzone.settings.set("updater_last_check", curtime)
|
updater.dangerzone.settings.set("updater_last_check", curtime)
|
||||||
|
updater.dangerzone.settings.set("updater_container_needs_update", False)
|
||||||
|
|
||||||
expected_settings = default_updater_settings()
|
expected_settings = default_updater_settings()
|
||||||
expected_settings["updater_check_all"] = True
|
expected_settings["updater_check_all"] = True
|
||||||
expected_settings["updater_errors"] = 0 # errors must be cleared
|
expected_settings["updater_errors"] = 0 # errors must be cleared
|
||||||
expected_settings["updater_last_check"] = curtime
|
expected_settings["updater_last_check"] = curtime
|
||||||
|
expected_settings["updater_container_needs_update"] = False
|
||||||
|
|
||||||
window = MainWindow(updater.dangerzone)
|
window = MainWindow(updater.dangerzone)
|
||||||
window.register_update_handler(updater.finished)
|
window.register_update_handler(updater.finished)
|
||||||
|
@ -148,7 +150,7 @@ def test_no_update(
|
||||||
|
|
||||||
# Check that the callback function gets an empty report.
|
# Check that the callback function gets an empty report.
|
||||||
handle_updates_spy.assert_called_once()
|
handle_updates_spy.assert_called_once()
|
||||||
assert_report_equal(handle_updates_spy.call_args.args[0], releases.UpdateReport())
|
assert_report_equal(handle_updates_spy.call_args.args[0], releases.UpdaterReport())
|
||||||
|
|
||||||
# Check that the menu entries remain exactly the same.
|
# Check that the menu entries remain exactly the same.
|
||||||
menu_actions_after = window.hamburger_button.menu().actions()
|
menu_actions_after = window.hamburger_button.menu().actions()
|
||||||
|
@ -158,7 +160,50 @@ def test_no_update(
|
||||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
|
||||||
def test_update_detected(
|
def test_new_container_update(
|
||||||
|
qtbot: QtBot,
|
||||||
|
updater: UpdaterThread,
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test that when a new container image is available, the user isn't alerted"""
|
||||||
|
curtime = int(time.time())
|
||||||
|
updater.dangerzone.settings.set("updater_check_all", True)
|
||||||
|
updater.dangerzone.settings.set("updater_errors", 9)
|
||||||
|
updater.dangerzone.settings.set("updater_last_check", curtime)
|
||||||
|
updater.dangerzone.settings.set("updater_container_needs_update", True)
|
||||||
|
|
||||||
|
window = MainWindow(updater.dangerzone)
|
||||||
|
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):
|
||||||
|
updater.start()
|
||||||
|
|
||||||
|
# Check that the callback function gets a report with the container update
|
||||||
|
handle_updates_spy.assert_called_once()
|
||||||
|
assert_report_equal(
|
||||||
|
handle_updates_spy.call_args.args[0],
|
||||||
|
releases.UpdaterReport(container_needs_update=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
expected_settings = default_updater_settings()
|
||||||
|
expected_settings["updater_check_all"] = True
|
||||||
|
expected_settings["updater_errors"] = 0 # errors must be cleared
|
||||||
|
expected_settings["updater_last_check"] = curtime
|
||||||
|
expected_settings["updater_container_needs_update"] = True
|
||||||
|
|
||||||
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_release_is_detected(
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
qt_updater: UpdaterThread,
|
qt_updater: UpdaterThread,
|
||||||
monkeypatch: MonkeyPatch,
|
monkeypatch: MonkeyPatch,
|
||||||
|
@ -182,6 +227,11 @@ def test_update_detected(
|
||||||
handle_updates_spy = mocker.spy(window, "handle_updates")
|
handle_updates_spy = mocker.spy(window, "handle_updates")
|
||||||
load_svg_spy = mocker.spy(main_window_module, "load_svg_image")
|
load_svg_spy = mocker.spy(main_window_module, "load_svg_image")
|
||||||
|
|
||||||
|
# Mock the response of the container updater check
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.releases.is_container_update_available",
|
||||||
|
return_value=[False, None],
|
||||||
|
)
|
||||||
menu_actions_before = window.hamburger_button.menu().actions()
|
menu_actions_before = window.hamburger_button.menu().actions()
|
||||||
|
|
||||||
with qtbot.waitSignal(qt_updater.finished):
|
with qtbot.waitSignal(qt_updater.finished):
|
||||||
|
@ -193,7 +243,7 @@ def test_update_detected(
|
||||||
handle_updates_spy.assert_called_once()
|
handle_updates_spy.assert_called_once()
|
||||||
assert_report_equal(
|
assert_report_equal(
|
||||||
handle_updates_spy.call_args.args[0],
|
handle_updates_spy.call_args.args[0],
|
||||||
releases.UpdateReport("99.9.9", "<p>changelog</p>"),
|
releases.UpdaterReport("99.9.9", "<p>changelog</p>"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check that the settings have been updated properly.
|
# Check that the settings have been updated properly.
|
||||||
|
|
|
@ -14,7 +14,7 @@ from dangerzone import settings
|
||||||
from dangerzone.gui import updater as updater_module
|
from dangerzone.gui import updater as updater_module
|
||||||
from dangerzone.gui.updater import UpdaterThread
|
from dangerzone.gui.updater import UpdaterThread
|
||||||
from dangerzone.updater import releases
|
from dangerzone.updater import releases
|
||||||
from dangerzone.updater.releases import UpdateReport
|
from dangerzone.updater.releases import UpdaterReport
|
||||||
from dangerzone.util import get_version
|
from dangerzone.util import get_version
|
||||||
|
|
||||||
from ..test_settings import default_settings_0_4_1, save_settings
|
from ..test_settings import default_settings_0_4_1, save_settings
|
||||||
|
@ -34,7 +34,7 @@ def default_updater_settings() -> dict:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def assert_report_equal(report1: UpdateReport, report2: UpdateReport) -> None:
|
def assert_report_equal(report1: UpdaterReport, report2: UpdaterReport) -> None:
|
||||||
assert report1.version == report2.version
|
assert report1.version == report2.version
|
||||||
assert report1.changelog == report2.changelog
|
assert report1.changelog == report2.changelog
|
||||||
assert report1.error == report2.error
|
assert report1.error == report2.error
|
||||||
|
@ -174,6 +174,11 @@ def test_update_checks(
|
||||||
requests_mock().status_code = 200 # type: ignore [call-arg]
|
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]
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.releases.is_container_update_available",
|
||||||
|
return_value=[False, None],
|
||||||
|
)
|
||||||
|
|
||||||
# Always assume that we can perform multiple update checks in a row.
|
# Always assume that we can perform multiple update checks in a row.
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"dangerzone.updater.releases._should_postpone_update_check", return_value=False
|
"dangerzone.updater.releases._should_postpone_update_check", return_value=False
|
||||||
|
@ -181,21 +186,21 @@ def test_update_checks(
|
||||||
|
|
||||||
# Test 1 - Check that the current version triggers no updates.
|
# Test 1 - Check that the current version triggers no updates.
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert_report_equal(report, UpdateReport())
|
assert_report_equal(report, UpdaterReport())
|
||||||
|
|
||||||
# Test 2 - Check that a newer version triggers updates, and that the changelog is
|
# Test 2 - Check that a newer version triggers updates, and that the changelog is
|
||||||
# rendered from Markdown to HTML.
|
# rendered from Markdown to HTML.
|
||||||
mock_upstream_info["tag_name"] = "v99.9.9"
|
mock_upstream_info["tag_name"] = "v99.9.9"
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert_report_equal(
|
assert_report_equal(
|
||||||
report, UpdateReport(version="99.9.9", changelog="<p>changelog</p>")
|
report, UpdaterReport(version="99.9.9", changelog="<p>changelog</p>")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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") # type: ignore [attr-defined]
|
requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined]
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
error_msg = f"Encountered an exception while checking {updater_module.releases.GH_RELEASE_URL}: failed"
|
error_msg = f"Encountered an exception while checking {updater_module.releases.GH_RELEASE_URL}: failed"
|
||||||
assert_report_equal(report, UpdateReport(error=error_msg))
|
assert_report_equal(report, UpdaterReport(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.
|
||||||
settings.set("updater_latest_version", "99.9.9")
|
settings.set("updater_latest_version", "99.9.9")
|
||||||
|
@ -203,7 +208,7 @@ def test_update_checks(
|
||||||
|
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert_report_equal(
|
assert_report_equal(
|
||||||
report, UpdateReport(version="99.9.9", changelog="<p>changelog</p>")
|
report, UpdaterReport(version="99.9.9", changelog="<p>changelog</p>")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,6 +225,12 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
mocker.patch("dangerzone.updater.releases.requests.get")
|
mocker.patch("dangerzone.updater.releases.requests.get")
|
||||||
requests_mock = updater_module.releases.requests.get
|
requests_mock = updater_module.releases.requests.get
|
||||||
|
|
||||||
|
# Mock the response of the container updater check
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.releases.is_container_update_available",
|
||||||
|
return_value=[False, None],
|
||||||
|
)
|
||||||
|
|
||||||
# # 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().status_code = 200 # type: ignore [call-arg]
|
||||||
|
@ -234,7 +245,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert cooldown_spy.spy_return is False
|
assert cooldown_spy.spy_return is False
|
||||||
assert settings.get("updater_last_check") == curtime
|
assert settings.get("updater_last_check") == curtime
|
||||||
assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
|
assert_report_equal(report, UpdaterReport("99.9.9", "<p>changelog</p>"))
|
||||||
|
|
||||||
# Test 2: Advance the current time by 1 second, and ensure that no update will take
|
# Test 2: Advance the current time by 1 second, and ensure that no update will take
|
||||||
# place, due to the cooldown period. The last check timestamp should remain the
|
# place, due to the cooldown period. The last check timestamp should remain the
|
||||||
|
@ -248,7 +259,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert cooldown_spy.spy_return is True
|
assert cooldown_spy.spy_return is True
|
||||||
assert settings.get("updater_last_check") == curtime - 1 # type: ignore [unreachable]
|
assert settings.get("updater_last_check") == curtime - 1 # type: ignore [unreachable]
|
||||||
assert_report_equal(report, UpdateReport())
|
assert_report_equal(report, UpdaterReport())
|
||||||
|
|
||||||
# Test 3: Advance the current time by <cooldown period> seconds. Ensure that
|
# Test 3: Advance the current time by <cooldown period> seconds. Ensure that
|
||||||
# Dangerzone checks for updates again, and the last check timestamp gets bumped.
|
# Dangerzone checks for updates again, and the last check timestamp gets bumped.
|
||||||
|
@ -259,7 +270,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert cooldown_spy.spy_return is False
|
assert cooldown_spy.spy_return is False
|
||||||
assert settings.get("updater_last_check") == curtime
|
assert settings.get("updater_last_check") == curtime
|
||||||
assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
|
assert_report_equal(report, UpdaterReport("99.9.9", "<p>changelog</p>"))
|
||||||
|
|
||||||
# Test 4: Make Dangerzone check for updates again, but this time, it should
|
# Test 4: Make Dangerzone check for updates again, but this time, it should
|
||||||
# encounter an error while doing so. In that case, the last check timestamp
|
# encounter an error while doing so. In that case, the last check timestamp
|
||||||
|
@ -275,7 +286,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
|
||||||
assert cooldown_spy.spy_return is False
|
assert cooldown_spy.spy_return is False
|
||||||
assert settings.get("updater_last_check") == curtime
|
assert settings.get("updater_last_check") == curtime
|
||||||
error_msg = f"Encountered an exception while checking {updater_module.releases.GH_RELEASE_URL}: failed"
|
error_msg = f"Encountered an exception while checking {updater_module.releases.GH_RELEASE_URL}: failed"
|
||||||
assert_report_equal(report, UpdateReport(error=error_msg))
|
assert_report_equal(report, UpdaterReport(error=error_msg))
|
||||||
|
|
||||||
|
|
||||||
def test_update_errors(
|
def test_update_errors(
|
||||||
|
@ -285,6 +296,10 @@ def test_update_errors(
|
||||||
settings = updater.dangerzone.settings
|
settings = updater.dangerzone.settings
|
||||||
# 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(releases, "_should_postpone_update_check", lambda _: False)
|
monkeypatch.setattr(releases, "_should_postpone_update_check", lambda _: False)
|
||||||
|
mocker.patch(
|
||||||
|
"dangerzone.updater.releases.is_container_update_available",
|
||||||
|
return_value=[False, None],
|
||||||
|
)
|
||||||
|
|
||||||
# Mock requests.get().
|
# Mock requests.get().
|
||||||
mocker.patch("dangerzone.updater.releases.requests.get")
|
mocker.patch("dangerzone.updater.releases.requests.get")
|
||||||
|
@ -363,7 +378,7 @@ def test_update_errors(
|
||||||
|
|
||||||
requests_mock.return_value = MockResponseValid() # type: ignore [attr-defined]
|
requests_mock.return_value = MockResponseValid() # type: ignore [attr-defined]
|
||||||
report = releases.check_for_updates(settings)
|
report = releases.check_for_updates(settings)
|
||||||
assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
|
assert_report_equal(report, UpdaterReport("99.9.9", "<p>changelog</p>"))
|
||||||
|
|
||||||
|
|
||||||
def test_update_check_prompt(
|
def test_update_check_prompt(
|
||||||
|
|
Loading…
Reference in a new issue