"""NinjaPearProvider against the real API shape documented at nubela.co/llms-full.txt: website-only company identification, and the vendor's actual response field names (executives/employee_count, total_funds_raised/funding_rounds, competitors keyed by website, three separately-categorized customer/investor/partner arrays, first_name+domain for work-email, x_profile_url for the person-profile endpoint).""" from __future__ import annotations import httpx import pytest import respx from app.core.config import Settings from app.enrichment.ninjapear import NinjaPearProvider _SETTINGS = Settings(ninjapear_api_key="test-key") _WEBSITE = "https://acme.example.com" @pytest.mark.asyncio async def test_get_company_details_parses_executives_and_employee_count(): payload = { "description": "A payments company.", "industry": "Fintech", "founded_year": 2010, "specialties": ["Payments", "APIs"], "employee_count": "1001-5000", "executives": [ {"name": "Jane Doe", "title": "CEO"}, {"name": "", "title": "Ignored - no name"}, ], } with respx.mock: respx.get("https://nubela.co/api/v1/company/details").mock( return_value=httpx.Response(200, json=payload) ) details = await NinjaPearProvider(_SETTINGS).get_company_details("Acme Corp", _WEBSITE) assert details.industry == "Fintech" assert details.employee_count_range == "1001-5000" assert len(details.leadership_team) == 1 assert details.leadership_team[0].name == "Jane Doe" @pytest.mark.asyncio async def test_company_details_raises_without_a_website(): with pytest.raises(ValueError, match="website"): await NinjaPearProvider(_SETTINGS).get_company_details("Acme Corp", None) @pytest.mark.asyncio async def test_get_funding_maps_total_funds_raised_and_funding_rounds(): # total_funds_raised/amount_usd are raw numbers, and each investor is # an object (not a plain string) - matches the real API's shape. payload = { "total_funds_raised_usd": 500_000_000, "funding_rounds": [ { "round_type": "Series C", "amount_usd": 200_000_000, "date": "2024-01-01", "investors": [{"name": "VC Co", "type": "company", "website": None}], } ], } with respx.mock: respx.get("https://nubela.co/api/v1/company/funding").mock( return_value=httpx.Response(200, json=payload) ) funding = await NinjaPearProvider(_SETTINGS).get_funding("Acme Corp", _WEBSITE) assert funding.total_raised == "500000000" assert funding.rounds[0].round_name == "Series C" assert funding.rounds[0].amount == "200000000" assert funding.rounds[0].investors == ["VC Co"] @pytest.mark.asyncio async def test_get_competitors_falls_back_to_website_when_no_name_given(): payload = { "competitors": [{"website": "rival.example.com", "competition_reason": "Same market"}] } with respx.mock: respx.get("https://nubela.co/api/v1/competitor/listing").mock( return_value=httpx.Response(200, json=payload) ) competitors = await NinjaPearProvider(_SETTINGS).get_competitors("Acme Corp", _WEBSITE) assert competitors[0].name == "rival.example.com" assert competitors[0].reason == "Same market" @pytest.mark.asyncio async def test_get_customers_merges_the_three_categorized_arrays(): payload = { "customers": [{"name": "BigCo"}], "investors": [{"name": "VC Fund"}], "partner_platforms": [{"name": "PlatformCo"}], } with respx.mock: respx.get("https://nubela.co/api/v1/customer/listing").mock( return_value=httpx.Response(200, json=payload) ) customers = await NinjaPearProvider(_SETTINGS).get_customers("Acme Corp", _WEBSITE) by_relationship = {c.relationship: c.name for c in customers} assert by_relationship == {"customer": "BigCo", "investor": "VC Fund", "partner": "PlatformCo"} @pytest.mark.asyncio async def test_get_products_joins_categories(): payload = { "products": [ {"name": "Widget", "description": "A widget", "categories": ["Hardware", "IoT"]} ] } with respx.mock: respx.get("https://nubela.co/api/v1/product/listing").mock( return_value=httpx.Response(200, json=payload) ) products = await NinjaPearProvider(_SETTINGS).get_products("Acme Corp", _WEBSITE) assert products[0].name == "Widget" assert products[0].category == "Hardware, IoT" @pytest.mark.asyncio async def test_get_updates_maps_title_and_source(): payload = { "updates": [ { "title": "We shipped a feature", "source": "blog", "url": "https://x", "timestamp": "2026-01-01", } ] } with respx.mock: respx.get("https://nubela.co/api/v1/company/updates").mock( return_value=httpx.Response(200, json=payload) ) updates = await NinjaPearProvider(_SETTINGS).get_updates("Acme Corp", _WEBSITE) assert updates[0].text == "We shipped a feature" assert updates[0].type == "blog" @pytest.mark.asyncio async def test_get_work_email_sends_first_last_name_and_domain(): with respx.mock: route = respx.get("https://nubela.co/api/v1/employee/work-email").mock( return_value=httpx.Response(200, json={"work_email": "jane@acme.example.com"}) ) email = await NinjaPearProvider(_SETTINGS).get_work_email("Jane Doe", _WEBSITE) assert email == "jane@acme.example.com" sent_params = dict(route.calls[0].request.url.params) assert sent_params == {"first_name": "Jane", "last_name": "Doe", "domain": "acme.example.com"} @pytest.mark.asyncio async def test_get_person_profile_hits_the_v2_endpoint_and_maps_x_profile_url(): with respx.mock: respx.get("https://nubela.co/api/v2/employee/profile").mock( return_value=httpx.Response( 200, json={"x_profile_url": "https://x.com/janedoe", "bio": "A bio."} ) ) profile_url, bio = await NinjaPearProvider(_SETTINGS).get_person_profile( "Jane Doe", _WEBSITE ) assert profile_url == "https://x.com/janedoe" assert bio == "A bio." @pytest.mark.asyncio async def test_a_provider_error_response_raises_rather_than_being_swallowed(): with respx.mock: respx.get("https://nubela.co/api/v1/company/details").mock( return_value=httpx.Response(429, text="rate limited") ) with pytest.raises(httpx.HTTPStatusError): await NinjaPearProvider(_SETTINGS).get_company_details("Acme Corp", _WEBSITE)