diff --git a/tests/unit/test_fee_payout_crash_safety.py b/tests/unit/test_fee_payout_crash_safety.py index 04206b70..0939baa4 100644 --- a/tests/unit/test_fee_payout_crash_safety.py +++ b/tests/unit/test_fee_payout_crash_safety.py @@ -91,6 +91,38 @@ async def test_fee_payout_checkpoints_before_sending() -> None: assert events == ["checkpoint", "send", "complete"] +@pytest.mark.asyncio +async def test_fee_payout_does_not_retry_an_unresolved_checkpoint() -> None: + session = Mock() + fee = SimpleNamespace( + accumulated_msats=10_000, + payout_in_progress_msats=5_000, + payout_started_at=123, + ) + + with ( + patch("routstr.auth.ROUTSTR_FEE_PAYOUT_INTERVAL_SECONDS", 1), + patch("routstr.auth.ROUTSTR_LN_ADDRESS", "fees@example.com"), + patch( + "routstr.wallet.asyncio.sleep", + AsyncMock(side_effect=[None, asyncio.CancelledError()]), + ), + patch("routstr.wallet.db.create_session", return_value=_session_context(session)), + patch("routstr.wallet.db.get_routstr_fee", AsyncMock(return_value=fee)), + patch("routstr.wallet.db.reset_routstr_fee", AsyncMock()) as checkpoint, + patch("routstr.wallet.get_wallet", AsyncMock()) as get_wallet, + patch("routstr.wallet.raw_send_to_lnurl", AsyncMock()) as send, + patch("routstr.wallet.logger.critical") as critical, + ): + with pytest.raises(asyncio.CancelledError): + await wallet.periodic_routstr_fee_payout() + + checkpoint.assert_not_awaited() + get_wallet.assert_not_awaited() + send.assert_not_awaited() + critical.assert_called_once() + + @pytest.mark.asyncio async def test_fee_payout_keeps_checkpoint_when_send_outcome_is_unknown() -> None: session = Mock() diff --git a/tests/unit/test_fee_payout_migration.py b/tests/unit/test_fee_payout_migration.py new file mode 100644 index 00000000..fe549194 --- /dev/null +++ b/tests/unit/test_fee_payout_migration.py @@ -0,0 +1,46 @@ +import os +import sqlite3 +import subprocess +import sys +from pathlib import Path + + +def _run_alembic(root: Path, database_url: str, revision: str) -> None: + env = os.environ.copy() + env["DATABASE_URL"] = database_url + subprocess.run( + [sys.executable, "-m", "alembic", "upgrade", revision], + cwd=root, + env=env, + check=True, + capture_output=True, + text=True, + ) + + +def test_fee_payout_checkpoint_migration_preserves_existing_row( + tmp_path: Path, +) -> None: + root = Path(__file__).resolve().parents[2] + database_path = tmp_path / "migration.db" + database_url = f"sqlite+aiosqlite:///{database_path}" + _run_alembic(root, database_url, "c6d7e8f9a0b1") + + with sqlite3.connect(database_path) as connection: + result = connection.execute( + "UPDATE routstr_fees SET accumulated_msats = 5000, " + "total_paid_msats = 1000, last_paid_at = 123 WHERE id = 1" + ) + assert result.rowcount == 1 + connection.commit() + + _run_alembic(root, database_url, "head") + + with sqlite3.connect(database_path) as connection: + row = connection.execute( + "SELECT accumulated_msats, total_paid_msats, last_paid_at, " + "payout_in_progress_msats, payout_started_at " + "FROM routstr_fees WHERE id = 1" + ).fetchone() + + assert row == (5000, 1000, 123, 0, None)