aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_cleanup_foreign_keys.py
blob: b4994b2573c2ad8141a7bf2b88ce9307a6aa849a (plain) (blame)
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"]