Files
CIAgent/apps/api/tests/unit/test_sources_api.py
saksham 1a4c80958f 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).
2026-08-05 10:48:20 -04:00

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