mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-28 08:42:24 +01:00
- Enforce vision probe SSRF safe local host validation, SafeProbeBackend TOCTOU elimination, and session owner authorization - Authorize session titling endpoints against registered owner configuration - Sanitize sensitive metadata logging from agent loop debug logs - Add async non-blocking execution for spec router tools and thinking decisions - Harden email cache invalidation fencing and thread-safe mutation - Add regression test suites for legacy browser authority, spec contracts, spec router request scoping, and task MCP delivery
218 lines
8.8 KiB
Python
218 lines
8.8 KiB
Python
"""Email list stale-while-revalidate (SWR) regression tests.
|
|
|
|
A plain list view must never block ~2-5 s on IMAP while we already hold a
|
|
recent snapshot or a durable index: it should return the snapshot immediately
|
|
(with ``sync.stale``) and refresh in the background. Blocking IMAP only remains
|
|
for a genuine cold start (no snapshot AND no indexed rows).
|
|
"""
|
|
import asyncio
|
|
import pathlib
|
|
|
|
from routes.email import email_routes as er
|
|
|
|
SRC = pathlib.Path(er.__file__).read_text(encoding="utf-8")
|
|
|
|
|
|
def _list_endpoint():
|
|
router = er.setup_email_routes()
|
|
for route in router.routes:
|
|
if getattr(route, "name", "") == "list_emails":
|
|
return route.endpoint
|
|
raise AssertionError("list_emails route not found")
|
|
|
|
|
|
def _call(endpoint, **over):
|
|
kwargs = dict(
|
|
folder="INBOX", limit=50, offset=0, filter="all", from_addr=None,
|
|
account_id=None, has_attachments=0, date_from=None, date_to=None,
|
|
cached_only=0, cache_bust=None, refresh=0, owner="pewds",
|
|
)
|
|
kwargs.update(over)
|
|
return asyncio.run(endpoint(**kwargs))
|
|
|
|
|
|
def test_list_returns_index_snapshot_with_a_stale_marker_without_blocking_on_imap(monkeypatch):
|
|
endpoint = _list_endpoint()
|
|
monkeypatch.setattr(
|
|
er, "_email_index_list",
|
|
lambda owner, account_id, folder, filter_, limit, offset, has_att: (
|
|
[{"uid": "11", "subject": "Indexed", "is_read": False}], 1, "2026-10-11T00:00:00Z",
|
|
),
|
|
)
|
|
# The synchronous path must never open IMAP; if it did the request would
|
|
# raise instead of returning the index snapshot immediately.
|
|
monkeypatch.setattr(er, "_imap_connect", lambda *a, **k: (_ for _ in ()).throw(
|
|
AssertionError("SWR must not open IMAP when an index snapshot exists")))
|
|
|
|
result = _call(endpoint, cache_bust="123")
|
|
assert result["total"] == 1
|
|
assert [e["uid"] for e in result["emails"]] == ["11"]
|
|
assert result["sync"]["stale"] is True
|
|
assert result["sync"]["source"] in ("index", "cache_stale")
|
|
|
|
|
|
def test_cold_start_with_no_snapshot_and_no_index_is_not_marked_stale(monkeypatch):
|
|
# With neither a snapshot nor an index, SWR must NOT fire: the request
|
|
# falls through to the real (blocking) path, so the response is not a
|
|
# stale-marked snapshot.
|
|
endpoint = _list_endpoint()
|
|
monkeypatch.setattr(er, "_email_index_list", lambda *a, **k: ([], 0, None))
|
|
monkeypatch.setattr(er, "_imap_connect", lambda *a, **k: (_ for _ in ()).throw(
|
|
RuntimeError("offline test")))
|
|
try:
|
|
result = _call(endpoint)
|
|
except Exception:
|
|
return # the cold path legitimately attempted real IMAP
|
|
assert isinstance(result, dict)
|
|
assert not (result.get("sync") or {}).get("stale")
|
|
|
|
|
|
def test_swr_contract_is_wired_into_the_list_path():
|
|
assert "_LIST_STALE" in SRC
|
|
assert "def _list_stale_put" in SRC
|
|
assert "def _schedule_list_refresh" in SRC
|
|
assert 'sync_meta["source"] = "cache_stale"' in SRC
|
|
assert 'sync_meta["stale"] = True' in SRC
|
|
# The manual-refresh contract the UI relies on must stay intact.
|
|
assert "manual_refresh = bool(refresh)" in SRC
|
|
assert "_invalidate_list_cache(account_id, folder)" in SRC
|
|
assert "cached_rows = {} if refresh else _email_index_rows" in SRC
|
|
|
|
|
|
def test_manual_refresh_fetches_mailbox_instead_of_returning_stale_index(monkeypatch):
|
|
class EmptyMailbox:
|
|
def select(self, folder, readonly=False):
|
|
return 'OK', [b'0']
|
|
def uid(self, command, *args):
|
|
assert command == 'SEARCH'
|
|
return 'OK', [b'']
|
|
def logout(self):
|
|
pass
|
|
|
|
endpoint = _list_endpoint()
|
|
monkeypatch.setattr(er, '_start_poller', lambda: None)
|
|
monkeypatch.setattr(er, '_imap_connect', lambda *a, **k: EmptyMailbox())
|
|
monkeypatch.setattr(er, '_email_index_list', lambda *a, **k: (
|
|
[{'uid': 'old', 'subject': 'Old'}], 1, 'yesterday'))
|
|
|
|
result = _call(endpoint, refresh=1)
|
|
assert result['emails'] == []
|
|
assert result['total'] == 0
|
|
assert not result.get('sync', {}).get('stale')
|
|
|
|
|
|
def test_stale_index_is_scoped_to_authenticated_owner(monkeypatch):
|
|
endpoint = _list_endpoint()
|
|
monkeypatch.setattr(er, '_start_poller', lambda: None)
|
|
monkeypatch.setattr(er, '_email_index_list', lambda owner, *a: (
|
|
[{'uid': owner, 'subject': owner}], 1, 'yesterday'))
|
|
monkeypatch.setattr(er, '_imap_connect', lambda *a, **k: (_ for _ in ()).throw(RuntimeError('offline')))
|
|
|
|
alice = _call(endpoint, owner='alice')
|
|
bob = _call(endpoint, owner='bob')
|
|
assert [mail['uid'] for mail in alice['emails']] == ['alice']
|
|
assert [mail['uid'] for mail in bob['emails']] == ['bob']
|
|
|
|
|
|
def test_inflight_refresh_cannot_restore_old_index_flags_after_mark_read(monkeypatch, tmp_path):
|
|
"""A delayed mailbox snapshot must not undo a completed owner mutation."""
|
|
import threading
|
|
from contextlib import contextmanager
|
|
from routes.email import email_helpers
|
|
|
|
database = tmp_path / 'email.db'
|
|
monkeypatch.setattr(email_helpers, 'SCHEDULED_DB', database)
|
|
monkeypatch.setattr(er, 'SCHEDULED_DB', database)
|
|
email_helpers._init_scheduled_db()
|
|
er._email_index_upsert('alice', 'acct', 'INBOX', [{
|
|
'uid': '42', 'subject': 'Example', 'flags': '', 'date_epoch': 1,
|
|
}])
|
|
fetched = threading.Event()
|
|
release = threading.Event()
|
|
threads = []
|
|
|
|
class Mailbox:
|
|
def __init__(self, mutation=False):
|
|
self.mutation = mutation
|
|
def select(self, *args, **kwargs):
|
|
return 'OK', [b'1']
|
|
def uid(self, command, *args):
|
|
if command == 'SEARCH':
|
|
return 'OK', [b'42']
|
|
if command == 'FETCH':
|
|
if self.mutation:
|
|
return 'OK', [b'1 (UID 42)']
|
|
threads.append(threading.current_thread())
|
|
fetched.set()
|
|
assert release.wait(5)
|
|
return 'OK', [(b'1 (UID 42 FLAGS () RFC822.SIZE 100 RFC822.HEADER {20}',
|
|
b'Subject: Example\r\n\r\n')]
|
|
if command == 'STORE':
|
|
return 'OK', []
|
|
raise AssertionError(command)
|
|
def logout(self):
|
|
pass
|
|
|
|
@contextmanager
|
|
def mailbox(*args, **kwargs):
|
|
yield Mailbox(mutation=True)
|
|
|
|
monkeypatch.setattr(er, '_start_poller', lambda: None)
|
|
monkeypatch.setattr(er, '_imap_connect', lambda *a, **kw: Mailbox())
|
|
monkeypatch.setattr(er, '_imap', mailbox)
|
|
router = er.setup_email_routes()
|
|
listing = next(r.endpoint for r in router.routes if getattr(r, 'name', '') == 'list_emails')
|
|
mark_read = next(r.endpoint for r in router.routes if getattr(r, 'name', '') == 'mark_read')
|
|
try:
|
|
assert _call(listing, owner='alice', account_id='acct')['emails'][0]['is_read'] is False
|
|
assert fetched.wait(5)
|
|
refresh_thread = threads[0]
|
|
result = asyncio.run(mark_read('42', folder='INBOX', account_id='acct', owner='alice'))
|
|
assert result['success'] is True
|
|
finally:
|
|
release.set()
|
|
refresh_thread.join(timeout=5)
|
|
assert not refresh_thread.is_alive()
|
|
rows, _, _ = er._email_index_list('alice', 'acct', 'INBOX', 'all', 50, 0)
|
|
assert rows[0]['is_read'] is True
|
|
|
|
|
|
def test_mark_read_keeps_other_owners_default_mailbox_unread(monkeypatch, tmp_path):
|
|
from contextlib import contextmanager
|
|
from routes.email import email_helpers
|
|
|
|
database = tmp_path / 'email.db'
|
|
monkeypatch.setattr(email_helpers, 'SCHEDULED_DB', database)
|
|
monkeypatch.setattr(er, 'SCHEDULED_DB', database)
|
|
email_helpers._init_scheduled_db()
|
|
for owner in ('alice', 'bob'):
|
|
er._email_index_upsert(owner, None, 'INBOX', [{
|
|
'uid': '42', 'subject': owner, 'flags': '', 'date_epoch': 1,
|
|
}])
|
|
|
|
class Mailbox:
|
|
def select(self, *args, **kwargs):
|
|
return 'OK', [b'1']
|
|
def uid(self, command, *args):
|
|
if command == 'FETCH':
|
|
return 'OK', [b'1 (UID 42)']
|
|
if command == 'STORE':
|
|
return 'OK', []
|
|
raise AssertionError(command)
|
|
|
|
@contextmanager
|
|
def mailbox(*args, **kwargs):
|
|
yield Mailbox()
|
|
|
|
monkeypatch.setattr(er, '_start_poller', lambda: None)
|
|
monkeypatch.setattr(er, '_imap', mailbox)
|
|
monkeypatch.setattr(er, '_imap_connect', lambda *a, **kw: (_ for _ in ()).throw(RuntimeError('offline')))
|
|
router = er.setup_email_routes()
|
|
listing = next(r.endpoint for r in router.routes if getattr(r, 'name', '') == 'list_emails')
|
|
mark_read = next(r.endpoint for r in router.routes if getattr(r, 'name', '') == 'mark_read')
|
|
assert _call(listing, owner='bob')['emails'][0]['is_read'] is False
|
|
assert asyncio.run(mark_read('42', folder='INBOX', account_id=None, owner='alice'))['success'] is True
|
|
assert _call(listing, owner='bob')['emails'][0]['is_read'] is False
|
|
rows, _, _ = er._email_index_list('alice', None, 'INBOX', 'all', 50, 0)
|
|
assert rows[0]['is_read'] is True
|