Files
webmail-client/backend/tests/sync/test_thread_repair.py
T

105 lines
3.7 KiB
Python
Raw Normal View History

"""Thread counters that drifted from their messages are repaired (nightly safety
net, app/threading_/threader.py repair_threads)."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from email.message import EmailMessage
from sqlalchemy import delete, select, update
from app.db import session_scope
from app.identity.passwords import hash_password
from app.indexer.indexer import persist_message
from app.models import Account, Draft, Mailbox, Thread, User
from app.providers.base import RemoteMessage
from app.threading_.threader import repair_threads
def _raw(subject: str) -> bytes:
m = EmailMessage()
m["Subject"] = subject
m["From"] = "billing@example.com"
m["To"] = "me@example.com"
m["Message-ID"] = f"<{uuid.uuid4().hex}@example.com>"
m.set_content("receipt")
return m.as_bytes()
async def test_repair_recounts_stale_threads_and_drops_empty_ones() -> None:
async with session_scope() as db:
user = User(
handle=f"rep{uuid.uuid4().hex[:8]}@test.local", password_hash=hash_password("pw")
)
db.add(user)
await db.flush()
account = Account(
user_id=user.id,
email="me@example.com",
imap_config={},
smtp_config={},
secret_ct=b"",
discovery_src="manual",
)
db.add(account)
await db.flush()
inbox = Mailbox(account_id=account.id, user_id=user.id, path="INBOX", special_use="inbox")
db.add(inbox)
await db.flush()
result = await persist_message(
db,
user_id=user.id,
account_id=account.id,
mailbox_id=inbox.id,
mailbox_special_use="inbox",
remote=RemoteMessage(
uid=1,
raw=_raw("Your receipt"),
flags=frozenset({"\\Seen"}),
internal_date=datetime.now(UTC),
),
)
assert result is not None
tid = result.message.thread_id
assert tid is not None
# A read message whose thread still says "unread" (the bug), and a
# thread left behind with no messages at all.
await db.execute(update(Thread).where(Thread.id == tid).values(unread_count=1))
empty = Thread(
user_id=user.id,
subject_norm="gone",
message_count=1,
unread_count=1,
last_message_at=datetime.now(UTC),
)
db.add(empty)
# An emptied thread a reply draft still points at: kept, counts zeroed.
replied = Thread(
user_id=user.id,
subject_norm="replying",
message_count=1,
unread_count=0,
last_message_at=datetime.now(UTC),
)
db.add(replied)
await db.flush()
db.add(Draft(user_id=user.id, account_id=account.id, thread_id=replied.id, payload={}))
await db.flush()
empty_id = empty.id
replied_id = replied.id
try:
async with session_scope() as db:
assert await repair_threads(db, user.id) == 3
async with session_scope() as db:
thread = await db.get(Thread, tid)
assert thread is not None and thread.unread_count == 0 and thread.message_count == 1
assert await db.get(Thread, empty_id) is None
kept = await db.get(Thread, replied_id)
assert kept is not None and kept.message_count == 0
assert await repair_threads(db, user.id) == 0 # nothing left to repair
finally:
async with session_scope() as db:
await db.execute(delete(User).where(User.id == user.id)) # cascades
assert await db.scalar(select(User.id).where(User.id == user.id)) is None