"""Shared pytest fixtures. The tests never touch the real 350 GB lake: `tests/fixtures/make_fixtures.py` builds a tiny synthetic Parquet lake (same layout as frd_downloader.py) in a temp directory, and the API is imported with HFMD_DATA_ROOT pointing at it. Redis is replaced by fakeredis; the SQLite state DB lives in the temp dir. """ from __future__ import annotations import importlib import os import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] API_DIR = ROOT / "hfmarketdata" / "api" if str(API_DIR) not in sys.path: sys.path.insert(0, str(API_DIR)) @pytest.fixture(scope="session") def lake(tmp_path_factory) -> Path: from tests.fixtures.make_fixtures import build_lake root = tmp_path_factory.mktemp("lake") build_lake(root) return root @pytest.fixture(scope="session") def app(lake): os.environ["HFMD_DATA_ROOT"] = str(lake) os.environ["HFMD_STATE_DB"] = str(lake / "state" / "hfmd.db") os.environ["HFMD_WEB_DIST"] = str(lake / "no-web") os.environ["HFMD_ENV"] = "test" os.environ["HFMD_REDIS_URL"] = "fakeredis://" os.environ["HFMD_SECRET_KEY"] = "test-secret" os.environ["HFMD_KEY_SALT"] = "test-salt" for m in [m for m in list(sys.modules) if m.split(".")[0] in ("core", "main", "futures", "accounts", "ratelimit", "fundamentals", "bulk", "stream", "openapi")]: del sys.modules[m] main = importlib.import_module("main") from tests.fixtures.v2_test_routes import install as _install_probes _install_probes(main.app) # /v1/_test/* probes for the rate-limit contract (test app only) return main.app @pytest.fixture(scope="session") def client(app): from fastapi.testclient import TestClient with TestClient(app) as c: yield c @pytest.fixture(autouse=True) def _fresh_counters(): """Every test starts with empty rate-limit / usage counters and an empty key cache (fakeredis only).""" yield if "ratelimit.redis_limiter" in sys.modules: sys.modules["ratelimit.redis_limiter"].reset_for_tests() if "ratelimit.middleware" in sys.modules: sys.modules["ratelimit.middleware"].invalidate_key_cache() @pytest.fixture def make_user(app): """Create an active, verified user with one active API key. Returns (user_id, raw_key, email, password).""" from accounts import service from core.db import session counter = {"n": 0} def _make(email: str | None = None, *, tier: str = "free", role: str = "user", password: str = "correct-horse-battery", with_key: bool = True): counter["n"] += 1 import uuid email = email or f"user{counter['n']}-{os.getpid()}-{uuid.uuid4().hex[:10]}@example.com" # id(counter) collided with session() as s: u = service.create_user(s, email, "Test User", password=password, tier=tier, role=role, status="active", actor="test") u.email_verified_at = service.now() raw = None if with_key: raw, _ = service.create_key(s, u, "default", actor="test", notify=False) uid = u.id return uid, raw, email, password return _make @pytest.fixture def web(app): """Fresh TestClient (own cookie jar) for browser-session flows. Not shared with `client`.""" from fastapi.testclient import TestClient with TestClient(app) as c: yield c @pytest.fixture def signin(app): """signin(client, email, password) → logs a TestClient in through POST /v1/auth/login (cookie session).""" def _signin(c, email: str, password: str): r = c.post("/v1/auth/login", json={"email": email, "password": password}, headers={"Content-Type": "application/json"}) assert r.status_code == 200, r.text return r.json()["data"] return _signin @pytest.fixture def admin_web(app, make_user, signin): """TestClient signed in (cookie) as a fresh admin. Returns (client, user_id, email, password).""" from fastapi.testclient import TestClient uid, _, email, pw = make_user(role="admin", with_key=False) with TestClient(app) as c: signin(c, email, pw) yield c, uid, email, pw @pytest.fixture def outbox(app, monkeypatch): """Capture every e-mail queued by the accounts service (provider considered configured, nothing sent).""" from dataclasses import replace from accounts import mailer sent: list = [] monkeypatch.setattr(mailer, "settings", replace(mailer.settings, resend_api_key="re_test_capture")) monkeypatch.setattr(mailer, "dispatch", lambda msg: sent.append(msg)) return sent @pytest.fixture def client_hu(app, make_user): """Client authenticated with a high_usage API key (600 req/min, 200 000 rows/request) — for data-heavy tests.""" from fastapi.testclient import TestClient _, raw, _, _ = make_user(tier="high_usage") with TestClient(app, headers={"Authorization": f"Bearer {raw}"}) as c: yield c # ----------------------------------------------------------------------------------------- fundamentals EDGAR_FIXTURES = ROOT / "tests" / "fixtures" / "edgar" PROTO_CIKS = {"AAPL": 320193, "MSFT": 789019, "SHAK": 1620533} def load_gz(name: str): import gzip import json with gzip.open(EDGAR_FIXTURES / name, "rt", encoding="utf-8") as fh: return json.load(fh) @pytest.fixture(scope="session") def edgar_mock(app): """respx router serving the recorded EDGAR fixtures — the tests never reach sec.gov.""" import respx from fundamentals import edgar_client as ec router = respx.mock(assert_all_called=False, assert_all_mocked=True) router.get(ec.URL_COMPANY_TICKERS).respond(json=load_gz("company_tickers.json.gz")) router.get(ec.URL_COMPANY_TICKERS_EXCHANGE).respond(json=load_gz("company_tickers_exchange.json.gz")) for cik in PROTO_CIKS.values(): router.get(f"{ec.DATA_BASE}/api/xbrl/companyfacts/CIK{cik:010d}.json").respond(json=load_gz(f"companyfacts_CIK{cik:010d}.json.gz")) router.get(f"{ec.DATA_BASE}/submissions/CIK{cik:010d}.json").respond(json=load_gz(f"submissions_CIK{cik:010d}.json.gz")) router.get(url__regex=r"https://data\.sec\.gov/api/xbrl/companyfacts/CIK\d+\.json").respond(404) router.get(url__regex=r"https://data\.sec\.gov/submissions/CIK\d+\.json").respond(404) router.get(url__regex=r"https://www\.sec\.gov/Archives/edgar/data/1620533/000162053326000018/MetaLinks\.json").respond( json=load_gz("metalinks_CIK0001620533_000162053326000018.json.gz")) router.get(url__regex=r"https://www\.sec\.gov/Archives/edgar/data/.*/MetaLinks\.json").respond(404) router.get(url__regex=r"https://www\.sec\.gov/cgi-bin/browse-edgar.*").respond(text=(EDGAR_FIXTURES / "atom_10-Q.xml").read_text()) router.get(url__regex=r"https://www\.sec\.gov/Archives/edgar/daily-index/.*").respond(text=(EDGAR_FIXTURES / "master.idx").read_text()) with router: yield router @pytest.fixture(scope="session") def fundamentals_data(app, lake, edgar_mock): """Universe + full ingestion of the three prototype companies into the test lake / state DB.""" from fundamentals import edgar_client, ingest client = edgar_client.EdgarClient(rate=10_000, raw_dir=lake / "edgar" / "raw") universe = ingest.sync_universe(client) results = {tk: ingest.ingest_company(client, cik) for tk, cik in PROTO_CIKS.items()} for tk, r in results.items(): assert r.error is None, f"{tk}: {r.error}" ingest._set_state("backfill", companies_total=len(PROTO_CIKS), companies_done=len(PROTO_CIKS)) ingest._update_mapping_failure_rate() return {"universe": universe, "results": results, "client": client}