import os import stat import pytest pytest.importorskip("tkinter") from ccma.config import AppConfig # noqa: E402 from ccma.storage.repository import MemberRepository # noqa: E402 # Root ignores the permission bits that stand in for a read-only mount here. pytestmark = pytest.mark.skipif( hasattr(os, "geteuid") and os.geteuid() == 0, reason="root bypasses file permissions" ) def _set_writable(root, writable: bool) -> None: for path in sorted(root.rglob("*"), reverse=True) + [root]: mode = stat.S_IMODE(path.stat().st_mode) path.chmod(mode | stat.S_IWUSR if writable else mode & ~stat.S_IWUSR) @pytest.fixture def read_only_window(tk_root, tmp_path): from ccma.ui.main_window import MainWindow root_path = tmp_path / "store" repository = MemberRepository(root_path) repository.initialize() repository.create_member(first_name="Ada", last_name="Lovelace", birth_date="1990-01-01") _set_writable(root_path, False) window = MainWindow(tk_root, MemberRepository(root_path), AppConfig(store_path=str(root_path)), [], []) window.pack(fill="both", expand=True) tk_root.update() yield window window.destroy() tk_root.title("") tk_root.update() _set_writable(root_path, True) def test_a_read_only_store_is_marked_everywhere(read_only_window, tk_root): assert "NUR LESEN" in tk_root.title() assert "NUR LESEN" in read_only_window.store_var.get() assert read_only_window.messages.winfo_manager(), "der Warnbanner fehlt" def test_the_banner_does_not_ask_for_a_restart(read_only_window): banner = read_only_window.messages.winfo_children()[0] text = " ".join( str(child.cget("text")) for child in banner.winfo_children()[0].winfo_children() if "text" in child.keys() ) # The banner used to end with "und CCMA neu starten", which is wrong -- the # session recovers in place. assert "Erneut prüfen" in text assert "neu starten" not in text.casefold() assert "Neustart ist dafür nicht nötig" in text def test_the_data_review_mailing_is_refused_on_a_read_only_store(read_only_window, monkeypatch): from tkinter import messagebox warned = [] monkeypatch.setattr(messagebox, "showwarning", lambda *args, **kwargs: warned.append(args)) read_only_window.open_data_review_mail() assert warned and "schreibgeschützt" in warned[0][0] assert "Der Versand der Datenüberprüfung ist nicht möglich" in warned[0][1] assert not [ child for child in read_only_window.winfo_children() if child.winfo_class() == "Toplevel" ] def test_a_remounted_store_clears_every_read_only_marker(read_only_window, tk_root, monkeypatch): from tkinter import messagebox _set_writable(read_only_window.repository.root, True) asked = [] monkeypatch.setattr( messagebox, "askyesno", lambda *args, **kwargs: asked.append(args) or False ) read_only_window._recheck_store_access() tk_root.update() assert read_only_window.repository.read_only is False assert "NUR LESEN" not in tk_root.title() assert "NUR LESEN" not in read_only_window.store_var.get() assert not read_only_window.messages.winfo_manager(), "der Warnbanner steht noch" assert read_only_window.status_var.get() == "Der Store ist wieder beschreibbar." assert asked, "der übersprungene Hausmeisterlauf wird nicht angeboten" def test_the_skipped_housekeeper_run_can_be_started_right_away( read_only_window, tk_root, monkeypatch ): from tkinter import messagebox _set_writable(read_only_window.repository.root, True) monkeypatch.setattr(messagebox, "askyesno", lambda *args, **kwargs: True) read_only_window._recheck_store_access() tk_root.update() assert (read_only_window.repository.root / "housekeeper.json").is_file() assert read_only_window.status_var.get().startswith("Hausmeisterlauf beendet") def test_a_store_that_is_still_read_only_keeps_its_markers(read_only_window, tk_root): read_only_window._recheck_store_access() tk_root.update() assert "NUR LESEN" in tk_root.title() assert "NUR LESEN" in read_only_window.store_var.get() assert read_only_window.messages.winfo_manager() assert read_only_window.status_var.get() == "Der Store ist weiterhin schreibgeschützt."