""" Network integration tests — requires internet access and live API. NOT run automatically on every edit (slow, rate-limited). Run manually before pushing an update: python -m pytest tests/test_network.py -v Or run with the fast tests excluded: python -m pytest tests/ -v -m "not network" """ import sys from pathlib import Path import pytest import requests sys.path.insert(0, str(Path(__file__).parent.parent / "dist")) API_BASE = "https://api.verimundsolutions.com" EDGAR_BASE = "https://data.sec.gov" EDGAR_UA = {"User-Agent": "Verimund Solutions support@verimundsolutions.com"} pytestmark = pytest.mark.network # --------------------------------------------------------------------------- # API health checks # --------------------------------------------------------------------------- class TestAPIHealth: def test_update_endpoint_alive(self): """GET /update should respond (not timeout, not 500).""" r = requests.get(f"{API_BASE}/update", timeout=10) assert r.status_code != 500, f"/update returned 500: {r.text}" assert r.status_code in (200, 400, 401, 422), ( f"Unexpected status from /update: {r.status_code}" ) def test_versions_endpoint_alive(self): """GET /admin/versions returns 401 without auth (not 500 or timeout).""" r = requests.get(f"{API_BASE}/admin/versions", timeout=10) assert r.status_code in (200, 401, 403), ( f"/admin/versions returned unexpected status: {r.status_code}" ) def test_auth_endpoint_rejects_bad_request(self): """POST /auth with garbage data returns 400/422, not 500.""" r = requests.post( f"{API_BASE}/auth", json={"license_key": "bad", "hwid": "bad", "timestamp": "0", "signature": "bad"}, timeout=10, ) assert r.status_code in (400, 401, 422), ( f"/auth returned unexpected status: {r.status_code}" ) def test_no_500_errors_on_any_public_endpoint(self): endpoints = ["/update", "/admin/versions"] for ep in endpoints: r = requests.get(f"{API_BASE}{ep}", timeout=10) assert r.status_code != 500, f"{ep} returned 500: {r.text}" # --------------------------------------------------------------------------- # yfinance field checks # --------------------------------------------------------------------------- class TestYFinanceFields: """ Verify that yfinance still returns the fields our scoring depends on. Uses AAPL as a known-stable ticker. If these fail, yfinance has renamed a field we use — scoring will silently return neutral 50 for affected metrics. """ @pytest.fixture(scope="class") def aapl_info(self): import yfinance as yf return yf.Ticker("AAPL").info REQUIRED_FIELDS = [ "trailingPE", "forwardPE", "priceToBook", "returnOnAssets", "returnOnEquity", "currentRatio", "debtToEquity", "revenueGrowth", "earningsGrowth", "shortPercentOfFloat", "recommendationMean", "targetMeanPrice", "currentPrice", "marketCap", ] def test_required_fields_present(self, aapl_info): missing = [f for f in self.REQUIRED_FIELDS if f not in aapl_info] assert not missing, ( f"yfinance dropped fields we depend on: {missing}\n" f"Update scoring functions or field mappings." ) def test_de_is_percentage_scale(self, aapl_info): """ yfinance returns debtToEquity as percentage (e.g. 150 not 1.5). Our code divides by 100. If this breaks, D/E scoring silently becomes wrong. """ de = aapl_info.get("debtToEquity") if de is not None: assert de > 1.0, ( f"debtToEquity={de} looks like a ratio, not percentage. " f"Remove the /100.0 normalization in stock_screener.py line ~1356." ) def test_price_history_available(self): import yfinance as yf hist = yf.download("AAPL", period="1mo", progress=False, auto_adjust=True) assert len(hist) > 10, "yfinance price history returned fewer than 10 bars for AAPL" # --------------------------------------------------------------------------- # EDGAR API checks # --------------------------------------------------------------------------- class TestEDGARAPI: def test_frames_endpoint_alive(self): """EDGAR XBRL frames API returns valid data for a known concept/period.""" r = requests.get( f"{EDGAR_BASE}/api/xbrl/frames/us-gaap/Revenues/USD/CY2023Q4.json", headers=EDGAR_UA, timeout=20, ) assert r.status_code == 200, f"EDGAR frames API returned {r.status_code}" data = r.json() assert "data" in data, "EDGAR response missing 'data' key" assert len(data["data"]) > 100, "EDGAR returned suspiciously few companies" def test_company_facts_endpoint_alive(self): """EDGAR company facts for Apple (CIK 320193) returns valid data.""" r = requests.get( f"{EDGAR_BASE}/api/xbrl/companyfacts/CIK0000320193.json", headers=EDGAR_UA, timeout=20, ) assert r.status_code == 200, f"EDGAR company facts returned {r.status_code}" data = r.json() assert "facts" in data assert "us-gaap" in data["facts"]