| 1 | from datetime import datetime, timezone |
| 2 | from uuid import uuid4 |
| 3 | |
| 4 | import pytest |
| 5 | |
| 6 | from app.models import CompanyResearchProfile, SourceClassification |
| 7 | from app.source_discovery import CandidateSearchResult, SearchDateWindow, SearchDiscoveryService, _candidate_has_positive_issuer_evidence |
| 8 | |
| 9 | |
| 10 | class _StaticSearchProvider: |
| 11 | provider_name = "test-search" |
| 12 | |
| 13 | def __init__(self, candidates: list[CandidateSearchResult]) -> None: |
| 14 | self.candidates = candidates |
| 15 | |
| 16 | async def discover(self, company, category, date_window: SearchDateWindow): |
| 17 | return self.candidates |
| 18 | |
| 19 | |
| 20 | def _federal_bank_profile() -> CompanyResearchProfile: |
| 21 | return CompanyResearchProfile( |
| 22 | instrument_id=uuid4(), company_id=uuid4(), company_name="FEDERAL BANK LTD", |
| 23 | aliases=["FEDERAL BANK"], provider_instrument_ids={"NSE": "FEDERALBNK"}, |
| 24 | isin="INE171A01029", ticker="FEDBAN", exchange="NSE", mic="XNSE", |
| 25 | country="IN", currency="INR", known_domains=["federalbank.co.in"], |
| 26 | ) |
| 27 | |
| 28 | |
| 29 | def _candidate(url: str, title: str = "", snippet: str = "") -> CandidateSearchResult: |
| 30 | return CandidateSearchResult( |
| 31 | title=title, url=url, snippet=snippet, discovered_at=datetime(2026, 1, 1, tzinfo=timezone.utc), |
| 32 | provider="test-search", query_id="FINANCIAL_RESULTS:1", query='"FEDERAL BANK LTD" results', |
| 33 | category="FINANCIAL_RESULTS", |
| 34 | ) |
| 35 | |
| 36 | |
| 37 | @pytest.mark.asyncio |
| 38 | async def test_generic_bse_result_naming_other_issuer_is_rejected_before_discovery_result() -> None: |
| 39 | profile = _federal_bank_profile() |
| 40 | search = SearchDiscoveryService(_StaticSearchProvider([ |
| 41 | _candidate("https://www.bseindia.com/xml-data/corpfiling/AttachLive/TAMILNADU.pdf", |
| 42 | "TAMILNADU PETROPRODUCTS LTD financial results", "Quarterly result"), |
| 43 | ])) |
| 44 | |
| 45 | assert await search.discover(profile, {"FINANCIAL_RESULTS"}, set()) == [] |
| 46 | assert search.last_stats.rejected_reasons == {"COMPANY_RELEVANCE_FAILED": 1} |
| 47 | assert search.rejected_candidates[0].reason == "COMPANY_RELEVANCE_FAILED" |
| 48 | |
| 49 | |
| 50 | @pytest.mark.asyncio |
| 51 | async def test_trusted_isin_accepts_generic_exchange_candidate() -> None: |
| 52 | profile = _federal_bank_profile() |
| 53 | search = SearchDiscoveryService(_StaticSearchProvider([ |
| 54 | _candidate("https://www.bseindia.com/xml-data/corpfiling/AttachLive/federal.pdf", |
| 55 | "Financial results", "INE171A01029 quarterly financial results"), |
| 56 | ])) |
| 57 | |
| 58 | results = await search.discover(profile, {"FINANCIAL_RESULTS"}, set()) |
| 59 | |
| 60 | assert len(results) == 1 |
| 61 | assert results[0].source.source_classification == SourceClassification.EXCHANGE |
| 62 | |
| 63 | |
| 64 | @pytest.mark.asyncio |
| 65 | async def test_trusted_provider_symbol_as_real_token_accepts_generic_exchange_candidate() -> None: |
| 66 | profile = _federal_bank_profile() |
| 67 | search = SearchDiscoveryService(_StaticSearchProvider([ |
| 68 | _candidate("https://www.nseindia.com/corporates/federal.pdf", "FEDERALBNK financial results", "Quarterly result"), |
| 69 | ])) |
| 70 | |
| 71 | assert len(await search.discover(profile, {"FINANCIAL_RESULTS"}, set())) == 1 |
| 72 | |
| 73 | |
| 74 | @pytest.mark.asyncio |
| 75 | async def test_known_company_domain_remains_accepted_without_metadata_name() -> None: |
| 76 | profile = _federal_bank_profile() |
| 77 | search = SearchDiscoveryService(_StaticSearchProvider([ |
| 78 | _candidate("https://www.federalbank.co.in/investors/results.pdf", "Investor document"), |
| 79 | ])) |
| 80 | |
| 81 | results = await search.discover(profile, {"FINANCIAL_RESULTS"}, set()) |
| 82 | |
| 83 | assert len(results) == 1 |
| 84 | assert results[0].source.source_classification == SourceClassification.OFFICIAL_COMPANY |
| 85 | |
| 86 | |
| 87 | def test_exchange_domain_alone_is_not_positive_issuer_evidence() -> None: |
| 88 | profile = _federal_bank_profile() |
| 89 | candidate = _candidate("https://www.bseindia.com/xml-data/corpfiling/AttachLive/filing.pdf", "Financial results") |
| 90 | |
| 91 | assert _candidate_has_positive_issuer_evidence(profile, f"{candidate.title} {candidate.snippet} {candidate.url}") is False |
| 92 | |
| 93 | |
| 94 | @pytest.mark.asyncio |
| 95 | async def test_untrusted_local_symbol_cannot_override_verified_provider_identity() -> None: |
| 96 | profile = _federal_bank_profile() |
| 97 | search = SearchDiscoveryService(_StaticSearchProvider([ |
| 98 | _candidate("https://www.nseindia.com/corporates/other.pdf", |
| 99 | "FEDBAN TAMILNADU PETROPRODUCTS LTD financial results", "Quarterly result"), |
| 100 | ])) |
| 101 | |
| 102 | assert await search.discover(profile, {"FINANCIAL_RESULTS"}, set()) == [] |
| 103 | assert search.last_stats.rejected_reasons == {"COMPANY_RELEVANCE_FAILED": 1} |
| 104 | |
| 105 | |
| 106 | @pytest.mark.asyncio |
| 107 | async def test_unsafe_and_duplicate_rejections_remain_unchanged() -> None: |
| 108 | profile = _federal_bank_profile() |
| 109 | duplicate = "https://www.federalbank.co.in/investors/results.pdf" |
| 110 | search = SearchDiscoveryService(_StaticSearchProvider([ |
| 111 | _candidate("http://127.0.0.1/private", "FEDERAL BANK LTD results"), |
| 112 | _candidate(duplicate, "Investor document"), |
| 113 | ])) |
| 114 | |
| 115 | assert await search.discover(profile, {"FINANCIAL_RESULTS"}, {duplicate}) == [] |
| 116 | assert search.last_stats.rejected_reasons == {"DOMAIN_VALIDATION_FAILED": 1, "DUPLICATE": 1} |