read.markets/tests/test_ticker_validate.py
Giorgio Gilestro ca953e5ea2 ticker-validate: cover failure + side-effect paths
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:43:15 +02:00

137 lines
4.8 KiB
Python

"""Tests for /api/ticker/validate and /api/ticker/historical."""
from __future__ import annotations
from datetime import datetime, timezone
from io import BytesIO
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
def _build_session_factory(tmp_path):
"""Spin up a fresh in-memory schema and return (engine, factory, setup).
Mirrors tests/test_llm_csv_parser.py / tests/test_referral_conversion.py."""
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app import db as db_mod
from app.db import Base
import app.models # noqa: F401
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/tv.db")
factory = async_sessionmaker(engine, expire_on_commit=False)
db_mod._engine = engine
db_mod._session_factory = factory
async def _setup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
return engine, factory, _setup
@pytest.mark.asyncio
async def test_validate_happy_path(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 successful quote.
async def _fake_yahoo(client, symbol, label, note, anchor=None):
return Quote(
symbol=symbol, source="yahoo", label=label, note=note,
price=172.40, currency="USD", as_of="2026-05-27", changes={},
)
monkeypatch.setattr(mod, "fetch_yahoo", _fake_yahoo)
# Avoid the MySQL-only upsert on SQLite.
async def _fake_upsert(session, tickers):
return len(list(tickers))
monkeypatch.setattr(mod, "upsert_tickers", _fake_upsert)
async with factory() as session:
result = await validate_ticker(symbol="aapl", session=session)
assert result["ok"] is True
assert result["symbol"] == "AAPL"
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"