Pin InvestEngine parser failure modes — empty-on-junk + partial-match

Context: The port's graceful-failure contract was implicit in the way
each strategy returns None/[] on malformed input, but without tests it
was an accidental property that could regress silently. Codify it.

Two invariants, each backed by a fixture:

1. Junk email → empty list, never raise.
   `unparseable.eml` is a pure-marketing IE newsletter with no order
   data. All three strategies try and fail; parse_invest_engine_email
   returns []. No exception leaks.

2. Partial HTML email → intact orders only.
   `html_partial_match.eml` has two nested summary tables: one with a
   valid VUAG order, one that is missing both the ticker and "Bought N
   @ £P" rows (simulates IE dropping content mid-render). The parser
   returns just the VUAG order.

No implementation change needed — the behaviour existed as a side
effect of _try_html_summary_table returning None on missing fields.
These tests lock it down so future refactors can't quietly break it.

Test plan:
  poetry run pytest tests/providers/parsers/ -q   →  8 passed in 0.19s
  poetry run mypy broker_sync/providers/parsers/invest_engine.py tests/providers/parsers/test_invest_engine.py   →  clean
  poetry run ruff check broker_sync/providers/parsers/invest_engine.py tests/providers/parsers/test_invest_engine.py   →  All checks passed!
  poetry run yapf --diff   →  clean (no diff)

Manual verification:
- Load unparseable.eml → parse returns [].
- Load html_partial_match.eml → parse returns exactly 1 activity (VUAG).
This commit is contained in:
Viktor Barzin 2026-04-17 22:02:48 +00:00
parent 020ba16723
commit 87526898e6
3 changed files with 72 additions and 0 deletions

View file

@ -89,3 +89,20 @@ def test_csv_attachment_parses_all_rows() -> None:
assert a.account_type is AccountType.ISA
assert a.notes is not None
assert "csv" in a.notes
# -- graceful failure modes --
def test_unparseable_email_returns_empty_list() -> None:
assert parse_invest_engine_email(_load("unparseable.eml")) == []
def test_html_partial_match_returns_only_parseable_orders() -> None:
activities = parse_invest_engine_email(_load("html_partial_match.eml"))
assert len(activities) == 1
a = activities[0]
assert a.symbol == "VUAG"
assert a.quantity == Decimal("3.0")
assert a.unit_price == Decimal("61.25")
assert a.date == datetime(2026, 4, 15)