convert to markdown
This commit is contained in:
parent
31604a0cd2
commit
6a9d2c4bcc
54 changed files with 1616 additions and 4012 deletions
|
|
@ -1,296 +1,180 @@
|
|||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox, QWidget
|
||||
|
||||
from bouquin.db import DBConfig
|
||||
import pytest
|
||||
from bouquin.settings_dialog import SettingsDialog
|
||||
from bouquin.theme import Theme
|
||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||||
|
||||
|
||||
class _ThemeSpy:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
@pytest.mark.gui
|
||||
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path):
|
||||
# Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...))
|
||||
app = QApplication.instance()
|
||||
parent = QWidget()
|
||||
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
|
||||
def set(self, t):
|
||||
self.calls.append(t)
|
||||
dlg.path_edit.setText(str(tmp_path / "alt.db"))
|
||||
dlg.idle_spin.setValue(3)
|
||||
dlg.theme_light.setChecked(True)
|
||||
dlg.move_todos.setChecked(True)
|
||||
|
||||
# Auto-accept the modal QMessageBox that _compact_btn_clicked() shows
|
||||
def _auto_accept_msgbox():
|
||||
for w in QApplication.topLevelWidgets():
|
||||
if isinstance(w, QMessageBox):
|
||||
w.accept()
|
||||
|
||||
QTimer.singleShot(0, _auto_accept_msgbox)
|
||||
dlg._compact_btn_clicked()
|
||||
qtbot.wait(50)
|
||||
|
||||
dlg._save()
|
||||
cfg = dlg.config
|
||||
assert cfg.path.name == "alt.db"
|
||||
assert cfg.idle_minutes == 3
|
||||
assert cfg.theme in ("light", "dark", "system")
|
||||
|
||||
|
||||
class _Parent(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.themes = _ThemeSpy()
|
||||
def test_save_key_toggle_roundtrip(qtbot, tmp_db_cfg, fresh_db, app):
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
from bouquin.key_prompt import KeyPrompt
|
||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
parent = QWidget()
|
||||
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
|
||||
dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
|
||||
# Ensure a clean starting state (suite may leave settings toggled on)
|
||||
dlg.save_key_btn.setChecked(False)
|
||||
dlg.key = ""
|
||||
|
||||
# Robust popup pump so we never miss late dialogs
|
||||
def _pump():
|
||||
for w in QApplication.topLevelWidgets():
|
||||
if isinstance(w, KeyPrompt):
|
||||
w.edit.setText("supersecret")
|
||||
w.accept()
|
||||
elif isinstance(w, QMessageBox):
|
||||
w.accept()
|
||||
|
||||
timer = QTimer()
|
||||
timer.setInterval(10)
|
||||
timer.timeout.connect(_pump)
|
||||
timer.start()
|
||||
try:
|
||||
dlg.save_key_btn.setChecked(True)
|
||||
qtbot.waitUntil(lambda: dlg.key == "supersecret", timeout=1000)
|
||||
assert dlg.save_key_btn.isChecked()
|
||||
|
||||
dlg.save_key_btn.setChecked(False)
|
||||
qtbot.waitUntil(lambda: dlg.key == "", timeout=1000)
|
||||
assert dlg.key == ""
|
||||
finally:
|
||||
timer.stop()
|
||||
|
||||
|
||||
class FakeDB:
|
||||
def __init__(self):
|
||||
self.rekey_called_with = None
|
||||
self.compact_called = False
|
||||
self.fail_compact = False
|
||||
def test_change_key_mismatch_shows_error(qtbot, tmp_db_cfg, tmp_path, app):
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||||
from bouquin.key_prompt import KeyPrompt
|
||||
from bouquin.db import DBManager, DBConfig
|
||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
||||
|
||||
def rekey(self, key: str):
|
||||
self.rekey_called_with = key
|
||||
cfg = DBConfig(
|
||||
path=tmp_path / "iso.db",
|
||||
key="oldkey",
|
||||
idle_minutes=0,
|
||||
theme="light",
|
||||
move_todos=True,
|
||||
)
|
||||
db = DBManager(cfg)
|
||||
assert db.connect()
|
||||
db.save_new_version("2000-01-01", "seed", "seed")
|
||||
|
||||
def compact(self):
|
||||
if self.fail_compact:
|
||||
raise RuntimeError("boom")
|
||||
self.compact_called = True
|
||||
|
||||
|
||||
class AcceptingPrompt:
|
||||
def __init__(self, parent=None, title="", message=""):
|
||||
self._key = ""
|
||||
self._accepted = True
|
||||
|
||||
def set_key(self, k: str):
|
||||
self._key = k
|
||||
return self
|
||||
|
||||
def exec(self):
|
||||
return QDialog.Accepted if self._accepted else QDialog.Rejected
|
||||
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
|
||||
class RejectingPrompt(AcceptingPrompt):
|
||||
def __init__(self, *a, **k):
|
||||
super().__init__()
|
||||
self._accepted = False
|
||||
|
||||
|
||||
def test_save_persists_all_fields(monkeypatch, qtbot, tmp_path):
|
||||
db = FakeDB()
|
||||
cfg = DBConfig(path=tmp_path / "db.sqlite", key="", idle_minutes=15)
|
||||
|
||||
saved = {}
|
||||
|
||||
def fake_save(cfg2):
|
||||
saved["cfg"] = cfg2
|
||||
|
||||
monkeypatch.setattr("bouquin.settings_dialog.save_db_config", fake_save)
|
||||
|
||||
# Drive the "remember key" checkbox via the prompt (no pre-set key)
|
||||
p = AcceptingPrompt().set_key("sekrit")
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p)
|
||||
|
||||
# Provide a lightweight parent that mimics MainWindow’s `themes` API
|
||||
class _ThemeSpy:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def set(self, theme):
|
||||
self.calls.append(theme)
|
||||
|
||||
class _Parent(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.themes = _ThemeSpy()
|
||||
|
||||
parent = _Parent()
|
||||
qtbot.addWidget(parent)
|
||||
parent = QWidget()
|
||||
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
dlg = SettingsDialog(cfg, db, parent=parent)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
# Change fields
|
||||
new_path = tmp_path / "new.sqlite"
|
||||
dlg.path_edit.setText(str(new_path))
|
||||
dlg.idle_spin.setValue(0)
|
||||
keys = ["one", "two"]
|
||||
|
||||
# User toggles "Remember key" -> stores prompted key
|
||||
dlg.save_key_btn.setChecked(True)
|
||||
def _pump_popups():
|
||||
for w in QApplication.topLevelWidgets():
|
||||
if isinstance(w, KeyPrompt):
|
||||
w.edit.setText(keys.pop(0) if keys else "zzz")
|
||||
w.accept()
|
||||
elif isinstance(w, QMessageBox):
|
||||
w.accept()
|
||||
|
||||
dlg._save()
|
||||
|
||||
out = saved["cfg"]
|
||||
assert out.path == new_path
|
||||
assert out.idle_minutes == 0
|
||||
assert out.key == "sekrit"
|
||||
assert parent.themes.calls and parent.themes.calls[-1] == Theme.SYSTEM
|
||||
timer = QTimer()
|
||||
timer.setInterval(10)
|
||||
timer.timeout.connect(_pump_popups)
|
||||
timer.start()
|
||||
try:
|
||||
dlg._change_key()
|
||||
finally:
|
||||
timer.stop()
|
||||
db.close()
|
||||
db2 = DBManager(cfg)
|
||||
assert db2.connect()
|
||||
db2.close()
|
||||
|
||||
|
||||
def test_save_key_checkbox_requires_key_and_reverts_if_cancelled(monkeypatch, qtbot):
|
||||
# When toggled on with no key yet, it prompts; cancelling should revert the check
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", RejectingPrompt)
|
||||
def test_change_key_success(qtbot, tmp_path, app):
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox
|
||||
from bouquin.key_prompt import KeyPrompt
|
||||
from bouquin.db import DBManager, DBConfig
|
||||
from bouquin.theme import ThemeManager, ThemeConfig, Theme
|
||||
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), FakeDB())
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
assert dlg.key == ""
|
||||
dlg.save_key_btn.click() # toggles True -> triggers prompt which rejects
|
||||
assert dlg.save_key_btn.isChecked() is False
|
||||
assert dlg.key == ""
|
||||
|
||||
|
||||
def test_save_key_checkbox_accepts_and_stores_key(monkeypatch, qtbot):
|
||||
# Toggling on with an accepting prompt should store the typed key
|
||||
p = AcceptingPrompt().set_key("remember-me")
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p)
|
||||
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), FakeDB())
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg.save_key_btn.click()
|
||||
assert dlg.save_key_btn.isChecked() is True
|
||||
assert dlg.key == "remember-me"
|
||||
|
||||
|
||||
def test_change_key_success(monkeypatch, qtbot):
|
||||
# Two prompts returning the same non-empty key -> rekey() and info message
|
||||
p1 = AcceptingPrompt().set_key("newkey")
|
||||
p2 = AcceptingPrompt().set_key("newkey")
|
||||
seq = [p1, p2]
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: seq.pop(0))
|
||||
|
||||
shown = {"info": 0}
|
||||
monkeypatch.setattr(
|
||||
QMessageBox,
|
||||
"information",
|
||||
lambda *a, **k: shown.__setitem__("info", shown["info"] + 1),
|
||||
cfg = DBConfig(
|
||||
path=tmp_path / "iso2.db",
|
||||
key="oldkey",
|
||||
idle_minutes=0,
|
||||
theme="light",
|
||||
move_todos=True,
|
||||
)
|
||||
db = DBManager(cfg)
|
||||
assert db.connect()
|
||||
db.save_new_version("2001-01-01", "seed", "seed")
|
||||
|
||||
db = FakeDB()
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
|
||||
parent = QWidget()
|
||||
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
|
||||
dlg = SettingsDialog(cfg, db, parent=parent)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg._change_key()
|
||||
keys = ["newkey", "newkey"]
|
||||
|
||||
assert db.rekey_called_with == "newkey"
|
||||
assert shown["info"] >= 1
|
||||
assert dlg.key == "newkey"
|
||||
def _pump():
|
||||
for w in QApplication.topLevelWidgets():
|
||||
if isinstance(w, KeyPrompt):
|
||||
w.edit.setText(keys.pop(0) if keys else "newkey")
|
||||
w.accept()
|
||||
elif isinstance(w, QMessageBox):
|
||||
w.accept()
|
||||
|
||||
timer = QTimer()
|
||||
timer.setInterval(10)
|
||||
timer.timeout.connect(_pump)
|
||||
timer.start()
|
||||
try:
|
||||
dlg._change_key()
|
||||
finally:
|
||||
timer.stop()
|
||||
qtbot.wait(50)
|
||||
|
||||
def test_change_key_mismatch_shows_warning_and_no_rekey(monkeypatch, qtbot):
|
||||
p1 = AcceptingPrompt().set_key("a")
|
||||
p2 = AcceptingPrompt().set_key("b")
|
||||
seq = [p1, p2]
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: seq.pop(0))
|
||||
|
||||
called = {"warn": 0}
|
||||
monkeypatch.setattr(
|
||||
QMessageBox,
|
||||
"warning",
|
||||
lambda *a, **k: called.__setitem__("warn", called["warn"] + 1),
|
||||
)
|
||||
|
||||
db = FakeDB()
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg._change_key()
|
||||
|
||||
assert db.rekey_called_with is None
|
||||
assert called["warn"] >= 1
|
||||
|
||||
|
||||
def test_change_key_empty_shows_warning(monkeypatch, qtbot):
|
||||
p1 = AcceptingPrompt().set_key("")
|
||||
p2 = AcceptingPrompt().set_key("")
|
||||
seq = [p1, p2]
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: seq.pop(0))
|
||||
|
||||
called = {"warn": 0}
|
||||
monkeypatch.setattr(
|
||||
QMessageBox,
|
||||
"warning",
|
||||
lambda *a, **k: called.__setitem__("warn", called["warn"] + 1),
|
||||
)
|
||||
|
||||
db = FakeDB()
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg._change_key()
|
||||
|
||||
assert db.rekey_called_with is None
|
||||
assert called["warn"] >= 1
|
||||
|
||||
|
||||
def test_browse_sets_path(monkeypatch, qtbot, tmp_path):
|
||||
def fake_get_save_file_name(*a, **k):
|
||||
return (str(tmp_path / "picked.sqlite"), "")
|
||||
|
||||
monkeypatch.setattr(
|
||||
QFileDialog, "getSaveFileName", staticmethod(fake_get_save_file_name)
|
||||
)
|
||||
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), FakeDB())
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg._browse()
|
||||
assert dlg.path_edit.text().endswith("picked.sqlite")
|
||||
|
||||
|
||||
def test_compact_success_and_failure(monkeypatch, qtbot):
|
||||
shown = {"info": 0, "crit": 0}
|
||||
monkeypatch.setattr(
|
||||
QMessageBox,
|
||||
"information",
|
||||
lambda *a, **k: shown.__setitem__("info", shown["info"] + 1),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
QMessageBox,
|
||||
"critical",
|
||||
lambda *a, **k: shown.__setitem__("crit", shown["crit"] + 1),
|
||||
)
|
||||
|
||||
db = FakeDB()
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg._compact_btn_clicked()
|
||||
assert db.compact_called is True
|
||||
assert shown["info"] >= 1
|
||||
|
||||
# Failure path
|
||||
db2 = FakeDB()
|
||||
db2.fail_compact = True
|
||||
dlg2 = SettingsDialog(DBConfig(Path("x"), key=""), db2)
|
||||
qtbot.addWidget(dlg2)
|
||||
dlg2.show()
|
||||
qtbot.waitExposed(dlg2)
|
||||
|
||||
dlg2._compact_btn_clicked()
|
||||
assert shown["crit"] >= 1
|
||||
|
||||
|
||||
def test_save_key_checkbox_preexisting_key_does_not_crash(monkeypatch, qtbot):
|
||||
p = AcceptingPrompt().set_key("already")
|
||||
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p)
|
||||
|
||||
dlg = SettingsDialog(DBConfig(Path("x"), key="already"), FakeDB())
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.show()
|
||||
qtbot.waitExposed(dlg)
|
||||
|
||||
dlg.save_key_btn.setChecked(True)
|
||||
# We should reach here with the original key preserved.
|
||||
assert dlg.key == "already"
|
||||
|
||||
|
||||
def test_save_unchecked_clears_key_and_applies_theme(qtbot, tmp_path):
|
||||
parent = _Parent()
|
||||
qtbot.addWidget(parent)
|
||||
cfg = DBConfig(tmp_path / "db.sqlite", key="sekrit", idle_minutes=5)
|
||||
dlg = SettingsDialog(cfg, FakeDB(), parent=parent)
|
||||
qtbot.addWidget(dlg)
|
||||
dlg.save_key_btn.setChecked(False)
|
||||
# Trigger save
|
||||
dlg._save()
|
||||
assert dlg.config.key == "" # cleared
|
||||
assert parent.themes.calls # applied some theme
|
||||
db.close()
|
||||
cfg.key = "newkey"
|
||||
db2 = DBManager(cfg)
|
||||
assert db2.connect()
|
||||
assert "seed" in db2.get_entry("2001-01-01")
|
||||
db2.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue