""" Unit tests for SEC full-index (company.idx and form.idx) parsers. """ import pytest from datetime import date from app.services.sec_full_index_service import ( parse_company_idx, _parse_form_idx, FORM4_TYPES, ACTIVIST_13DG_TYPES, ) # Realistic company.idx sample — fixed-width, matches real SEC EDGAR format. # Column offsets: Company(0-61), Form(62-73), CIK(74-85), Date(86-97), File(98+) # The dashes line is the sentinel that marks the start of data rows. _H = "Company Name Form Type CIK Date Filed Filename" _D = "-" * 120 def _row(company, form_type, cik, filing_date, acc): """Build a fixed-width company.idx data row matching real EDGAR format. Real EDGAR layout: company(62) + form_type(12) + 5_spaces + cik(12, left-pad) + date(10) + 2_spaces + filename CIK always has 5 mandatory leading spaces; date is at position 91, filename at 103. """ filename = f"edgar/data/{int(cik)}/{acc}-index.htm" cik_str = f"{str(int(cik)):<12}" # 12-char left-aligned CIK (trailing spaces) # 5 mandatory leading spaces + 12-char CIK + date(10) + 2 spaces = positions 74-102 line = f"{company:<62}{form_type:<12} {cik_str}{filing_date} {filename}" return line SAMPLE_IDX = "\n".join([ "Full-Index of EDGAR Filings Submitted to the Commission", "", _H, _D, _row("APPLE INC", "4", "320193", "2026-01-15", "0000320193-26-000001"), _row("BERKSHIRE HATHAWAY INC", "SC 13G", "1067983", "2026-02-14", "0001067983-26-000042"), _row("BLACKROCK INC", "SC 13D/A", "1364742", "2026-01-20", "0001364742-26-000010"), _row("TESLA INC", "4/A", "1318605", "2026-01-18", "0001318605-26-000005"), _row("MICROSOFT CORP", "10-K", "789019", "2026-02-01", "0000789019-26-000020"), ]) def test_parse_form4_entries(): entries = parse_company_idx(SAMPLE_IDX, form_types=FORM4_TYPES) assert len(entries) == 2 form_types = {e.form_type for e in entries} assert "4" in form_types assert "4/A" in form_types def test_parse_activist_entries(): entries = parse_company_idx(SAMPLE_IDX, form_types=ACTIVIST_13DG_TYPES) assert len(entries) == 2 form_types = {e.form_type for e in entries} assert "SC 13G" in form_types assert "SC 13D/A" in form_types def test_parse_all_entries_no_filter(): entries = parse_company_idx(SAMPLE_IDX) # 4, SC 13G, SC 13D/A, 4/A, 10-K assert len(entries) == 5 def test_parse_entry_fields(): entries = parse_company_idx(SAMPLE_IDX, form_types=FORM4_TYPES) apple = next((e for e in entries if "APPLE" in e.company_name), None) assert apple is not None, f"APPLE not found in {[e.company_name for e in entries]}" assert apple.cik == "0000320193" assert apple.filing_date == date(2026, 1, 15) assert "0000320193-26-000001" in apple.accession_number assert "edgar/data" in apple.filename def test_parse_skips_short_lines(): idx = "short\n" + SAMPLE_IDX entries = parse_company_idx(idx, form_types=FORM4_TYPES) # Should not crash; just skip the malformed line assert isinstance(entries, list) def test_parse_empty_text(): entries = parse_company_idx("", form_types=FORM4_TYPES) assert entries == [] def test_parse_header_only(): # Only the 4 header lines (no data rows after the dash separator) lines = SAMPLE_IDX.splitlines() dash_idx = next(i for i, l in enumerate(lines) if set(l.strip()) == {"-"} and len(l.strip()) > 20) header_only = "\n".join(lines[:dash_idx + 1]) # up to and including dash line entries = parse_company_idx(header_only, form_types=FORM4_TYPES) assert entries == [] # ------------------------------------------------------------------ # form.idx parser (form345.zip column layout: Form Type is FIRST) # ------------------------------------------------------------------ def _form_row(form_type, company, cik, filing_date, acc): """Build a fixed-width form.idx row (Form Type first, then Company Name). Same EDGAR offsets from position 74 onward as company.idx. """ filename = f"edgar/data/{int(cik)}/{acc}-index.htm" cik_str = f"{str(int(cik)):<12}" # 12-char left-aligned CIK line = f"{form_type:<12}{company:<62} {cik_str}{filing_date} {filename}" return line _FORM_IDX = "\n".join([ "Form Type Company Name CIK Date Filed Filename", "-" * 120, _form_row("4", "TIM COOK", "320193", "2026-01-15", "0000320193-26-000001"), _form_row("4/A", "ELON MUSK", "1318605", "2026-01-18", "0001318605-26-000005"), _form_row("10-K", "MICROSOFT CORP", "789019", "2026-02-01", "0000789019-26-000020"), ]) def test_form_idx_parse_form4(): entries = _parse_form_idx(_FORM_IDX, form_types=FORM4_TYPES) assert len(entries) == 2 form_types = {e.form_type for e in entries} assert "4" in form_types assert "4/A" in form_types def test_form_idx_company_name_correct(): entries = _parse_form_idx(_FORM_IDX, form_types=FORM4_TYPES) cook = next((e for e in entries if "TIM COOK" in e.company_name), None) assert cook is not None assert cook.cik == "0000320193" assert cook.filing_date == date(2026, 1, 15) assert "0000320193-26-000001" in cook.accession_number def test_form_idx_does_not_mix_offsets_with_company_idx(): # Same data fed to company.idx parser would produce garbled output; # form_idx parser must correctly extract form_type from cols 0-12. company_idx_entries = parse_company_idx(_FORM_IDX, form_types=FORM4_TYPES) form_idx_entries = _parse_form_idx(_FORM_IDX, form_types=FORM4_TYPES) # company.idx parser treats "4 TIM COOK..." as company_name="4" # and form_type from offset 62 — misses "4" form type → 0 entries # form.idx parser correctly gets 2 Form 4 entries assert len(form_idx_entries) == 2 assert len(company_idx_entries) == 0 # wrong parser on wrong format = empty