TerraFin / tests /interface /test_dashboard_data_api.py
sk851's picture
fix(tests): add missing capability stubs and fix mock targets
89e5718
from fastapi.testclient import TestClient
import TerraFin.data.cache.manager as cache_manager_module
import TerraFin.interface.watchlist_service as watchlist_service_module
from TerraFin.data.cache.registry import reset_cache_manager
from TerraFin.data.providers.private_access import PRIVATE_SERIES, clear_private_series_cache
from TerraFin.data.providers.private_access.client import PrivateAccessClient
from TerraFin.interface.server import create_app
from TerraFin.interface.watchlist_service import reset_watchlist_service
def _assert_watchlist_item_shape(item: dict) -> None:
assert set(item) == {"symbol", "name", "move", "tags"}
assert isinstance(item["symbol"], str)
assert isinstance(item["name"], str)
assert isinstance(item["move"], str)
assert isinstance(item["tags"], list)
def _assert_breadth_metric_shape(metric: dict) -> None:
assert set(metric) == {"label", "value", "tone"}
assert isinstance(metric["label"], str)
assert isinstance(metric["value"], str)
assert isinstance(metric["tone"], str)
def _reset_services() -> None:
reset_cache_manager()
reset_watchlist_service()
def test_dashboard_data_uses_fallback_when_private_source_unconfigured(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
monkeypatch.delenv("TERRAFIN_MONGODB_URI", raising=False)
monkeypatch.delenv("MONGODB_URI", raising=False)
_reset_services()
client = TestClient(create_app())
watchlist_response = client.get("/dashboard/api/watchlist")
assert watchlist_response.status_code == 200
watchlist_payload = watchlist_response.json()
assert isinstance(watchlist_payload["items"], list)
assert watchlist_payload["backendConfigured"] is False
assert watchlist_payload["mode"] == "fallback"
assert len(watchlist_payload["items"]) >= 7
_assert_watchlist_item_shape(watchlist_payload["items"][0])
breadth_response = client.get("/dashboard/api/market-breadth")
assert breadth_response.status_code == 200
breadth_payload = breadth_response.json()
assert isinstance(breadth_payload["metrics"], list)
assert len(breadth_payload["metrics"]) >= 1
_assert_breadth_metric_shape(breadth_payload["metrics"][0])
def test_dashboard_market_breadth_uses_private_source_when_available(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
def _mock_panel(self, resource):
_ = self
assert resource == "market-breadth"
return {"metrics": [{"label": "Advancers", "value": "500", "tone": "#047857"}]}
monkeypatch.setattr(PrivateAccessClient, "fetch_panel", _mock_panel)
_reset_services()
client = TestClient(create_app())
breadth_payload = client.get("/dashboard/api/market-breadth").json()
_assert_breadth_metric_shape(breadth_payload["metrics"][0])
assert breadth_payload["metrics"][0]["value"] == "500"
def test_dashboard_fear_greed_falls_back_to_cached_history_when_current_misses(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
_reset_services()
clear_private_series_cache(PRIVATE_SERIES["fear_greed"])
def _mock_history(self, key):
_ = self, key
return [
{"time": "2026-01-01", "close": 25},
{"time": "2026-01-10", "close": 40},
{"time": "2026-01-28", "close": 62},
{"time": "2026-02-01", "close": 70},
]
def _mock_current(self, key):
_ = self, key
return {
"name": "Fear & Greed",
"value": 70,
"as_of": "2026-02-01",
"rating": "Greed",
"metadata": {
"score": 70,
"rating": "Greed",
"timestamp": "2026-02-01",
"previous_close": 62,
"previous_1_week": 40,
"previous_1_month": 25,
},
}
monkeypatch.setattr(PrivateAccessClient, "fetch_series_history", _mock_history)
monkeypatch.setattr(PrivateAccessClient, "fetch_series_current", _mock_current)
client = TestClient(create_app())
payload = client.get("/dashboard/api/fear-greed").json()
assert payload["score"] == 70
assert payload["rating"] == "Greed"
assert payload["previous_close"] == 62
assert payload["previous_1_week"] == 40
assert payload["previous_1_month"] == 25
def test_dashboard_cape_falls_back_to_series_history_when_current_misses(monkeypatch, tmp_path) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
_reset_services()
clear_private_series_cache(PRIVATE_SERIES["cape"])
def _mock_history(self, key):
_ = self, key
return [
{"time": "2025-12-01", "close": 29.4},
{"time": "2026-01-01", "close": 31.1},
]
def _mock_current(self, key):
_ = self, key
return {
"name": "CAPE",
"value": 31.1,
"as_of": "2026-01-01",
"metadata": {"date": "2026-01", "cape": 31.1},
}
monkeypatch.setattr(PrivateAccessClient, "fetch_series_history", _mock_history)
monkeypatch.setattr(PrivateAccessClient, "fetch_series_current", _mock_current)
client = TestClient(create_app())
payload = client.get("/dashboard/api/cape").json()
assert payload["date"] == "2026-01"
assert payload["cape"] == 31.1
def test_dashboard_watchlist_crud(monkeypatch, tmp_path) -> None:
storage: dict[str, dict] = {}
class _FakeCollection:
def find_one(self, query):
return storage.get(query["_id"])
def update_one(self, query, update, upsert=False):
_ = upsert
storage[query["_id"]] = {"_id": query["_id"], **update["$set"]}
class _FakeDatabase:
def __getitem__(self, name):
_ = name
return _FakeCollection()
class _FakeMongoClient:
def __init__(self, uri, serverSelectionTimeoutMS=2000):
self.uri = uri
_ = serverSelectionTimeoutMS
def __getitem__(self, name):
_ = name
return _FakeDatabase()
monkeypatch.setenv("TERRAFIN_MONGODB_URI", "mongodb://example.test")
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
import TerraFin.data.providers.market.ticker_info as _ticker_info_module
monkeypatch.setattr(_ticker_info_module, "get_ticker_info", lambda symbol: {"currentPrice": 102.0, "previousClose": 100.0})
monkeypatch.setattr(watchlist_service_module, "_resolve_company_name", lambda symbol: f"{symbol} Holdings")
monkeypatch.setattr(watchlist_service_module, "MongoClient", _FakeMongoClient)
_reset_services()
client = TestClient(create_app())
initial = client.get("/dashboard/api/watchlist")
assert initial.status_code == 200
assert initial.json()["items"] == []
assert initial.json()["backendConfigured"] is True
assert initial.json()["mode"] == "mongo"
assert storage["terrafin_watchlist"] == {
"_id": "terrafin_watchlist",
"Company List": [],
"items": [],
"explicit_groups": [],
"group_order": [],
"item_order": {},
}
created = client.post("/dashboard/api/watchlist", json={"symbol": "meta"})
assert created.status_code == 200
created_payload = created.json()
assert created_payload["backendConfigured"] is True
assert created_payload["mode"] == "mongo"
assert created_payload["items"] == [{"symbol": "META", "name": "META Holdings", "move": "+2.00%", "tags": []}]
duplicate = client.post("/dashboard/api/watchlist", json={"symbol": "META"})
assert duplicate.status_code == 409
removed = client.delete("/dashboard/api/watchlist/META")
assert removed.status_code == 200
assert removed.json()["items"] == []
def test_dashboard_watchlist_falls_back_when_mongo_backend_is_unreachable(monkeypatch, tmp_path) -> None:
class _UnavailableCollection:
def find_one(self, query):
_ = query
raise RuntimeError("mongo unavailable")
def update_one(self, query, update, upsert=False):
_ = query, update, upsert
raise RuntimeError("mongo unavailable")
class _UnavailableDatabase:
def __getitem__(self, name):
_ = name
return _UnavailableCollection()
class _UnavailableMongoClient:
def __init__(self, uri, serverSelectionTimeoutMS=2000):
_ = uri, serverSelectionTimeoutMS
def __getitem__(self, name):
_ = name
return _UnavailableDatabase()
monkeypatch.setenv("TERRAFIN_MONGODB_URI", "mongodb://unavailable.test")
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
monkeypatch.setattr(watchlist_service_module, "MongoClient", _UnavailableMongoClient)
_reset_services()
client = TestClient(create_app())
response = client.get("/dashboard/api/watchlist")
assert response.status_code == 200
payload = response.json()
assert payload["backendConfigured"] is False
assert payload["mode"] == "fallback"
assert len(payload["items"]) >= 1
def test_dashboard_cache_status_endpoint(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
_reset_services()
client = TestClient(create_app())
response = client.get("/dashboard/api/cache-status")
assert response.status_code == 200
body = response.json()
assert "sources" in body
assert isinstance(body["sources"], list)
assert len(body["sources"]) >= 3
first = body["sources"][0]
assert {
"source",
"mode",
"intervalSeconds",
"enabled",
"lastRunAt",
"lastSuccessAt",
"lastError",
}.issubset(first.keys())
assert any(item["source"] == "portfolio.cache" for item in body["sources"])
def test_watchlist_page_route_serves_frontend(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
_reset_services()
client = TestClient(create_app())
response = client.get("/watchlist")
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/html")
def test_dashboard_cache_refresh_endpoint(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(cache_manager_module, "_FILE_CACHE_DIR", tmp_path)
_reset_services()
client = TestClient(create_app())
response = client.post("/dashboard/api/cache-refresh?force=true")
assert response.status_code == 200
body = response.json()
assert body["ok"] is True
assert body["force"] is True
assert isinstance(body["sources"], list)
assert len(body["sources"]) >= 3