"""Unit tests for ``openbb_sec.utils.helpers``.""" import asyncio from unittest.mock import patch import pytest from openbb_core.app.model.abstract.error import OpenBBError from openbb_sec.utils import helpers def test_sec_callback_text_fallback(): """sec_callback returns plain text when Content-Type is neither json/html.""" class _Resp: headers = {"Content-Type": "application/octet-stream"} async def json(self): return {"unused": True} async def text(self, encoding=None): return f"plain:{encoding}" out = asyncio.run(helpers.sec_callback(_Resp(), None)) assert out == "plain:None" def test_sec_callback_json_and_html(): """sec_callback dispatches on the json and html content types.""" class _Json: headers = {"Content-Type": "application/json; charset=utf-8"} async def json(self): return {"ok": 1} async def text(self, encoding=None): return "x" class _Html: headers = {"Content-Type": "text/html"} async def json(self): return {} async def text(self, encoding=None): return f"html:{encoding}" assert asyncio.run(helpers.sec_callback(_Json(), None)) == {"ok": 1} assert asyncio.run(helpers.sec_callback(_Html(), None)) == "html:latin-1" def test_get_all_companies_invalid_response(): """An empty/invalid response raises OpenBBError.""" async def _fake(url, **kwargs): return None with patch.object(helpers, "cached_request", _fake): with pytest.raises(OpenBBError, match="Empty or invalid"): asyncio.run(helpers.get_all_companies()) def test_get_all_companies_unexpected_columns(): """A response whose row width != 3 raises a format error.""" async def _fake(url, **kwargs): return {"0": {"cik_str": 320193, "ticker": "AAPL"}} with patch.object(helpers, "cached_request", _fake): with pytest.raises(OpenBBError, match="Unexpected SEC response format"): asyncio.run(helpers.get_all_companies()) def test_get_all_companies_ok(): """A well-formed response yields a 3-column string DataFrame.""" async def _fake(url, **kwargs): return { "0": {"cik_str": 320193, "ticker": "AAPL", "title": "Apple Inc."}, "1": {"cik_str": 789019, "ticker": "MSFT", "title": "Microsoft"}, } with patch.object(helpers, "cached_request", _fake): df = asyncio.run(helpers.get_all_companies()) assert list(df.columns) == ["cik", "symbol", "name"] assert df.loc["0", "symbol"] == "AAPL" assert df.loc["0", "cik"] == "320193" def _ciks_frame(): """Patch get_all_ciks to a small institutions frame.""" async def _fake(use_cache=True): from pandas import DataFrame return DataFrame( { "Institution": ["VANGUARD GROUP INC", "BLACKROCK INC", "ACME LLC"], "CIK Number": ["0000102909", "0001364742", "0000999999"], } ) return _fake def test_search_institutions_case_insensitive(): """search_institutions filters by case-insensitive substring.""" with patch.object(helpers, "get_all_ciks", _ciks_frame()): out = asyncio.run(helpers.search_institutions("blackrock")) assert len(out) == 1 assert out.iloc[0]["CIK Number"] == "0001364742" def _companies_frame(): from pandas import DataFrame return DataFrame( { "cik": ["320193", "789019"], "symbol": ["AAPL", "MSFT"], "name": ["Apple Inc.", "Microsoft"], } ) def _mf_frame(): from pandas import DataFrame return DataFrame( { "cik": ["36405"], "seriesId": ["S000002277"], "classId": ["C000005942"], "symbol": ["VFINX"], } ) def test_symbol_map_company_hit(): """A ticker present in the company list zero-pads to a 10-digit CIK.""" async def _companies(use_cache=True): return _companies_frame() with patch.object(helpers, "get_all_companies", _companies): cik = asyncio.run(helpers.symbol_map("aapl")) assert cik == "0000320193" def test_symbol_map_fund_fallback(): """A ticker absent from companies falls back to the MF/ETF map.""" async def _companies(use_cache=True): return _companies_frame() async def _mf(use_cache=True): return _mf_frame() with ( patch.object(helpers, "get_all_companies", _companies), patch.object(helpers, "get_mf_and_etf_map", _mf), ): cik = asyncio.run(helpers.symbol_map("VFINX")) assert cik == "0000036405" def test_symbol_map_not_found_returns_empty(): """A ticker in neither list returns an empty string.""" async def _companies(use_cache=True): return _companies_frame() async def _mf(use_cache=True): return _mf_frame() with ( patch.object(helpers, "get_all_companies", _companies), patch.object(helpers, "get_mf_and_etf_map", _mf), ): assert asyncio.run(helpers.symbol_map("NOPE")) == "" def test_cik_map_hit_and_miss(): """cik_map resolves an integer CIK to a ticker, else returns an error string.""" async def _companies(use_cache=True): return _companies_frame() with patch.object(helpers, "get_all_companies", _companies): assert asyncio.run(helpers.cik_map(320193)) == "AAPL" msg = asyncio.run(helpers.cik_map("0000000001")) assert "does not have a unique ticker" in msg def test_get_schema_filelist(): """get_schema_filelist parses the 'Name' column of an HTML table.""" html = ( "" "" "" "
Name
Parent Directory
elts/
us-gaap-2024.xsd
" ) with patch.object(helpers, "cached_text", return_value=html): out = helpers.get_schema_filelist(query="elts") assert out[0] == "https://xbrl.fasb.org/us-gaap/elts/" assert "us-gaap-2024.xsd" in out def test_get_schema_filelist_explicit_url_no_query(): """With an explicit url and no query the first cell becomes that url.""" html = "
Name
Parent Directory
a.xsd
" with patch.object(helpers, "cached_text", return_value=html): out = helpers.get_schema_filelist(url="https://example.com/dir") assert out[0] == "https://example.com/dir" assert out[1] == "a.xsd" def _ftd_zip_bytes(rows: str) -> bytes: """Build an in-memory pipe-delimited zip the way the SEC serves FTD data.""" import io import zipfile buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr("ftd.txt", rows) return buf.getvalue() def test_download_zip_file_zip_branch_and_settlement(): """The ZipFile fallback branch renames SETTLEMENT DATE columns.""" rows = ( "SETTLEMENT DATE|CUSIP|SYMBOL|QUANTITY (FAILS)|DESCRIPTION|PRICE\n" "20230103|000000000|AAPL|100|APPLE INC|130.5\n" "20230103|000000001|MSFT|200|MICROSOFT|UNAVAILABLE\n" "summary line one\n" "summary line two\n" ) zip_bytes = _ftd_zip_bytes(rows) async def _fake(url, **kwargs): return zip_bytes real_read_csv = __import__("pandas").read_csv calls = {"n": 0} def _read_csv(*args, **kwargs): calls["n"] += 1 if calls["n"] == 1 and kwargs.get("compression") == "zip": raise ValueError("forced fallback") return real_read_csv(*args, **kwargs) with ( patch.object(helpers, "cached_request", _fake), patch("pandas.read_csv", _read_csv), ): out = asyncio.run(helpers.download_zip_file("http://x/ftd.zip", symbol="AAPL")) assert isinstance(out, list) assert len(out) == 1 rec = out[0] assert rec["symbol"] == "AAPL" assert "cusip" in rec assert str(rec["date"]) == "2023-01-03" assert rec["price"] == 130.5 def test_download_zip_file_direct_read_branch(): """A single-member zip is parsed directly by read_csv.""" rows = ( "SETTLEMENT DATE|CUSIP|SYMBOL|QUANTITY (FAILS)|DESCRIPTION|PRICE\n" "20230103|000000000|AAPL|100|APPLE INC|130.5\n" "20230104|000000001|MSFT|150|MICROSOFT|UNAVAILABLE\n" "summary one\n" "summary two\n" ) zip_bytes = _ftd_zip_bytes(rows) async def _fake(url, **kwargs): return zip_bytes with patch.object(helpers, "cached_request", _fake): out = asyncio.run(helpers.download_zip_file("http://x/ftd.zip", symbol="AAPL")) assert len(out) == 1 rec = out[0] assert rec["symbol"] == "AAPL" assert "cusip" in rec assert str(rec["date"]) == "2023-01-03" assert rec["price"] == 130.5 def test_get_series_id_requires_input(): """get_series_id raises when neither symbol nor cik is supplied.""" with pytest.raises(OpenBBError, match="Either symbol or cik"): asyncio.run(helpers.get_series_id()) def test_get_nport_candidates_empty_frame_falls_back_to_symbol_map(): """An empty series-id frame falls back to symbol_map and raises not-found.""" from pandas import DataFrame async def _series(symbol, use_cache=True): return DataFrame({"seriesId": []}) async def _symbol_map(symbol, use_cache=True): return "" with ( patch.object(helpers, "get_series_id", _series), patch.object(helpers, "symbol_map", _symbol_map), ): with pytest.raises(OpenBBError, match="Fund not found"): asyncio.run(helpers.get_nport_candidates("BADFUND")) def test_get_nport_candidates_empty_series_raises(): """A blank series id raises the fund-not-found error before any request.""" async def _series(symbol, use_cache=True): return None async def _symbol_map(symbol, use_cache=True): return "" with ( patch.object(helpers, "get_series_id", _series), patch.object(helpers, "symbol_map", _symbol_map), ): with pytest.raises(OpenBBError, match="Fund not found"): asyncio.run(helpers.get_nport_candidates("BADFUND")) def test_get_all_ciks(): """get_all_ciks parses the colon-delimited CIK lookup file.""" class _Resp: async def text(self, encoding=None): return "APPLE INC:0000320193:\nMICROSOFT CORP:0000789019:\n" async def _fake(url, response_callback=None, **kwargs): return await response_callback(_Resp(), None) with patch.object(helpers, "cached_request", _fake): df = asyncio.run(helpers.get_all_ciks()) assert list(df.columns) == ["Institution", "CIK Number"] assert (df["Institution"] == "APPLE INC").any() def test_get_mf_and_etf_map(): """get_mf_and_etf_map builds a frame from the fields/data payload.""" async def _fake(url, **kwargs): return { "fields": ["cik", "seriesId", "classId", "symbol"], "data": [["36405", "S000002277", "C000005942", "VFINX"]], } with patch.object(helpers, "cached_request", _fake): df = asyncio.run(helpers.get_mf_and_etf_map()) assert list(df.columns) == ["cik", "seriesId", "classId", "symbol"] assert df.iloc[0]["symbol"] == "VFINX" def test_get_ftd_urls_found(): """get_ftd_urls extracts download URLs from the SEC data.json catalog.""" async def _fake(url, **kwargs): return { "dataset": [ {"title": "Other"}, { "title": "Fails-to-Deliver Data", "distribution": [ {"downloadURL": "https://x/cnsfails202301a.zip"}, {"noUrl": True}, ], }, ] } with patch.object(helpers, "amake_request", _fake): out = asyncio.run(helpers.get_ftd_urls()) assert list(out.values()) == ["https://x/cnsfails202301a.zip"] def test_get_ftd_urls_not_found(): """A catalog without the FTD entry yields an empty mapping.""" async def _fake(url, **kwargs): return {"dataset": [{"title": "Other"}]} with patch.object(helpers, "amake_request", _fake): assert asyncio.run(helpers.get_ftd_urls()) == {} def test_get_series_id_symbol_match(): """get_series_id returns the row matching an exact symbol.""" async def _mf(use_cache=True): return _mf_frame() with patch.object(helpers, "get_mf_and_etf_map", _mf): out = asyncio.run(helpers.get_series_id(symbol="VFINX")) assert out.iloc[0]["seriesId"] == "S000002277" def test_get_series_id_cik_match(): """get_series_id resolves by CIK when no symbol is given.""" async def _mf(use_cache=True): return _mf_frame() with patch.object(helpers, "get_mf_and_etf_map", _mf): out = asyncio.run(helpers.get_series_id(cik="36405")) assert out.iloc[0]["symbol"] == "VFINX" def test_download_zip_file_invokes_callback(): """download_zip_file reads the response body through its callback.""" zip_bytes = _ftd_zip_bytes("SYMBOL|PRICE\nAAPL|1\nx\ny\n") class _Resp: async def read(self): return zip_bytes async def _fake(url, response_callback=None, **kwargs): return await response_callback(_Resp(), None) with patch.object(helpers, "cached_request", _fake): out = asyncio.run(helpers.download_zip_file("http://x/ftd.zip")) assert isinstance(out, list) @pytest.mark.parametrize( "value, expected", [ (5.7e12, "$5.7T AUM"), (4e9, "$4.0B AUM"), (3e6, "$3.0M AUM"), (1000, "$1,000 AUM"), (0, ""), (None, ""), ], ) def test_format_aum(value, expected): """_format_aum renders compact T/B/M/raw strings.""" assert helpers._format_aum(value) == expected def test_sec_range_read(monkeypatch): """_sec_range_read returns the body, with and without a Range header.""" class _R: def read(self): return b"BYTES" import urllib.request as _u monkeypatch.setattr(_u, "urlopen", lambda req, timeout=120: _R()) assert helpers._sec_range_read("http://x", "UA") == b"BYTES" assert helpers._sec_range_read("http://x", "UA", "bytes=0-3") == b"BYTES" def _zip_blob(name, content): """Build a single-member deflated zip.""" import io import zipfile buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr(name, content) return buf.getvalue() def _range_server(blob): """Return a fake _sec_range_read that serves byte ranges of ``blob``.""" def _read(url, ua, byte_range=None): if byte_range is None: return blob spec = byte_range.split("=")[1] if spec.startswith("-"): return blob[-int(spec[1:]) :] start, end = spec.split("-") return blob[int(start) : int(end) + 1] return _read def test_zip_member_text_found(monkeypatch): """_zip_member_text extracts and decompresses a named member.""" blob = _zip_blob("T.tsv", "a\tb\nx\ty\n") monkeypatch.setattr(helpers, "_sec_range_read", _range_server(blob)) assert helpers._zip_member_text("http://x.zip", "T.tsv", "UA") == "a\tb\nx\ty\n" def test_zip_member_text_missing_member(monkeypatch): """A member not in the archive returns None.""" blob = _zip_blob("T.tsv", "x") monkeypatch.setattr(helpers, "_sec_range_read", _range_server(blob)) assert helpers._zip_member_text("http://x.zip", "NOPE.tsv", "UA") is None def test_zip_member_text_no_eocd(monkeypatch): """Bytes without an end-of-central-directory record return None.""" monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua, byte_range=None: b"not-a-zip" ) assert helpers._zip_member_text("http://x.zip", "T.tsv", "UA") is None def test_series_names_from_dataset_ok(monkeypatch): """A dataset member yields a series_id -> name map, skipping N/A rows.""" monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/2026q1_nport.zip"' ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: "SERIES_ID\tSERIES_NAME\nS1\tFund One\nN/A\tNope\n", ) out = helpers._series_names_from_dataset( "http://l", "nport", "M.tsv", "SERIES_ID", "SERIES_NAME", "UA" ) assert out == {"S1": "Fund One"} def test_series_names_from_dataset_no_zips(monkeypatch): """No matching zip link yields an empty map.""" monkeypatch.setattr(helpers, "_sec_range_read", lambda u, ua: b"") assert ( helpers._series_names_from_dataset("http://l", "nport", "M.tsv", "A", "B", "UA") == {} ) def test_series_names_from_dataset_member_missing(monkeypatch): """A missing member yields an empty map.""" monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/2026q1_nport.zip"' ) monkeypatch.setattr(helpers, "_zip_member_text", lambda url, member, ua: None) assert ( helpers._series_names_from_dataset("http://l", "nport", "M.tsv", "A", "B", "UA") == {} ) def test_series_names_from_dataset_missing_columns(monkeypatch): """Headers lacking the id or name column yield an empty map.""" monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/2026q1_nport.zip"' ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: "SERIES_ID\tZ\nS1\tx\n" ) assert ( helpers._series_names_from_dataset( "http://l", "nport", "M.tsv", "SERIES_ID", "SERIES_NAME", "UA" ) == {} ) def test_fetch_series_name_map_merges_sources(monkeypatch): """N-MFP and N-PORT maps merge into one.""" def _fake(listing_url, pattern, member, sid, name, ua): # noqa: PLR0917 return {"S_MMF": "MMF"} if pattern == "nmfp" else {"S_ETF": "ETF"} monkeypatch.setattr(helpers, "_series_names_from_dataset", _fake) assert helpers._fetch_series_name_map() == {"S_MMF": "MMF", "S_ETF": "ETF"} def test_fetch_series_name_map_suppresses_errors(monkeypatch): """An error in one source is suppressed; the other still contributes.""" def _fake(listing_url, pattern, *args): if pattern == "nmfp": raise RuntimeError("boom") return {"S_ETF": "ETF"} monkeypatch.setattr(helpers, "_series_names_from_dataset", _fake) assert helpers._fetch_series_name_map() == {"S_ETF": "ETF"} def test_get_series_name_map_cache_miss_then_hit(monkeypatch): """The series map is fetched once then served from cache.""" store: dict = {} async def _aget(key): return store.get(key) async def _aset(key, value, expire=None): store[key] = value monkeypatch.setattr("openbb_sec.utils.cache.aget_cached", _aget) monkeypatch.setattr("openbb_sec.utils.cache.aset_cached", _aset) monkeypatch.setattr(helpers, "_fetch_series_name_map", lambda: {"S": "F"}) assert asyncio.run(helpers.get_series_name_map()) == {"S": "F"} monkeypatch.setattr(helpers, "_fetch_series_name_map", lambda: {"OTHER": "X"}) assert asyncio.run(helpers.get_series_name_map()) == {"S": "F"} def test_get_series_name_map_no_cache(monkeypatch): """use_cache=False always rebuilds the map.""" monkeypatch.setattr(helpers, "_fetch_series_name_map", lambda: {"S": "F"}) assert asyncio.run(helpers.get_series_name_map(use_cache=False)) == {"S": "F"} def _nport_funds_frame(): from pandas import DataFrame return DataFrame( { "cik": ["1", "2", "3", "4", "4"], "seriesId": ["S1", "S2", "S3", "S4", "S4"], "classId": ["C1", "C2", "C3", "C4", "C4"], "symbol": ["AAA", "BBB", "CCC", "", "DDD"], } ) def test_get_nport_fund_choices_filters_and_labels(monkeypatch): """Only funds with a series name are kept, labelled 'TICKER - Name'.""" async def _mf(use_cache=True): return _nport_funds_frame() async def _names(use_cache=True): return {"S1": "Fund One", "S2": "Fund Two", "S4": "Fund Four"} monkeypatch.setattr(helpers, "get_mf_and_etf_map", _mf) monkeypatch.setattr(helpers, "get_series_name_map", _names) out = asyncio.run(helpers.get_nport_fund_choices(use_cache=False)) values = [c["value"] for c in out] assert "AAA" in values and "CCC" not in values aaa = next(c for c in out if c["value"] == "AAA") assert aaa["label"] == "AAA - Fund One" def test_get_nport_fund_choices_no_map_keeps_all(monkeypatch): """With no series map the funds are kept and labelled by ticker only.""" async def _mf(use_cache=True): from pandas import DataFrame return DataFrame( {"cik": ["1"], "seriesId": ["S1"], "classId": ["C1"], "symbol": ["AAA"]} ) async def _names(use_cache=True): return {} monkeypatch.setattr(helpers, "get_mf_and_etf_map", _mf) monkeypatch.setattr(helpers, "get_series_name_map", _names) out = asyncio.run(helpers.get_nport_fund_choices(use_cache=False)) assert out[0]["label"] == "AAA" def test_get_nport_fund_choices_cache_hit(monkeypatch): """A cached choices list is returned without rebuilding.""" async def _aget(key): return [{"label": "cached", "value": "X"}] monkeypatch.setattr("openbb_sec.utils.cache.aget_cached", _aget) assert asyncio.run(helpers.get_nport_fund_choices()) == [ {"label": "cached", "value": "X"} ] def test_fetch_13f_filers(monkeypatch): """13F filers join submission/coverpage/summary keeping the largest value.""" members = { "SUBMISSION.tsv": ( "ACCESSION_NUMBER\tCIK\nA1\t0000001067983\nA2\t0000001067983\nA3\t0000000999\n" ), "COVERPAGE.tsv": ( "ACCESSION_NUMBER\tFILINGMANAGER_NAME\n" "A1\tBerkshire Hathaway Inc\nA2\tBerkshire Hathaway Inc\nA3\t \n" ), "SUMMARYPAGE.tsv": ( "ACCESSION_NUMBER\tTABLEVALUETOTAL\nA1\t100\nA2\t5000\nA3\tnan\n" ), } monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/01mar2026-31may2026_form13f.zip"', ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: members[member] ) out = helpers._fetch_13f_filers() assert out["1067983"]["name"] == "Berkshire Hathaway Inc" assert out["1067983"]["aum"] == 5000.0 assert "999" not in out def test_fetch_13f_filers_no_zips(monkeypatch): """No 13F zip link yields an empty map.""" monkeypatch.setattr(helpers, "_sec_range_read", lambda u, ua: b"") assert helpers._fetch_13f_filers() == {} def test_get_13f_filer_choices_sorted_with_tickers(monkeypatch): """Choices are AUM-sorted; company filers get a ticker tag; no-AUM dropped.""" def _filers(): return { "1067983": {"name": "Berkshire", "aum": 5e12}, "999": {"name": "Small LLC", "aum": 1e6}, "777": {"name": "No AUM Notice", "aum": None}, "555": {"name": "Zero AUM", "aum": 0.0}, } async def _companies(use_cache=True): from pandas import DataFrame return DataFrame( {"cik": ["1067983"], "symbol": ["BRK-B"], "name": ["Berkshire"]} ) monkeypatch.setattr(helpers, "_fetch_13f_filers", _filers) monkeypatch.setattr(helpers, "get_all_companies", _companies) out = asyncio.run(helpers.get_13f_filer_choices(use_cache=False)) assert [c["value"] for c in out] == ["1067983", "999"] assert out[0]["label"] == "Berkshire (BRK-B)" assert out[1]["label"] == "Small LLC" def test_get_13f_filer_choices_company_lookup_error(monkeypatch): """A failed company lookup degrades to name-only labels.""" monkeypatch.setattr( helpers, "_fetch_13f_filers", lambda: {"1": {"name": "X", "aum": 1.0}} ) async def _boom(use_cache=True): raise RuntimeError("down") monkeypatch.setattr(helpers, "get_all_companies", _boom) out = asyncio.run(helpers.get_13f_filer_choices(use_cache=False)) assert out[0]["label"] == "X" def test_get_13f_filer_choices_cache_hit(monkeypatch): """A cached filer list is returned without rebuilding.""" async def _aget(key): return [{"label": "cached", "value": "1"}] monkeypatch.setattr("openbb_sec.utils.cache.aget_cached", _aget) assert asyncio.run(helpers.get_13f_filer_choices()) == [ {"label": "cached", "value": "1"} ] def test_get_nport_candidates_paginates(monkeypatch): """Candidates are paged from EFTS and sorted newest-first.""" from pandas import DataFrame async def _series(symbol, use_cache=True): return DataFrame({"seriesId": ["S000001"]}) pages = [ { "hits": { "hits": [ { "_id": "acc1:f.htm", "_source": { "display_names": ["Fund"], "ciks": ["1"], "file_date": "2026-05-31", "period_ending": "2026-05-31", "form": "N-MFP3", }, } ], "total": {"value": 2}, } }, { "hits": { "hits": [ { "_id": "acc2:g.htm", "_source": { "display_names": ["Fund"], "ciks": ["1"], "file_date": "2026-04-30", "form": "N-MFP3", }, } ], "total": {"value": 2}, } }, ] state = {"n": 0} async def _cached(url, **kwargs): page = pages[state["n"]] state["n"] += 1 return page monkeypatch.setattr(helpers, "get_series_id", _series) monkeypatch.setattr(helpers, "cached_request", _cached) out = asyncio.run(helpers.get_nport_candidates("VMFXX")) assert len(out) == 2 assert out[0]["file_date"] == "2026-05-31" assert out[1]["period_ending"] == "2026-04-30" def test_get_nport_candidates_non_dict_breaks(monkeypatch): """A non-dict EFTS response ends pagination with no candidates.""" from pandas import DataFrame async def _series(symbol, use_cache=True): return DataFrame({"seriesId": ["S000001"]}) async def _cached(url, **kwargs): return "throttled" monkeypatch.setattr(helpers, "get_series_id", _series) monkeypatch.setattr(helpers, "cached_request", _cached) assert asyncio.run(helpers.get_nport_candidates("VMFXX")) == [] def test_series_names_from_dataset_missing_id_column(monkeypatch): """A header lacking the id column yields an empty map.""" monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/2026q1_nport.zip"' ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: "X\tY\n1\t2\n" ) assert ( helpers._series_names_from_dataset( "http://l", "nport", "M.tsv", "SERIES_ID", "SERIES_NAME", "UA" ) == {} ) def test_get_nport_fund_choices_writes_cache(monkeypatch): """A cache miss builds and stores the fund choices.""" store: dict = {} async def _aget(key): return store.get(key) async def _aset(key, value, expire=None): store[key] = value async def _mf(use_cache=True): from pandas import DataFrame return DataFrame( {"cik": ["1"], "seriesId": ["S1"], "classId": ["C1"], "symbol": ["AAA"]} ) async def _names(use_cache=True): return {"S1": "Fund One"} monkeypatch.setattr("openbb_sec.utils.cache.aget_cached", _aget) monkeypatch.setattr("openbb_sec.utils.cache.aset_cached", _aset) monkeypatch.setattr(helpers, "get_mf_and_etf_map", _mf) monkeypatch.setattr(helpers, "get_series_name_map", _names) out = asyncio.run(helpers.get_nport_fund_choices()) assert out[0]["value"] == "AAA" assert "sec_nport_fund_choices" in store def _thirteen_f_members(summary): """Build 13F dataset members with a given SUMMARYPAGE body.""" return { "SUBMISSION.tsv": "ACCESSION_NUMBER\tCIK\nA1\t0000000001\n", "COVERPAGE.tsv": "ACCESSION_NUMBER\tFILINGMANAGER_NAME\nA1\tManager LLC\n", "SUMMARYPAGE.tsv": summary, } def test_fetch_13f_filers_empty_summary(monkeypatch): """An empty summary member leaves the filer's AUM unset.""" members = _thirteen_f_members("") monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/01mar2026-31may2026_form13f.zip"', ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: members[member] ) assert helpers._fetch_13f_filers()["1"]["aum"] is None def test_fetch_13f_filers_summary_missing_column(monkeypatch): """A summary member without the value column leaves AUM unset.""" members = _thirteen_f_members("ACCESSION_NUMBER\tWRONG\nA1\t5\n") monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/01mar2026-31may2026_form13f.zip"', ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: members[member] ) assert helpers._fetch_13f_filers()["1"]["aum"] is None def test_fetch_13f_filers_bad_aum(monkeypatch): """A non-numeric portfolio value is coerced to None.""" members = _thirteen_f_members("ACCESSION_NUMBER\tTABLEVALUETOTAL\nA1\tabc\n") monkeypatch.setattr( helpers, "_sec_range_read", lambda u, ua: b'href="/x/01mar2026-31may2026_form13f.zip"', ) monkeypatch.setattr( helpers, "_zip_member_text", lambda url, member, ua: members[member] ) assert helpers._fetch_13f_filers()["1"]["aum"] is None def test_get_13f_filer_choices_writes_cache(monkeypatch): """A cache miss builds and stores the filer list.""" store: dict = {} async def _aget(key): return store.get(key) async def _aset(key, value, expire=None): store[key] = value async def _companies(use_cache=True): from pandas import DataFrame return DataFrame({"cik": [], "symbol": [], "name": []}) monkeypatch.setattr("openbb_sec.utils.cache.aget_cached", _aget) monkeypatch.setattr("openbb_sec.utils.cache.aset_cached", _aset) monkeypatch.setattr( helpers, "_fetch_13f_filers", lambda: {"1": {"name": "X", "aum": 1.0}} ) monkeypatch.setattr(helpers, "get_all_companies", _companies) out = asyncio.run(helpers.get_13f_filer_choices()) assert out[0]["label"] == "X" assert "sec_13f_filer_choices" in store