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).
96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""POST /companies/discover: persists nothing, returns a proposed profile,
|
|
rate-limited tightly since it costs a real search + LLM call."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from unittest.mock import patch
|
|
|
|
import httpx
|
|
import respx
|
|
|
|
from app.core.rate_limit import limiter
|
|
|
|
|
|
def _register_and_login(client) -> dict[str, str]:
|
|
email = f"user-{uuid.uuid4().hex[:12]}@example.com"
|
|
client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": email, "password": "correct-horse-1", "display_name": "Test User"},
|
|
)
|
|
tokens = client.post(
|
|
"/api/v1/auth/login", json={"email": email, "password": "correct-horse-1"}
|
|
).json()
|
|
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
|
|
|
|
|
def _mock_empty_github_sec():
|
|
respx.get("https://api.github.com/search/users").mock(
|
|
return_value=httpx.Response(200, text=json.dumps({"items": []}))
|
|
)
|
|
respx.get("https://www.sec.gov/cgi-bin/browse-edgar").mock(
|
|
return_value=httpx.Response(
|
|
200, text='<?xml version="1.0"?><feed xmlns="http://www.w3.org/2005/Atom"></feed>'
|
|
)
|
|
)
|
|
|
|
|
|
def test_discover_returns_a_profile_without_persisting_a_company(client):
|
|
headers = _register_and_login(client)
|
|
|
|
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]):
|
|
with respx.mock:
|
|
_mock_empty_github_sec()
|
|
respx.get("https://acmewidgets.com/robots.txt").mock(return_value=httpx.Response(404))
|
|
respx.get("https://acmewidgets.com").mock(
|
|
return_value=httpx.Response(200, html="<html><body>Acme Widgets</body></html>")
|
|
)
|
|
resp = client.post(
|
|
"/api/v1/companies/discover",
|
|
json={"name": "Acme Widgets"},
|
|
headers=headers,
|
|
)
|
|
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["name"] == "Acme Widgets"
|
|
assert body["official_website"] == "https://acmewidgets.com"
|
|
assert "potential_sources" in body
|
|
assert "sources_consulted" in body
|
|
|
|
companies = client.get("/api/v1/companies", headers=headers).json()
|
|
assert companies == []
|
|
|
|
|
|
def test_discover_requires_auth(client):
|
|
resp = client.post("/api/v1/companies/discover", json={"name": "Acme"})
|
|
assert resp.status_code in (401, 403)
|
|
|
|
|
|
def test_discover_enforces_rate_limit(client):
|
|
headers = _register_and_login(client)
|
|
|
|
limiter.enabled = True
|
|
try:
|
|
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]):
|
|
with respx.mock:
|
|
_mock_empty_github_sec()
|
|
respx.route(host="acme0.com").mock(return_value=httpx.Response(404))
|
|
respx.route(host="acme1.com").mock(return_value=httpx.Response(404))
|
|
respx.route(host="acme2.com").mock(return_value=httpx.Response(404))
|
|
respx.route(host="acme3.com").mock(return_value=httpx.Response(404))
|
|
respx.route(host="acme4.com").mock(return_value=httpx.Response(404))
|
|
respx.route(host="acme5.com").mock(return_value=httpx.Response(404))
|
|
statuses = [
|
|
client.post(
|
|
"/api/v1/companies/discover",
|
|
json={"name": f"Acme{i}"},
|
|
headers=headers,
|
|
).status_code
|
|
for i in range(6)
|
|
]
|
|
assert 429 in statuses, f"Expected a 429 among {statuses} after 6 rapid discover calls"
|
|
finally:
|
|
limiter.enabled = False
|