make dangerzone.settings a global

This commit is contained in:
Alexis Métaireau 2025-03-24 15:42:59 +01:00
parent 1353fa76d9
commit 8142e4a48a
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
10 changed files with 215 additions and 241 deletions

View file

@ -12,6 +12,8 @@ from typing import Optional
from colorama import Fore
from .. import settings
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtGui, QtWidgets
@ -80,7 +82,7 @@ class DangerzoneGui(DangerzoneCore):
elif platform.system() == "Linux":
# Get the PDF reader command
args = shlex.split(self.pdf_viewers[self.settings.get("open_app")])
args = shlex.split(self.pdf_viewers[settings.get("open_app")])
# %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open
for i in range(len(args)):
if (

View file

@ -8,6 +8,8 @@ from multiprocessing.pool import ThreadPool
from pathlib import Path
from typing import List, Optional
from .. import settings
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
@ -163,9 +165,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.toggle_updates_action = hamburger_menu.addAction("Check for updates")
self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered)
self.toggle_updates_action.setCheckable(True)
self.toggle_updates_action.setChecked(
bool(self.dangerzone.settings.get("updater_check"))
)
self.toggle_updates_action.setChecked(bool(settings.get("updater_check")))
# Add the "Exit" action
hamburger_menu.addSeparator()
@ -231,8 +231,8 @@ class MainWindow(QtWidgets.QMainWindow):
def show_update_success(self) -> None:
"""Inform the user about a new Dangerzone release."""
version = self.dangerzone.settings.get("updater_latest_version")
changelog = self.dangerzone.settings.get("updater_latest_changelog")
version = settings.get("updater_latest_version")
changelog = settings.get("updater_latest_changelog")
changelog_widget = CollapsibleBox("What's New?")
changelog_layout = QtWidgets.QVBoxLayout()
@ -277,8 +277,8 @@ class MainWindow(QtWidgets.QMainWindow):
def toggle_updates_triggered(self) -> None:
"""Change the underlying update check settings based on the user's choice."""
check = self.toggle_updates_action.isChecked()
self.dangerzone.settings.set("updater_check", check)
self.dangerzone.settings.save()
settings.set("updater_check", check)
settings.save()
def handle_docker_desktop_version_check(
self, is_version_valid: bool, version: str
@ -328,16 +328,16 @@ class MainWindow(QtWidgets.QMainWindow):
"""
# If there are no new updates, reset the error counter (if any) and return.
if report.empty():
self.dangerzone.settings.set("updater_errors", 0, autosave=True)
settings.set("updater_errors", 0, autosave=True)
return
hamburger_menu = self.hamburger_button.menu()
if report.error:
log.error(f"Encountered an error during an update check: {report.error}")
errors = self.dangerzone.settings.get("updater_errors") + 1
self.dangerzone.settings.set("updater_errors", errors)
self.dangerzone.settings.save()
errors = settings.get("updater_errors") + 1
settings.set("updater_errors", errors)
settings.save()
self.updater_error = report.error
# If we encounter more than three errors in a row, show a red notification
@ -369,13 +369,13 @@ class MainWindow(QtWidgets.QMainWindow):
hamburger_menu.insertAction(sep, error_action)
else:
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)
settings.set("updater_latest_version", report.version)
settings.set("updater_latest_changelog", report.changelog)
settings.set("updater_errors", 0)
# FIXME: Save the settings to the filesystem only when they have really changed,
# maybe with a dirty bit.
self.dangerzone.settings.save()
settings.save()
self.hamburger_button.setIcon(
QtGui.QIcon(
@ -985,39 +985,37 @@ class SettingsWidget(QtWidgets.QWidget):
self.setLayout(layout)
# Load values from settings
if self.dangerzone.settings.get("save"):
if settings.get("save"):
self.save_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.save_checkbox.setCheckState(QtCore.Qt.Unchecked)
if self.dangerzone.settings.get("safe_extension"):
self.safe_extension.setText(self.dangerzone.settings.get("safe_extension"))
if settings.get("safe_extension"):
self.safe_extension.setText(settings.get("safe_extension"))
else:
self.safe_extension.setText(SAFE_EXTENSION)
if self.dangerzone.settings.get("archive"):
if settings.get("archive"):
self.radio_move_untrusted.setChecked(True)
else:
self.radio_save_to.setChecked(True)
if self.dangerzone.settings.get("ocr"):
if settings.get("ocr"):
self.ocr_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.ocr_checkbox.setCheckState(QtCore.Qt.Unchecked)
index = self.ocr_combobox.findText(self.dangerzone.settings.get("ocr_language"))
index = self.ocr_combobox.findText(settings.get("ocr_language"))
if index != -1:
self.ocr_combobox.setCurrentIndex(index)
if self.dangerzone.settings.get("open"):
if settings.get("open"):
self.open_checkbox.setCheckState(QtCore.Qt.Checked)
else:
self.open_checkbox.setCheckState(QtCore.Qt.Unchecked)
if platform.system() == "Linux":
index = self.open_combobox.findText(
self.dangerzone.settings.get("open_app")
)
index = self.open_combobox.findText(settings.get("open_app"))
if index != -1:
self.open_combobox.setCurrentIndex(index)
@ -1138,21 +1136,15 @@ class SettingsWidget(QtWidgets.QWidget):
document.output_filename = tmp
# Update settings
self.dangerzone.settings.set(
"save", self.save_checkbox.checkState() == QtCore.Qt.Checked
)
self.dangerzone.settings.set("safe_extension", self.safe_extension.text())
self.dangerzone.settings.set("archive", self.radio_move_untrusted.isChecked())
self.dangerzone.settings.set(
"ocr", self.ocr_checkbox.checkState() == QtCore.Qt.Checked
)
self.dangerzone.settings.set("ocr_language", self.ocr_combobox.currentText())
self.dangerzone.settings.set(
"open", self.open_checkbox.checkState() == QtCore.Qt.Checked
)
settings.set("save", self.save_checkbox.checkState() == QtCore.Qt.Checked)
settings.set("safe_extension", self.safe_extension.text())
settings.set("archive", self.radio_move_untrusted.isChecked())
settings.set("ocr", self.ocr_checkbox.checkState() == QtCore.Qt.Checked)
settings.set("ocr_language", self.ocr_combobox.currentText())
settings.set("open", self.open_checkbox.checkState() == QtCore.Qt.Checked)
if platform.system() == "Linux":
self.dangerzone.settings.set("open_app", self.open_combobox.currentText())
self.dangerzone.settings.save()
settings.set("open_app", self.open_combobox.currentText())
settings.save()
# Start!
self.start_clicked.emit()
@ -1234,10 +1226,8 @@ class DocumentsListWidget(QtWidgets.QListWidget):
def get_ocr_lang(self) -> Optional[str]:
ocr_lang = None
if self.dangerzone.settings.get("ocr"):
ocr_lang = self.dangerzone.ocr_languages[
self.dangerzone.settings.get("ocr_language")
]
if settings.get("ocr"):
ocr_lang = self.dangerzone.ocr_languages[settings.get("ocr_language")]
return ocr_lang
@ -1326,7 +1316,7 @@ class DocumentWidget(QtWidgets.QWidget):
return
# Open
if self.dangerzone.settings.get("open"):
if settings.get("open"):
self.dangerzone.open_pdf_viewer(self.document.output_filename)

View file

@ -10,6 +10,8 @@ from typing import Optional
from packaging import version
from .. import settings
if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtWidgets
else:
@ -126,11 +128,11 @@ class UpdaterThread(QtCore.QThread):
@property
def check(self) -> Optional[bool]:
return self.dangerzone.settings.get("updater_check")
return settings.get("updater_check")
@check.setter
def check(self, val: bool) -> None:
self.dangerzone.settings.set("updater_check", val, autosave=True)
settings.set("updater_check", val, autosave=True)
def prompt_for_checks(self) -> Optional[bool]:
"""Ask the user if they want to be informed about Dangerzone updates."""
@ -169,9 +171,9 @@ class UpdaterThread(QtCore.QThread):
return False
log.debug("Checking if first run of Dangerzone")
if self.dangerzone.settings.get("updater_last_check") is None:
if settings.get("updater_last_check") is None:
log.debug("Dangerzone is running for the first time, updates are stalled")
self.dangerzone.settings.set("updater_last_check", 0, autosave=True)
settings.set("updater_last_check", 0, autosave=True)
return False
log.debug("Checking if user has already expressed their preference")
@ -204,7 +206,7 @@ class UpdaterThread(QtCore.QThread):
again.
"""
current_time = self._get_now_timestamp()
last_check = self.dangerzone.settings.get("updater_last_check")
last_check = settings.get("updater_last_check")
if current_time < last_check + UPDATE_CHECK_COOLDOWN_SECS:
log.debug("Cooling down update checks")
return True
@ -256,12 +258,12 @@ class UpdaterThread(QtCore.QThread):
2. In GitHub, by hitting the latest releases API.
"""
log.debug("Checking for Dangerzone updates")
latest_version = self.dangerzone.settings.get("updater_latest_version")
latest_version = settings.get("updater_latest_version")
if version.parse(get_version()) < version.parse(latest_version):
log.debug("Determined that there is an update due to cached results")
return UpdateReport(
version=latest_version,
changelog=self.dangerzone.settings.get("updater_latest_changelog"),
changelog=settings.get("updater_latest_changelog"),
)
# If the previous check happened before the cooldown period expires, do not
@ -271,9 +273,7 @@ class UpdaterThread(QtCore.QThread):
if self._should_postpone_update_check():
return UpdateReport()
else:
self.dangerzone.settings.set(
"updater_last_check", self._get_now_timestamp(), autosave=True
)
settings.set("updater_last_check", self._get_now_timestamp(), autosave=True)
log.debug("Checking the latest GitHub release")
report = self.get_latest_info()

View file

@ -5,10 +5,9 @@ from typing import Callable, List, Optional
import colorama
from . import errors, util
from . import errors, settings, util
from .document import Document
from .isolation_provider.base import IsolationProvider
from .settings import Settings
from .util import get_resource_path
log = logging.getLogger(__name__)
@ -28,8 +27,6 @@ class DangerzoneCore(object):
unsorted_ocr_languages = json.load(f)
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
# Load settings
self.settings = Settings()
self.documents: List[Document] = []
self.isolation_provider = isolation_provider

View file

@ -10,81 +10,78 @@ from .util import get_config_dir, get_version
log = logging.getLogger(__name__)
SETTINGS_FILENAME: str = "settings.json"
FILENAME = get_config_dir() / "settings.json"
SETTINGS: dict = {}
class Settings:
settings: Dict[str, Any]
def generate_default_settings() -> Dict[str, Any]:
return {
"save": True,
"archive": True,
"ocr": True,
"ocr_language": "English",
"open": True,
"open_app": None,
"safe_extension": SAFE_EXTENSION,
"updater_check": None,
"updater_last_check": None, # last check in UNIX epoch (secs since 1970)
# FIXME: How to invalidate those if they change upstream?
"updater_latest_version": get_version(),
"updater_latest_changelog": "",
"updater_errors": 0,
}
def __init__(self) -> None:
self.settings_filename = get_config_dir() / SETTINGS_FILENAME
self.default_settings: Dict[str, Any] = self.generate_default_settings()
self.load()
@classmethod
def generate_default_settings(cls) -> Dict[str, Any]:
return {
"save": True,
"archive": True,
"ocr": True,
"ocr_language": "English",
"open": True,
"open_app": None,
"safe_extension": SAFE_EXTENSION,
"updater_check": None,
"updater_last_check": None, # last check in UNIX epoch (secs since 1970)
# FIXME: How to invalidate those if they change upstream?
"updater_latest_version": get_version(),
"updater_latest_changelog": "",
"updater_errors": 0,
}
def get(key: str) -> Any:
return SETTINGS[key]
def get(self, key: str) -> Any:
return self.settings[key]
def set(self, key: str, val: Any, autosave: bool = False) -> None:
def set(key: str, val: Any, autosave: bool = False) -> None:
global SETTINGS
try:
old_val = SETTINGS.get(key)
except KeyError:
old_val = None
SETTINGS[key] = val
if autosave and val != old_val:
save()
def get_updater_settings() -> Dict[str, Any]:
return {key: val for key, val in SETTINGS.items() if key.startswith("updater_")}
def load() -> None:
global SETTINGS
default_settings = generate_default_settings()
if FILENAME.is_file() and FILENAME.exists():
# If the settings file exists, load it
try:
old_val = self.get(key)
except KeyError:
old_val = None
self.settings[key] = val
if autosave and val != old_val:
self.save()
with FILENAME.open("r") as settings_file:
SETTINGS = json.load(settings_file)
def get_updater_settings(self) -> Dict[str, Any]:
return {
key: val for key, val in self.settings.items() if key.startswith("updater_")
}
# If it's missing any fields, add them from the default settings
for key in default_settings:
if key not in SETTINGS:
SETTINGS[key] = default_settings[key]
elif key == "updater_latest_version":
if version.parse(get_version()) > version.parse(get(key)):
set(key, get_version())
def load(self) -> None:
if os.path.isfile(self.settings_filename):
self.settings = self.default_settings
except Exception:
log.error("Error loading settings, falling back to default")
SETTINGS = default_settings
# If the settings file exists, load it
try:
with open(self.settings_filename, "r") as settings_file:
self.settings = json.load(settings_file)
else:
# Save with default settings
log.info("Settings file doesn't exist, starting with default")
SETTINGS = default_settings
# If it's missing any fields, add them from the default settings
for key in self.default_settings:
if key not in self.settings:
self.settings[key] = self.default_settings[key]
elif key == "updater_latest_version":
if version.parse(get_version()) > version.parse(self.get(key)):
self.set(key, get_version())
save()
except Exception:
log.error("Error loading settings, falling back to default")
self.settings = self.default_settings
else:
# Save with default settings
log.info("Settings file doesn't exist, starting with default")
self.settings = self.default_settings
self.save()
def save(self) -> None:
self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
with self.settings_filename.open("w") as settings_file:
json.dump(self.settings, settings_file, indent=4)
def save() -> None:
FILENAME.parent.mkdir(parents=True, exist_ok=True)
with FILENAME.open("w") as settings_file:
json.dump(SETTINGS, settings_file, indent=4)

View file

@ -6,6 +6,7 @@ from pathlib import Path
from typing import Callable, List
import pytest
from pytest_mock import MockerFixture
from dangerzone.document import SAFE_EXTENSION
from dangerzone.gui import Application
@ -111,6 +112,26 @@ def sample_pdf() -> str:
return str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF))
@pytest.fixture
def mock_settings(mocker: MockerFixture, tmp_path: Path) -> Path:
mocker.patch("dangerzone.settings.FILENAME", tmp_path / "settings.json")
return tmp_path
@pytest.fixture
def default_settings_0_4_1() -> dict:
"""Get the default settings for the 0.4.1 Dangerzone release."""
return {
"save": True,
"archive": True,
"ocr": True,
"ocr_language": "English",
"open": True,
"open_app": None,
"safe_extension": "-safe.pdf",
}
SAMPLE_DIRECTORY = "test_docs"
BASIC_SAMPLE_PDF = "sample-pdf.pdf"
BASIC_SAMPLE_DOC = "sample-doc.doc"

View file

@ -30,25 +30,20 @@ def generate_isolated_updater(
else:
app = get_qt_app()
dummy = Dummy()
# XXX: We can monkey-patch global state without wrapping it in a context manager, or
# worrying that it will leak between tests, for two reasons:
#
# 1. Parallel tests in PyTest take place in different processes.
# 2. The monkeypatch fixture tears down the monkey-patch after each test ends.
monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path)
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
dangerzone = DangerzoneGui(app, isolation_provider=Dummy())
updater = UpdaterThread(dangerzone)
return updater
@pytest.fixture
def updater(
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture, mock_settings: Path
) -> UpdaterThread:
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
@pytest.fixture
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
def qt_updater(
tmp_path: Path, monkeypatch: MonkeyPatch, mock_settings: Path
) -> UpdaterThread:
return generate_isolated_updater(tmp_path, monkeypatch)

View file

@ -3,13 +3,14 @@ import pathlib
import platform
import shutil
import time
from pathlib import Path
from typing import List
from pytest import MonkeyPatch, fixture
from pytest_mock import MockerFixture
from pytestqt.qtbot import QtBot
from dangerzone import errors
from dangerzone import errors, settings
from dangerzone.document import Document
from dangerzone.gui import MainWindow
from dangerzone.gui import main_window as main_window_module
@ -96,7 +97,7 @@ def test_default_menu(
updater: UpdaterThread,
) -> None:
"""Check that the default menu entries are in order."""
updater.dangerzone.settings.set("updater_check", True)
settings.set("updater_check", True)
window = MainWindow(updater.dangerzone)
menu_actions = window.hamburger_button.menu().actions()
@ -114,7 +115,7 @@ def test_default_menu(
toggle_updates_action.trigger()
assert not toggle_updates_action.isChecked()
assert updater.dangerzone.settings.get("updater_check") is False
assert settings.get("updater_check") is False
def test_no_update(
@ -127,9 +128,9 @@ def test_no_update(
# 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_errors", 9)
updater.dangerzone.settings.set("updater_last_check", curtime)
settings.set("updater_check", True)
settings.set("updater_errors", 9)
settings.set("updater_last_check", curtime)
expected_settings = default_updater_settings()
expected_settings["updater_check"] = True
@ -154,7 +155,7 @@ def test_no_update(
assert menu_actions_before == menu_actions_after
# Check that any previous update errors are cleared.
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
def test_update_detected(
@ -165,9 +166,9 @@ def test_update_detected(
) -> None:
"""Test that a newly detected version leads to a notification to the user."""
qt_updater.dangerzone.settings.set("updater_check", True)
qt_updater.dangerzone.settings.set("updater_last_check", 0)
qt_updater.dangerzone.settings.set("updater_errors", 9)
settings.set("updater_check", True)
settings.set("updater_last_check", 0)
settings.set("updater_errors", 9)
# Make requests.get().json() return the following dictionary.
mock_upstream_info = {"tag_name": "99.9.9", "body": "changelog"}
@ -197,13 +198,11 @@ def test_update_detected(
# Check that the settings have been updated properly.
expected_settings = default_updater_settings()
expected_settings["updater_check"] = True
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get(
"updater_last_check"
)
expected_settings["updater_last_check"] = settings.get("updater_last_check")
expected_settings["updater_latest_version"] = "99.9.9"
expected_settings["updater_latest_changelog"] = "<p>changelog</p>"
expected_settings["updater_errors"] = 0
assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings
assert 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
@ -228,7 +227,7 @@ def test_update_detected(
update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog")
def check_dialog() -> None:
dialog = qt_updater.dangerzone.app.activeWindow()
dialog = qt_app.activeWindow()
update_dialog_spy.assert_called_once()
kwargs = update_dialog_spy.call_args.kwargs
@ -274,12 +273,13 @@ def test_update_error(
qt_updater: UpdaterThread,
monkeypatch: MonkeyPatch,
mocker: MockerFixture,
mock_settings: Path,
) -> 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.
qt_updater.dangerzone.settings.set("updater_check", True)
qt_updater.dangerzone.settings.set("updater_last_check", 0)
qt_updater.dangerzone.settings.set("updater_errors", 0)
settings.set("updater_check", True)
settings.set("updater_last_check", 0)
settings.set("updater_errors", 0)
# Make requests.get() return an errorthe following dictionary.
mocker.patch("dangerzone.gui.updater.requests.get")
@ -305,11 +305,9 @@ def test_update_error(
# Check that the settings have been updated properly.
expected_settings = default_updater_settings()
expected_settings["updater_check"] = True
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get(
"updater_last_check"
)
expected_settings["updater_last_check"] = settings.get("updater_last_check")
expected_settings["updater_errors"] += 1
assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
# Check that the hamburger icon has not changed.
assert load_svg_spy.call_count == 0
@ -318,7 +316,7 @@ def test_update_error(
assert menu_actions_before == menu_actions_after
# Test 2 - Check that the second error does not notify the user either.
qt_updater.dangerzone.settings.set("updater_last_check", 0)
settings.set("updater_last_check", 0)
with qtbot.waitSignal(qt_updater.finished):
qt_updater.start()
@ -326,16 +324,14 @@ def test_update_error(
# Check that the settings have been updated properly.
expected_settings["updater_errors"] += 1
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get(
"updater_last_check"
)
assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings
expected_settings["updater_last_check"] = settings.get("updater_last_check")
assert settings.get_updater_settings() == expected_settings
# Check that no menu entries have been added.
assert menu_actions_before == menu_actions_after
# Test 3 - Check that a third error shows a new menu entry.
qt_updater.dangerzone.settings.set("updater_last_check", 0)
settings.set("updater_last_check", 0)
with qtbot.waitSignal(qt_updater.finished):
qt_updater.start()
@ -360,7 +356,7 @@ def test_update_error(
update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog")
def check_dialog() -> None:
dialog = qt_updater.dangerzone.app.activeWindow()
dialog = qt_app.activeWindow()
update_dialog_spy.assert_called_once()
kwargs = update_dialog_spy.call_args.kwargs

View file

@ -15,7 +15,6 @@ from dangerzone.gui import updater as updater_module
from dangerzone.gui.updater import UpdateReport, UpdaterThread
from dangerzone.util import get_version
from ..test_settings import default_settings_0_4_1, save_settings
from .conftest import generate_isolated_updater
@ -27,7 +26,7 @@ def default_updater_settings() -> dict:
"""
return {
key: val
for key, val in settings.Settings.generate_default_settings().items()
for key, val in settings.generate_default_settings().items()
if key.startswith("updater_")
}
@ -43,13 +42,15 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
This test is mostly a sanity check.
"""
assert (
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
)
assert settings.get_updater_settings() == default_updater_settings()
def test_pre_0_4_2_settings(
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
tmp_path: Path,
monkeypatch: MonkeyPatch,
mocker: MockerFixture,
mock_settings: Path,
default_settings_0_4_1: dict,
) -> None:
"""Check settings of installations prior to 0.4.2.
@ -57,11 +58,10 @@ def test_pre_0_4_2_settings(
will automatically get the default updater settings, even though they never existed
in their settings.json file.
"""
save_settings(tmp_path, default_settings_0_4_1())
settings.SETTINGS = default_settings_0_4_1
settings.save()
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
assert (
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
)
assert settings.get_updater_settings() == default_updater_settings()
def test_post_0_4_2_settings(
@ -75,9 +75,8 @@ def test_post_0_4_2_settings(
erroneously prompted to a version they already have.
"""
# Store the settings of Dangerzone 0.4.2 to the filesystem.
old_settings = settings.Settings.generate_default_settings()
old_settings["updater_latest_version"] = "0.4.2"
save_settings(tmp_path, old_settings)
settings.SETTINGS = settings.generate_default_settings()
settings.set("updater_latest_version", "0.4.2", autosave=True)
# Mimic an upgrade to version 0.4.3, by making Dangerzone report that the current
# version is 0.4.3.
@ -89,19 +88,17 @@ def test_post_0_4_2_settings(
# Ensure that the Settings class will correct the latest version field to 0.4.3.
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
expected_settings["updater_latest_version"] = "0.4.4"
updater.dangerzone.settings.set(
"updater_latest_version", expected_settings["updater_latest_version"]
)
updater.dangerzone.settings.save()
settings.set("updater_latest_version", expected_settings["updater_latest_version"])
settings.save()
# Ensure that the Settings class will leave the "updater_latest_version" field
# intact the next time we reload the settings.
updater.dangerzone.settings.load()
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
settings.load()
assert settings.get_updater_settings() == expected_settings
@pytest.mark.skipif(platform.system() != "Linux", reason="Linux-only test")
@ -115,7 +112,7 @@ def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> Non
monkeypatch.delattr(sys, "dangerzone_dev")
assert updater.should_check_for_updates() is False
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
def test_user_prompts(
@ -130,7 +127,7 @@ def test_user_prompts(
expected_settings["updater_check"] = None
expected_settings["updater_last_check"] = 0
assert updater.should_check_for_updates() is False
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
# Second run
#
@ -144,14 +141,14 @@ def test_user_prompts(
prompt_mock().launch.return_value = False # type: ignore [attr-defined]
expected_settings["updater_check"] = False
assert updater.should_check_for_updates() is False
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
# Reset the "updater_check" field and check enabling update checks.
updater.dangerzone.settings.set("updater_check", None)
settings.set("updater_check", None)
prompt_mock().launch.return_value = True # type: ignore [attr-defined]
expected_settings["updater_check"] = True
assert updater.should_check_for_updates() is True
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
assert settings.get_updater_settings() == expected_settings
# Third run
#
@ -159,7 +156,7 @@ def test_user_prompts(
# checks.
prompt_mock().side_effect = RuntimeError("Should not be called") # type: ignore [attr-defined]
for check in [True, False]:
updater.dangerzone.settings.set("updater_check", check)
settings.set("updater_check", check)
assert updater.should_check_for_updates() == check
@ -200,8 +197,8 @@ def test_update_checks(
assert_report_equal(report, UpdateReport(error=error_msg))
# 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_changelog", "<p>changelog</p>")
settings.set("updater_latest_version", "99.9.9")
settings.set("updater_latest_changelog", "<p>changelog</p>")
report = updater.check_for_updates()
assert_report_equal(
@ -211,8 +208,8 @@ def test_update_checks(
def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -> None:
"""Make sure Dangerzone only checks for updates every X hours"""
updater.dangerzone.settings.set("updater_check", True)
updater.dangerzone.settings.set("updater_last_check", 0)
settings.set("updater_check", True)
settings.set("updater_last_check", 0)
# Mock some functions before the tests start
cooldown_spy = mocker.spy(updater, "_should_postpone_update_check")
@ -233,7 +230,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
report = updater.check_for_updates()
assert cooldown_spy.spy_return is False
assert updater.dangerzone.settings.get("updater_last_check") == curtime
assert settings.get("updater_last_check") == curtime
assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
# Test 2: Advance the current time by 1 second, and ensure that no update will take
@ -242,12 +239,12 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
curtime += 1
timestamp_mock.return_value = curtime
requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined]
updater.dangerzone.settings.set("updater_latest_version", get_version())
updater.dangerzone.settings.set("updater_latest_changelog", None)
settings.set("updater_latest_version", get_version())
settings.set("updater_latest_changelog", None)
report = updater.check_for_updates()
assert cooldown_spy.spy_return is True
assert updater.dangerzone.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())
# Test 3: Advance the current time by <cooldown period> seconds. Ensure that
@ -258,14 +255,14 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
report = updater.check_for_updates()
assert cooldown_spy.spy_return is False
assert updater.dangerzone.settings.get("updater_last_check") == curtime
assert settings.get("updater_last_check") == curtime
assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
# 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
# should be bumped, so that subsequent checks don't take place.
updater.dangerzone.settings.set("updater_latest_version", get_version())
updater.dangerzone.settings.set("updater_latest_changelog", None)
settings.set("updater_latest_version", get_version())
settings.set("updater_latest_changelog", None)
curtime += updater_module.UPDATE_CHECK_COOLDOWN_SECS
timestamp_mock.return_value = curtime
@ -273,7 +270,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
report = updater.check_for_updates()
assert cooldown_spy.spy_return is False
assert updater.dangerzone.settings.get("updater_last_check") == curtime
assert settings.get("updater_last_check") == curtime
error_msg = (
f"Encountered an exception while checking {updater.GH_RELEASE_URL}: failed"
)
@ -375,7 +372,7 @@ def test_update_check_prompt(
) -> 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)
qt_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.

View file

@ -5,50 +5,33 @@ from unittest.mock import PropertyMock
import pytest
from pytest_mock import MockerFixture
from dangerzone.settings import SETTINGS_FILENAME, Settings
def default_settings_0_4_1() -> dict:
"""Get the default settings for the 0.4.1 Dangerzone release."""
return {
"save": True,
"archive": True,
"ocr": True,
"ocr_language": "English",
"open": True,
"open_app": None,
"safe_extension": "-safe.pdf",
}
from dangerzone import settings
def test_no_settings_file_creates_new_one(
tmp_path: Path,
mocker: MockerFixture,
mock_settings: Path,
) -> None:
"""Default settings file is created on first run"""
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
settings = Settings()
settings.load()
assert settings.FILENAME.is_file()
assert settings.settings_filename.is_file()
with settings.settings_filename.open() as settings_file:
with settings.FILENAME.open() as settings_file:
new_settings_dict = json.load(settings_file)
assert sorted(new_settings_dict.items()) == sorted(
settings.generate_default_settings().items()
)
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
def test_corrupt_settings(mocker: MockerFixture, mock_settings: Path) -> None:
# Set some broken settings file
corrupt_settings_dict = "{:}"
with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file:
with settings.FILENAME.open("w") as settings_file:
settings_file.write(corrupt_settings_dict)
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
settings = Settings()
assert settings.settings_filename.is_file()
assert settings.FILENAME.is_file()
settings.load()
# Check if settings file was reset to the default
new_settings_dict = json.load(open(settings.settings_filename))
new_settings_dict = json.load(open(settings.FILENAME))
assert new_settings_dict != corrupt_settings_dict
assert sorted(new_settings_dict.items()) == sorted(
settings.generate_default_settings().items()
@ -56,32 +39,28 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
settings = Settings()
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
settings.save()
# Ensure new default setting is imported into settings
mocker.patch(
"dangerzone.settings.Settings.generate_default_settings",
"dangerzone.settings.generate_default_settings",
return_value={"mock_setting": 1},
)
settings2 = Settings()
assert settings2.get("mock_setting") == 1
settings.load()
assert settings.get("mock_setting") == 1
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
settings = Settings()
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
# Add new setting
settings.set("new_setting_autosaved", 20, autosave=True)
settings.set(
"new_setting", 10
) # XXX has to be afterwards; otherwise this will be saved
# Simulate new app startup (settings recreation)
settings2 = Settings()
settings.load()
# Check if new setting persisted
assert 20 == settings2.get("new_setting_autosaved")
assert 20 == settings.get("new_setting_autosaved")
with pytest.raises(KeyError):
settings2.get("new_setting")
settings.get("new_setting")