Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.enrichment.mock import MockEnrichmentProvider
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_provider_returns_honest_empties_for_everything():
|
||||
provider = MockEnrichmentProvider()
|
||||
|
||||
details = await provider.get_company_details("Acme Corp", "https://acme.example.com")
|
||||
assert details.description is None
|
||||
assert details.leadership_team == []
|
||||
|
||||
funding = await provider.get_funding("Acme Corp", None)
|
||||
assert funding.total_raised is None
|
||||
assert funding.rounds == []
|
||||
|
||||
assert await provider.get_updates("Acme Corp", None) == []
|
||||
assert await provider.get_competitors("Acme Corp", None) == []
|
||||
assert await provider.get_products("Acme Corp", None) == []
|
||||
assert await provider.get_customers("Acme Corp", None) == []
|
||||
assert await provider.get_work_email("Jane Doe", "https://acme.example.com") is None
|
||||
assert await provider.get_person_profile("Jane Doe", "https://acme.example.com") == (None, None)
|
||||
@@ -0,0 +1,186 @@
|
||||
"""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": "[email protected]"})
|
||||
)
|
||||
email = await NinjaPearProvider(_SETTINGS).get_work_email("Jane Doe", _WEBSITE)
|
||||
|
||||
assert email == "[email protected]"
|
||||
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)
|
||||
Reference in New Issue
Block a user