mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-05 02:45:28 +00:00
atomic_write_json/atomic_write_text build their temp filename as
"{path}.tmp.{os.getpid()}". os.getpid() is constant for the life of a
process, so it only ever distinguishes concurrent writers that live in
different OS processes. Odysseus runs as a single long-lived process
per container, so two concurrent writers to the same path (e.g. two
request handlers racing a settings save) always compute the identical
temp path. Whichever finishes os.replace() first removes the shared
tmp file out from under the other, which then raises FileNotFoundError
on its own os.replace() instead of landing its write.
Fix: derive the temp suffix from uuid4() instead of the PID, so every
call gets a distinct temp path regardless of process/thread identity.
routes/prefs_routes.py's _save() had an independent, hand-rolled copy
of the exact same PID-suffix logic (not the shared core.atomic_io
helper other routes already use, e.g. routes/auth_routes.py) with the
same bug. Replaced it with a call to atomic_write_json.
Fixes #5596
48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
import json
|
|
|
|
import routes.prefs_routes as prefs_routes
|
|
from core import atomic_io
|
|
|
|
|
|
def test_save_replaces_prefs_file_atomically(monkeypatch, tmp_path):
|
|
calls = []
|
|
real_replace = atomic_io.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(atomic_io.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"}
|