fix(manual-ledger): harden schema init race and sanitize error responses
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
78e347703e
commit
a36b451a3d
@@ -9,6 +9,7 @@ or production application database.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from urllib.parse import unquote, urlparse
|
||||
@@ -407,3 +408,58 @@ def test_large_invest_and_large_dividend_tax_metadata_are_accepted_when_snapshot
|
||||
)
|
||||
assert dividend.tax_amount == 50000000.0
|
||||
repo.close()
|
||||
|
||||
|
||||
def test_cold_repositories_concurrent_dividend_requests_follow_duplicate_contract(
|
||||
isolated_settings: Settings,
|
||||
) -> None:
|
||||
"""Cold concurrent writes should not fail on ensure_schema DDL race."""
|
||||
upgrade_to_head(isolated_settings)
|
||||
seed_repo = _repo(isolated_settings)
|
||||
portfolio_id = _seed_portfolio(seed_repo, "cold-concurrent-dividend")
|
||||
seed_repo.close()
|
||||
barrier = threading.Barrier(2)
|
||||
results: list[tuple[str, object]] = []
|
||||
|
||||
def _worker(note: str) -> None:
|
||||
repo = _repo(isolated_settings)
|
||||
try:
|
||||
barrier.wait()
|
||||
entry = repo.record_manual_ledger(
|
||||
portfolio_id,
|
||||
action="DIVIDEND",
|
||||
amount=Decimal("1.00000000"),
|
||||
executed_at=datetime(2026, 9, 1, 0, 0, 0, tzinfo=UTC),
|
||||
note=note,
|
||||
)
|
||||
results.append(("ok", entry))
|
||||
except ValueError as exc:
|
||||
results.append(("error", str(exc)))
|
||||
finally:
|
||||
repo.close()
|
||||
|
||||
t1 = threading.Thread(target=_worker, args=("same-note",))
|
||||
t2 = threading.Thread(target=_worker, args=("same-note",))
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
|
||||
assert len(results) == 2
|
||||
successes = [value for kind, value in results if kind == "ok"]
|
||||
errors = [value for kind, value in results if kind == "error"]
|
||||
assert len(successes) == 1
|
||||
assert len(errors) == 1
|
||||
assert "POTENTIAL_DUPLICATE_DIVIDEND:" in str(errors[0])
|
||||
assert "duplicate key value" not in str(errors[0]).lower()
|
||||
|
||||
verify_repo = _repo(isolated_settings)
|
||||
with verify_repo._connect().cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM portfolio_ledger WHERE portfolio_id = %s AND side = 'dividend'",
|
||||
(portfolio_id,),
|
||||
)
|
||||
assert cur.fetchone()[0] == 1
|
||||
cur.execute("SELECT profit_buffer::text FROM portfolios WHERE portfolio_id = %s", (portfolio_id,))
|
||||
assert cur.fetchone()[0] == "1"
|
||||
verify_repo.close()
|
||||
|
||||
@@ -386,6 +386,31 @@ def test_insufficient_cash_rejected_as_409() -> None:
|
||||
|
||||
assert resp.status_code == 409
|
||||
assert resp.json()["detail"]["code"] == "INSUFFICIENT_CASH"
|
||||
assert resp.json()["detail"]["message"] == "Insufficient cash balance"
|
||||
|
||||
|
||||
def test_manual_ledger_conflict_message_is_sanitized() -> None:
|
||||
repo = _FakeRepo(_portfolio())
|
||||
client = _make_client(repo)
|
||||
|
||||
secret = "SENTINEL_SECRET_TOKEN_12345"
|
||||
|
||||
def _raise_secret(*args: Any, **kwargs: Any) -> LedgerEntry:
|
||||
raise ValueError(f"synthetic backend failure: {secret}")
|
||||
|
||||
repo.record_manual_ledger = _raise_secret # type: ignore[method-assign]
|
||||
|
||||
resp = client.post(
|
||||
f"/v1/portfolios/{_PORTFOLIO_ID}/ledger/manual",
|
||||
json={"action": "DIVIDEND", "amount": 1, "executed_at": "2026-09-01"},
|
||||
headers=_auth(),
|
||||
)
|
||||
|
||||
assert resp.status_code == 422
|
||||
detail = resp.json()["detail"]
|
||||
assert detail["code"] == "MANUAL_LEDGER_CONFLICT"
|
||||
assert detail["message"] == "Manual ledger request could not be processed"
|
||||
assert secret not in resp.text
|
||||
|
||||
|
||||
def test_manual_history_returns_total_value_after() -> None:
|
||||
|
||||
Reference in New Issue
Block a user