From e340674c12ae3bba5b63f1051152ddf4f49b5d7f Mon Sep 17 00:00:00 2001 From: ".bulat" Date: Thu, 4 Jun 2026 05:55:22 +0300 Subject: [PATCH] Persist user prefs atomically (#1840) --- routes/prefs_routes.py | 8 ++++-- tests/test_prefs_atomic_write.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/test_prefs_atomic_write.py diff --git a/routes/prefs_routes.py b/routes/prefs_routes.py index ce88fc8..f58049c 100644 --- a/routes/prefs_routes.py +++ b/routes/prefs_routes.py @@ -19,9 +19,13 @@ def _load(): def _save(prefs): - os.makedirs(os.path.dirname(PREFS_FILE), exist_ok=True) - with open(PREFS_FILE, "w", encoding="utf-8") as f: + os.makedirs(os.path.dirname(PREFS_FILE) or ".", exist_ok=True) + tmp = f"{PREFS_FILE}.tmp.{os.getpid()}" + with open(tmp, "w", encoding="utf-8") as f: json.dump(prefs, f, indent=2) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, PREFS_FILE) def _load_for_user(user: Optional[str] = None) -> dict: diff --git a/tests/test_prefs_atomic_write.py b/tests/test_prefs_atomic_write.py new file mode 100644 index 0000000..d7eac30 --- /dev/null +++ b/tests/test_prefs_atomic_write.py @@ -0,0 +1,47 @@ +import json + +import routes.prefs_routes as prefs_routes + + +def test_save_replaces_prefs_file_atomically(monkeypatch, tmp_path): + calls = [] + real_replace = prefs_routes.os.replace + + def fake_replace(src, dst): + calls.append((src, dst)) + real_replace(src, dst) + + prefs_file = tmp_path / "data" / "user_prefs.json" + monkeypatch.setattr(prefs_routes, "PREFS_FILE", str(prefs_file)) + monkeypatch.setattr(prefs_routes.os, "replace", fake_replace) + + prefs_routes._save({"theme": "dark"}) + + assert len(calls) == 1 + src, dst = calls[0] + assert dst == str(prefs_file) + assert src.startswith(str(prefs_file) + ".tmp.") + assert json.loads(prefs_file.read_text(encoding="utf-8")) == {"theme": "dark"} + assert not list(prefs_file.parent.glob("*.tmp.*")) + + +def test_save_for_user_preserves_scoped_user_prefs(monkeypatch, tmp_path): + prefs_file = tmp_path / "data" / "user_prefs.json" + monkeypatch.setattr(prefs_routes, "PREFS_FILE", str(prefs_file)) + + prefs_routes._save_for_user("alice", {"theme": "dark"}) + + data = json.loads(prefs_file.read_text(encoding="utf-8")) + assert data == {"_users": {"alice": {"theme": "dark"}}} + assert prefs_routes._load_for_user("alice") == {"theme": "dark"} + + +def test_save_for_user_preserves_flat_prefs_when_auth_disabled(monkeypatch, tmp_path): + prefs_file = tmp_path / "data" / "user_prefs.json" + monkeypatch.setattr(prefs_routes, "PREFS_FILE", str(prefs_file)) + + prefs_routes._save_for_user(None, {"theme": "dark"}) + + data = json.loads(prefs_file.read_text(encoding="utf-8")) + assert data == {"theme": "dark"} + assert prefs_routes._load_for_user(None) == {"theme": "dark"}