Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions application/account_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def collect_account_facts(
except Exception:
raise _fail("spot_read_failed") from None
if not isinstance(account, Mapping):
raise _fail("spot_response_invalid")
raise _fail("spot_response_not_object")
uid = account.get("uid")
if isinstance(uid, bool) or not isinstance(uid, (str, int)) or not str(uid):
raise _fail("account_identity_missing")
Expand All @@ -178,19 +178,23 @@ def collect_account_facts(
raise _fail("account_identity_mismatch")

balances = account.get("balances")
if not isinstance(balances, list) or len(balances) > 5000:
raise _fail("spot_response_invalid")
if not isinstance(balances, list):
raise _fail("spot_balances_invalid")
if len(balances) > 5000:
raise _fail("spot_balances_limit_exceeded")
assets: dict[str, dict[str, Decimal]] = {}
for row in balances:
if not isinstance(row, Mapping):
raise _fail("spot_response_invalid")
raise _fail("spot_balance_row_invalid")
asset = row.get("asset")
if (
not isinstance(asset, str)
or not _ASSET.fullmatch(asset)
or asset in assets
):
raise _fail("spot_response_invalid")
if not isinstance(asset, str):
raise _fail("spot_asset_type_invalid")
if not asset.isascii():
raise _fail("spot_asset_non_ascii")
if not _ASSET.fullmatch(asset):
raise _fail("spot_asset_format_invalid")
if asset in assets:
raise _fail("spot_asset_duplicate")
assets[asset] = {
"spot_free": _decimal(row.get("free")),
"spot_locked": _decimal(row.get("locked")),
Expand Down
26 changes: 26 additions & 0 deletions tests/test_binance_account_facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,32 @@ def get_account(self):
assert client.calls == [("spot",)]


@pytest.mark.parametrize(("account", "reason"), [
(None, "spot_response_not_object"),
({"uid": "123456", "balances": None}, "spot_balances_invalid"),
({"uid": "123456", "balances": [{}] * 5001}, "spot_balances_limit_exceeded"),
({"uid": "123456", "balances": ["synthetic-private-row"]}, "spot_balance_row_invalid"),
({"uid": "123456", "balances": [{"asset": 17}]}, "spot_asset_type_invalid"),
({"uid": "123456", "balances": [{"asset": "synthetic-bad-symbol"}]}, "spot_asset_format_invalid"),
({"uid": "123456", "balances": [{"asset": "币"}]}, "spot_asset_non_ascii"),
({"uid": "123456", "balances": [
{"asset": "SYNTHETIC", "free": "0", "locked": "0"},
{"asset": "SYNTHETIC", "free": "0", "locked": "0"},
]}, "spot_asset_duplicate"),
])
def test_spot_validation_uses_fixed_reason_without_echoing_response_values(account, reason):
class InvalidSpot(FakeBinance):
def get_account(self):
self.calls.append(("spot",))
return account

client = InvalidSpot()
with pytest.raises(AccountFactsUnavailable) as raised:
_collect(client)
assert str(raised.value) == f"binance_account_facts_{reason}"
assert client.calls == [("spot",)]


def test_rejects_earn_asset_without_an_explicit_spot_balance_row():
class SpotWithoutEarnAsset(FakeBinance):
def get_account(self):
Expand Down
Loading