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