wabot/tests/unit/test_sessions.py
Mathew Sir Guest the best 154344e94a refactor: rename sessions submodule to _sessions to avoid shadowing sessions()
wabot/__init__.py defines def sessions(...) and also had a sessions.py
submodule, so wabot.sessions (attribute) resolved to the function,
shadowing the module for import wabot.sessions as m style access. This
is the same class of collision that browser.py -> _browser.py already
fixed. Renaming to _sessions.py removes the ambiguity and lets
docs/api.rst drop the :no-index: Sphinx workaround in favor of plain
autoclass directives on the public SessionRecord/SessionStore
re-exports.
2026-07-12 15:37:56 -06:00

126 lines
5.0 KiB
Python

from datetime import datetime, timedelta, timezone
from wabot._sessions import SessionRecord, SessionStore
def make_record(name="scraper1", created_at=None):
return SessionRecord(
name=name,
executor_url="http://127.0.0.1:9515",
session_id="abc123",
browser="chromium",
created_at=(created_at or datetime.now(timezone.utc)).isoformat(),
service_pid=4242,
service_port=9515,
)
class TestSessionStore:
def test_get_missing_returns_none(self, tmp_path):
store = SessionStore(path=tmp_path / "sessions.json")
assert store.get("nope") is None
def test_save_and_get_round_trip(self, tmp_path):
store = SessionStore(path=tmp_path / "sessions.json")
store.save(make_record())
rec = store.get("scraper1")
assert rec == make_record(created_at=datetime.fromisoformat(rec.created_at))
assert rec.session_id == "abc123"
assert rec.service_pid == 4242
def test_save_is_readable_by_a_fresh_store_instance(self, tmp_path):
path = tmp_path / "sessions.json"
SessionStore(path=path).save(make_record())
assert SessionStore(path=path).get("scraper1") is not None
def test_names_lists_sessions_sorted(self, tmp_path):
store = SessionStore(path=tmp_path / "sessions.json")
store.save(make_record(name="zeta"))
store.save(make_record(name="alpha"))
assert store.names() == ["alpha", "zeta"]
def test_remove(self, tmp_path):
store = SessionStore(path=tmp_path / "sessions.json")
store.save(make_record())
store.remove("scraper1")
assert store.get("scraper1") is None
store.remove("scraper1") # removing twice must not raise
def test_stale_records_are_evicted_on_load(self, tmp_path):
store = SessionStore(path=tmp_path / "sessions.json", max_age=timedelta(days=3))
old = datetime.now(timezone.utc) - timedelta(days=4)
store.save(make_record(name="old", created_at=old))
store.save(make_record(name="fresh"))
assert store.names() == ["fresh"]
def test_corrupt_file_is_moved_aside_not_crashed_on(self, tmp_path):
path = tmp_path / "sessions.json"
path.write_text("{ this is not json")
store = SessionStore(path=path)
assert store.load() == {}
assert (tmp_path / "sessions.json.bad").exists()
store.save(make_record()) # store is usable again
assert store.get("scraper1") is not None
def test_default_path_is_under_platformdirs(self):
from wabot._sessions import default_store_path
assert default_store_path().name == "sessions.json"
assert "wabot" in str(default_store_path())
def test_naive_created_at_is_treated_as_utc_not_crash(self, tmp_path):
# legacy/hand-written records may carry naive timestamps
import json
path = tmp_path / "sessions.json"
record = make_record()
data = {"scraper1": {**record.__dict__, "created_at": "2999-01-01T00:00:00"}}
path.write_text(json.dumps(data))
store = SessionStore(path=path)
assert store.get("scraper1") is not None # fresh: kept, assumed UTC
def test_garbage_created_at_evicts_only_that_record(self, tmp_path):
import json
path = tmp_path / "sessions.json"
good, bad = make_record(name="good"), make_record(name="bad")
data = {
"good": good.__dict__,
"bad": {**bad.__dict__, "created_at": "not-a-date"},
}
path.write_text(json.dumps(data))
store = SessionStore(path=path)
assert store.names() == ["good"] # no crash, bad evicted, file rewritten
assert "not-a-date" not in path.read_text()
def test_null_created_at_evicts_only_that_record(self, tmp_path):
import json
path = tmp_path / "sessions.json"
data = {"broken": {**make_record(name="broken").__dict__, "created_at": None}}
path.write_text(json.dumps(data))
assert SessionStore(path=path).names() == []
def test_unknown_record_fields_are_ignored_not_fatal(self, tmp_path):
# a newer wabot may add fields; older versions must not quarantine the store
import json
path = tmp_path / "sessions.json"
data = {"scraper1": {**make_record().__dict__, "future_field": 42}}
path.write_text(json.dumps(data))
assert SessionStore(path=path).get("scraper1") is not None
def test_repeat_corruption_with_existing_bad_file_does_not_crash(self, tmp_path):
path = tmp_path / "sessions.json"
(tmp_path / "sessions.json.bad").write_text("old corruption")
path.write_text("{ corrupt again")
store = SessionStore(path=path)
assert store.load() == {}
store.save(make_record())
assert store.get("scraper1") is not None
def test_write_is_atomic_no_tmp_leftover(self, tmp_path):
store = SessionStore(path=tmp_path / "sessions.json")
store.save(make_record())
assert not (tmp_path / "sessions.json.tmp").exists()