aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_cleanup_foreign_keys.py103
1 files changed, 103 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_cleanup_foreign_keys.py b/packages/meshbay-hub/tests/test_cleanup_foreign_keys.py
new file mode 100644
index 0000000..b4994b2
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_cleanup_foreign_keys.py
@@ -0,0 +1,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"]