Stock-Tool/tests/test_network.py

151 lines
5.4 KiB
Python

"""
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"]