From ca953e5ea28a8a7d64cc5ebbaff7acad74680c4a Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Wed, 27 May 2026 14:43:15 +0200 Subject: [PATCH] ticker-validate: cover failure + side-effect paths Co-Authored-By: Claude Opus 4.7 --- tests/test_ticker_validate.py | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/test_ticker_validate.py b/tests/test_ticker_validate.py index 77bb7bc..6ccfbb2 100644 --- a/tests/test_ticker_validate.py +++ b/tests/test_ticker_validate.py @@ -60,3 +60,78 @@ async def test_validate_happy_path(tmp_path, monkeypatch): assert result["price"] == 172.40 assert result["currency"] == "USD" assert result["as_of"] == "2026-05-27" + + +@pytest.mark.asyncio +async def test_validate_unknown_symbol(tmp_path, monkeypatch): + from app.routers.ticker_validate import validate_ticker + from app.services.market import Quote + import app.routers.ticker_validate as mod + + _, factory, setup = _build_session_factory(tmp_path) + await setup() + + # Mock fetch_yahoo to return a Quote with error and no price. + async def _fake_yahoo(client, symbol, label, note, anchor=None): + return Quote(symbol=symbol, source="yahoo", label=label, note=note, + price=None, currency=None, as_of=None, + error="empty result") + monkeypatch.setattr(mod, "fetch_yahoo", _fake_yahoo) + + async with factory() as session: + result = await validate_ticker(symbol="XYZNOTREAL", session=session) + + assert result["ok"] is False + assert "not recognised" in result["error"].lower() + + +@pytest.mark.asyncio +async def test_validate_empty_symbol_rejects(): + from app.routers.ticker_validate import validate_ticker + + # Direct call — no session needed because we short-circuit before any DB use. + result = await validate_ticker(symbol=" ", session=None) + assert result["ok"] is False + assert "required" in result["error"].lower() + + +@pytest.mark.asyncio +async def test_validate_seeds_universe_and_quote(tmp_path, monkeypatch): + """Side-effect check: on success, the symbol is upserted into the + universe and a Quote row is written.""" + from sqlalchemy import select + + from app.models import Quote as QuoteModel + from app.routers.ticker_validate import validate_ticker + from app.services.market import Quote + import app.routers.ticker_validate as mod + + _, factory, setup = _build_session_factory(tmp_path) + await setup() + + upsert_calls: list[list[str]] = [] + + async def _fake_yahoo(client, symbol, label, note, anchor=None): + return Quote(symbol=symbol, source="yahoo", label=label, note=note, + price=100.0, currency="USD", as_of="2026-05-27", changes={}) + monkeypatch.setattr(mod, "fetch_yahoo", _fake_yahoo) + + async def _fake_upsert(session, tickers): + upsert_calls.append(list(tickers)) + return len(list(tickers)) + monkeypatch.setattr(mod, "upsert_tickers", _fake_upsert) + + async with factory() as session: + result = await validate_ticker(symbol="MSFT", session=session) + + assert result["ok"] is True + assert upsert_calls == [["MSFT"]] + + # Quote row was written. + async with factory() as session: + rows = (await session.execute( + select(QuoteModel).where(QuoteModel.symbol == "MSFT") + )).scalars().all() + assert len(rows) == 1 + assert rows[0].price == 100.0 + assert rows[0].currency == "USD"