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).
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
"""Sources API: ownership isolation, user-creatable type restriction, and
|
|
the ad-hoc test action - via HTTP, with respx mocking the network call the
|
|
test action makes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from unittest.mock import patch
|
|
|
|
import httpx
|
|
import respx
|
|
|
|
|
|
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 _create_company(client, headers):
|
|
return client.post(
|
|
"/api/v1/companies",
|
|
json={"name": f"Co {uuid.uuid4().hex[:6]}", "frequency_type": "weekly"},
|
|
headers=headers,
|
|
).json()
|
|
|
|
|
|
def test_create_custom_url_source(client):
|
|
headers = _register_and_login(client)
|
|
company = _create_company(client, headers)
|
|
|
|
resp = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={"source_type": "custom_url", "name": "Pricing", "base_url": "example.com/pricing"},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 201
|
|
body = resp.json()
|
|
assert body["base_url"] == "https://example.com/pricing"
|
|
assert body["status"] == "active"
|
|
|
|
|
|
def test_create_source_rejects_non_user_creatable_type(client):
|
|
headers = _register_and_login(client)
|
|
company = _create_company(client, headers)
|
|
|
|
resp = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={"source_type": "github", "name": "GitHub", "base_url": "https://github.com/acme"},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_sources_scoped_to_owner(client):
|
|
owner_headers = _register_and_login(client)
|
|
other_headers = _register_and_login(client)
|
|
company = _create_company(client, owner_headers)
|
|
|
|
created = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={
|
|
"source_type": "custom_url",
|
|
"name": "Pricing",
|
|
"base_url": "https://example.com/pricing",
|
|
},
|
|
headers=owner_headers,
|
|
).json()
|
|
|
|
# Another user can't list this company's sources...
|
|
resp = client.get(f"/api/v1/companies/{company['id']}/sources", headers=other_headers)
|
|
assert resp.status_code == 404
|
|
|
|
# ...or update/delete the source directly.
|
|
resp = client.patch(
|
|
f"/api/v1/sources/{created['id']}", json={"active": False}, headers=other_headers
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_update_and_delete_source(client):
|
|
headers = _register_and_login(client)
|
|
company = _create_company(client, headers)
|
|
created = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={
|
|
"source_type": "custom_url",
|
|
"name": "Pricing",
|
|
"base_url": "https://example.com/pricing",
|
|
},
|
|
headers=headers,
|
|
).json()
|
|
|
|
resp = client.patch(f"/api/v1/sources/{created['id']}", json={"active": False}, headers=headers)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["active"] is False
|
|
|
|
resp = client.delete(f"/api/v1/sources/{created['id']}", headers=headers)
|
|
assert resp.status_code == 204
|
|
|
|
resp = client.get(f"/api/v1/companies/{company['id']}/sources", headers=headers)
|
|
assert resp.json() == []
|
|
|
|
|
|
def test_update_source_sets_a_frequency_override(client):
|
|
headers = _register_and_login(client)
|
|
company = _create_company(client, headers)
|
|
created = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={
|
|
"source_type": "custom_url",
|
|
"name": "Pricing",
|
|
"base_url": "https://example.com/pricing",
|
|
},
|
|
headers=headers,
|
|
).json()
|
|
|
|
resp = client.patch(
|
|
f"/api/v1/sources/{created['id']}", json={"frequency_type": "daily"}, headers=headers
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["frequency_type"] == "daily"
|
|
assert body["next_check"] is None # takes effect on the next scheduler tick
|
|
|
|
# Clearing the override back to "same as company" is an explicit null.
|
|
resp = client.patch(
|
|
f"/api/v1/sources/{created['id']}", json={"frequency_type": None}, headers=headers
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["frequency_type"] is None
|
|
|
|
|
|
def test_update_source_rejects_a_custom_frequency_below_the_minimum_interval(client):
|
|
headers = _register_and_login(client)
|
|
company = _create_company(client, headers)
|
|
created = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={
|
|
"source_type": "custom_url",
|
|
"name": "Pricing",
|
|
"base_url": "https://example.com/pricing",
|
|
},
|
|
headers=headers,
|
|
).json()
|
|
|
|
resp = client.patch(
|
|
f"/api/v1/sources/{created['id']}",
|
|
json={"frequency_type": "custom", "interval_minutes": 1},
|
|
headers=headers,
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_source_test_action_runs_a_real_collection(client):
|
|
headers = _register_and_login(client)
|
|
company = _create_company(client, headers)
|
|
created = client.post(
|
|
f"/api/v1/companies/{company['id']}/sources",
|
|
json={
|
|
"source_type": "custom_url",
|
|
"name": "Pricing",
|
|
"base_url": "https://example.com/pricing",
|
|
},
|
|
headers=headers,
|
|
).json()
|
|
|
|
with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]):
|
|
with respx.mock:
|
|
respx.get("https://example.com/robots.txt").mock(return_value=httpx.Response(404))
|
|
respx.get("https://example.com/pricing").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
html="<html><head><title>Pricing</title></head><body>"
|
|
"<article><h1>Pricing</h1><p>Plans start at $10/month.</p></article>"
|
|
"</body></html>",
|
|
)
|
|
)
|
|
resp = client.post(f"/api/v1/sources/{created['id']}/test", headers=headers)
|
|
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["status"] == "active"
|
|
assert body["documents_found"] == 1
|