""" 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"]