1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
"""
The daily cleanup, against a database that enforces foreign keys.
PostgreSQL always does; the SQLite the rest of the suite runs on does not unless
asked. So `DELETE FROM users` for an account that still had an IP-log row passed
every test here and raised on the real hub — and because every step shared one
`try`, the purges behind it (mail counters, sign-in counters, invitation links)
stopped running for good. Both halves are held here, on an engine with
`PRAGMA foreign_keys=ON`.
"""
from datetime import UTC, datetime, timedelta
import pytest
from meshbay_hub.db.models import (
Base,
EmailVerification,
Group,
GroupMember,
IPLog,
Notification,
User,
)
from meshbay_hub.tasks import cleanup
from sqlalchemy import event, select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
@pytest.fixture
async def strict_db(tmp_path):
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'hub.db'}")
@event.listens_for(engine.sync_engine, "connect")
def _enforce(dbapi_conn, _record):
dbapi_conn.execute("PRAGMA foreign_keys=ON")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield async_sessionmaker(engine, expire_on_commit=False)
await engine.dispose()
def _user(name, status, age_days):
return User(username=name, email="x", pw_hash=b"x", pw_salt=b"x", hub_id="h",
status=status,
created_at=datetime.now(UTC) - timedelta(days=age_days))
async def test_a_stale_pending_account_is_purged_with_what_points_at_it(strict_db):
async with strict_db() as db:
owner = _user("groupowner", "active", 30)
stale = _user("neververified", "pending", 8)
db.add_all([owner, stale])
await db.flush()
group = Group(name="g", admin_id=owner.id)
db.add(group)
await db.flush()
db.add_all([
IPLog(user_id=stale.id, event="account_create", ip_address="192.0.2.1"),
GroupMember(group_id=group.id, user_id=stale.id),
Notification(user_id=stale.id, kind="group_invite", title="t"),
EmailVerification(email_hash="h", code="1", purpose="registration",
user_id=stale.id,
expires_at=datetime.now(UTC) - timedelta(days=6)),
])
await db.commit()
stale_id = stale.id
async with strict_db() as db:
assert await cleanup.purge_stale_pending_users(db) == 1
async with strict_db() as db:
assert await db.get(User, stale_id) is None
log_row = (await db.execute(select(IPLog))).scalar_one()
# The legal record survives, still saying who it was about.
assert log_row.user_id is None and log_row.username == "neververified"
assert (await db.execute(select(GroupMember).where(
GroupMember.user_id == stale_id))).first() is None
async def test_a_recent_or_active_account_is_left_alone(strict_db):
async with strict_db() as db:
db.add_all([_user("stillpending", "pending", 2),
_user("activeuser", "active", 30)])
await db.commit()
async with strict_db() as db:
assert await cleanup.purge_stale_pending_users(db) == 0
async def test_one_failing_step_does_not_stop_the_others(strict_db, monkeypatch):
ran = []
async def broken(db):
raise RuntimeError("boom")
async def later(db):
ran.append("later")
return 0
monkeypatch.setattr(cleanup, "_STEPS", (("broken", broken), ("later", later)))
done = await cleanup.run_cleanup(strict_db)
assert done == {"broken": None, "later": 0}
assert ran == ["later"]
|