from pathlib import Path from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox from bouquin.db import DBConfig from bouquin.settings_dialog import SettingsDialog class FakeDB: def __init__(self): self.rekey_called_with = None self.compact_called = False self.fail_compact = False def rekey(self, key: str): self.rekey_called_with = key 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) dlg = SettingsDialog(cfg, db) 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) # User toggles "Remember key" -> stores prompted key dlg.save_key_btn.setChecked(True) dlg._save() out = saved["cfg"] assert out.path == new_path assert out.idle_minutes == 0 assert out.key == "sekrit" 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) 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), ) 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 == "newkey" assert shown["info"] >= 1 assert dlg.key == "newkey" 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"