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
9 changes: 5 additions & 4 deletions application/firstrade_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,14 +451,15 @@ def get_positions(self, account: str) -> dict[str, Any]:
def get_orders(self, account: str, *, per_page: int = 0) -> list[dict[str, Any]]:
_, account_data = self.require_connected()
payload = account_data.get_orders(account, per_page=per_page)
if isinstance(payload, list):
return [dict(row) for row in payload if isinstance(row, dict)]
if isinstance(payload, dict):
for key in ("items", "orders", "data", "result"):
value = payload.get(key)
if isinstance(value, list):
return [dict(row) for row in value if isinstance(row, dict)]
return []
payload = value
break
if not isinstance(payload, list) or any(not isinstance(row, dict) for row in payload):
raise FirstradePlatformError("Firstrade returned an invalid order response.")
return [dict(row) for row in payload]

def get_order_status(self, account: str, order_id: str) -> dict[str, Any] | None:
normalized_order_id = str(order_id or "").strip()
Expand Down
27 changes: 27 additions & 0 deletions tests/test_firstrade_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from application.firstrade_client import (
FirstradeBrokerClient,
FirstradeCredentials,
FirstradePlatformError,
FirstradeSafetyError,
StockOrderRequest,
mask_account_id,
Expand Down Expand Up @@ -314,3 +315,29 @@ def test_client_reuses_persisted_session_cache_when_local_cache_is_missing(tmp_p
assert second_client.session_reused is True
assert ReusableFakeSession.login_calls == 1
assert store.writes == 2


@pytest.mark.parametrize(
"payload",
[None, {}, {"error": "private-provider-response"}, [None], {"items": [{}, "private-row"]}],
)
def test_order_reads_reject_incomplete_payload_instead_of_empty_success(payload):
client = FirstradeBrokerClient(FirstradeCredentials(username="unused", password="unused"))
client.session = object()
client.account_data = SimpleNamespace(get_orders=lambda *_args, **_kwargs: payload)

for read in (lambda: client.get_orders("test-account"), lambda: client.get_order_status("test-account", "test-order")):
with pytest.raises(FirstradePlatformError) as error:
read()
assert str(error.value) == "Firstrade returned an invalid order response."


@pytest.mark.parametrize("wrapper", [None, "items", "orders", "data", "result"])
@pytest.mark.parametrize("rows", [[], [{"order_id": "test-order", "status": "Submitted"}]])
def test_order_reads_preserve_supported_complete_payloads(wrapper, rows):
payload = rows if wrapper is None else {wrapper: rows}
client = FirstradeBrokerClient(FirstradeCredentials(username="unused", password="unused"))
client.session = object()
client.account_data = SimpleNamespace(get_orders=lambda *_args, **_kwargs: payload)

assert client.get_orders("test-account") == rows