diff --git a/app/treasury.py b/app/treasury.py index b7d042d5..0342a139 100644 --- a/app/treasury.py +++ b/app/treasury.py @@ -34,6 +34,7 @@ TreasuryProposal, utc_now, ) +from app.path_params import SQLITE_INTEGER_MAX from app.serializers import bounty_to_dict TREASURY_PROPOSAL_DELAY = timedelta(hours=24) @@ -135,6 +136,8 @@ def _canonical_payload(action: str, payload: dict[str, Any]) -> dict[str, Any]: bounty_id = _payload_int(_required_payload_value(payload, "bounty_id"), "bounty_id") if bounty_id <= 0: raise LedgerError("bounty id must be positive") + if bounty_id > SQLITE_INTEGER_MAX: + raise LedgerError("bounty id is too large") clean: dict[str, Any] = { "bounty_id": bounty_id, "to_account": _clean_string( @@ -159,6 +162,8 @@ def _canonical_payload(action: str, payload: dict[str, Any]) -> dict[str, Any]: bounty_id = _payload_int(_required_payload_value(payload, "bounty_id"), "bounty_id") if bounty_id <= 0: raise LedgerError("bounty id must be positive") + if bounty_id > SQLITE_INTEGER_MAX: + raise LedgerError("bounty id is too large") reference = _optional_string(payload.get("reference"), "reference", 500) clean = { "bounty_id": bounty_id, diff --git a/tests/test_treasury_proposals.py b/tests/test_treasury_proposals.py index 3b93e2b8..3bc75bda 100644 --- a/tests/test_treasury_proposals.py +++ b/tests/test_treasury_proposals.py @@ -380,6 +380,36 @@ def test_manual_payout_rejects_missing_bounty_before_proposal_creation( assert session.scalar(select(func.count(TreasuryProposal.id))) == 0 +@pytest.mark.parametrize("action", ["pay_bounty", "close_bounty"]) +def test_direct_pay_and_close_proposals_reject_oversized_bounty_id( + sqlite_url: str, monkeypatch: pytest.MonkeyPatch, action: str +) -> None: + client = _client(sqlite_url, monkeypatch) + payload: dict[str, object] = { + "bounty_id": 2**63, + "closed_by": "maintainer", + "reference": "https://github.com/ramimbo/mergework/issues/1#close", + } + if action == "pay_bounty": + payload = { + "bounty_id": 2**63, + "to_account": "github:bob", + "submission_url": "https://github.com/ramimbo/mergework/pull/1", + "accepted_by": "maintainer", + } + + response = client.post( + "/api/v1/treasury/proposals", + headers=ADMIN_HEADERS, + json={"action": action, "payload": payload}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "bounty id is too large" + with session_scope(sqlite_url) as session: + assert session.scalar(select(func.count(TreasuryProposal.id))) == 0 + + def test_manual_payout_rejects_existing_unpaid_submission_before_proposal_creation( sqlite_url: str, monkeypatch: pytest.MonkeyPatch ) -> None: