"""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="Pricing" "

Pricing

Plans start at $10/month.

" "", ) ) 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