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,254 @@
|
||||
"""Company CRUD, monitor configuration, and ownership isolation tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
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, **overrides):
|
||||
payload = {
|
||||
"name": "Acme Mobility Systems",
|
||||
"official_website": "acme-mobility.example.com",
|
||||
"monitoring_focus": "EV manufacturing expansion",
|
||||
"competitor_names": ["Rival Motors"],
|
||||
"alias_names": ["Acme"],
|
||||
"frequency_type": "weekly",
|
||||
**overrides,
|
||||
}
|
||||
return client.post("/api/v1/companies", json=payload, headers=headers)
|
||||
|
||||
|
||||
def test_create_company_normalizes_website_and_sets_defaults(client):
|
||||
headers = _register_and_login(client)
|
||||
resp = _create_company(client, headers)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["official_website"] == "https://acme-mobility.example.com"
|
||||
assert body["slug"] == "acme-mobility-systems"
|
||||
assert body["status"] == "active"
|
||||
assert body["aliases"] == ["Acme"]
|
||||
assert body["competitors"] == ["Rival Motors"]
|
||||
assert body["monitor_configuration"]["frequency_type"] == "weekly"
|
||||
assert body["monitor_configuration"]["enabled"] is True
|
||||
assert body["monitor_configuration"]["next_run"] is not None
|
||||
|
||||
|
||||
def test_duplicate_company_names_get_distinct_slugs(client):
|
||||
headers = _register_and_login(client)
|
||||
first = _create_company(client, headers).json()
|
||||
second = _create_company(client, headers).json()
|
||||
assert first["slug"] != second["slug"]
|
||||
|
||||
|
||||
def test_duplicate_company_names_get_a_unique_display_name(client):
|
||||
headers = _register_and_login(client)
|
||||
first = _create_company(client, headers).json()
|
||||
second = _create_company(client, headers).json()
|
||||
third = _create_company(client, headers).json()
|
||||
|
||||
assert first["name"] == "Acme Mobility Systems"
|
||||
assert second["name"] == "Acme Mobility Systems (2)"
|
||||
assert third["name"] == "Acme Mobility Systems (3)"
|
||||
|
||||
|
||||
def test_company_name_uniqueness_is_case_insensitive(client):
|
||||
headers = _register_and_login(client)
|
||||
first = _create_company(client, headers, name="Stripe").json()
|
||||
second = _create_company(client, headers, name="STRIPE").json()
|
||||
|
||||
assert first["name"] == "Stripe"
|
||||
assert second["name"] == "STRIPE (2)"
|
||||
|
||||
|
||||
def test_company_name_uniqueness_is_scoped_per_user(client):
|
||||
owner_headers = _register_and_login(client)
|
||||
other_headers = _register_and_login(client)
|
||||
owner_company = _create_company(client, owner_headers).json()
|
||||
other_company = _create_company(client, other_headers).json()
|
||||
|
||||
# Two different users can each have a company with the exact same name -
|
||||
# uniqueness is per-user, not global.
|
||||
assert owner_company["name"] == other_company["name"] == "Acme Mobility Systems"
|
||||
|
||||
|
||||
def test_company_list_and_detail_are_scoped_to_owner(client):
|
||||
owner_headers = _register_and_login(client)
|
||||
other_headers = _register_and_login(client)
|
||||
|
||||
created = _create_company(client, owner_headers).json()
|
||||
|
||||
owner_list = client.get("/api/v1/companies", headers=owner_headers).json()
|
||||
assert any(c["id"] == created["id"] for c in owner_list)
|
||||
|
||||
other_list = client.get("/api/v1/companies", headers=other_headers).json()
|
||||
assert all(c["id"] != created["id"] for c in other_list)
|
||||
|
||||
other_detail = client.get(f"/api/v1/companies/{created['id']}", headers=other_headers)
|
||||
assert other_detail.status_code == 404
|
||||
|
||||
owner_detail = client.get(f"/api/v1/companies/{created['id']}", headers=owner_headers)
|
||||
assert owner_detail.status_code == 200
|
||||
|
||||
|
||||
def test_pause_and_resume_company_toggles_monitor_enabled(client):
|
||||
headers = _register_and_login(client)
|
||||
company = _create_company(client, headers).json()
|
||||
|
||||
paused = client.post(f"/api/v1/companies/{company['id']}/pause", headers=headers).json()
|
||||
assert paused["status"] == "paused"
|
||||
assert paused["monitor_configuration"]["enabled"] is False
|
||||
|
||||
resumed = client.post(f"/api/v1/companies/{company['id']}/resume", headers=headers).json()
|
||||
assert resumed["status"] == "active"
|
||||
assert resumed["monitor_configuration"]["enabled"] is True
|
||||
|
||||
|
||||
def test_update_company_replaces_aliases_and_competitors(client):
|
||||
headers = _register_and_login(client)
|
||||
company = _create_company(client, headers).json()
|
||||
|
||||
resp = client.patch(
|
||||
f"/api/v1/companies/{company['id']}",
|
||||
json={"alias_names": ["New Alias"], "competitor_names": []},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["aliases"] == ["New Alias"]
|
||||
assert body["competitors"] == []
|
||||
|
||||
|
||||
def test_delete_company_removes_it(client):
|
||||
headers = _register_and_login(client)
|
||||
company = _create_company(client, headers).json()
|
||||
|
||||
resp = client.delete(f"/api/v1/companies/{company['id']}", headers=headers)
|
||||
assert resp.status_code == 204
|
||||
|
||||
resp = client.get(f"/api/v1/companies/{company['id']}", headers=headers)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_monitor_configuration_get_and_patch(client):
|
||||
headers = _register_and_login(client)
|
||||
company = _create_company(client, headers).json()
|
||||
|
||||
resp = client.get(f"/api/v1/companies/{company['id']}/monitor", headers=headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["frequency_type"] == "weekly"
|
||||
|
||||
patch_resp = client.patch(
|
||||
f"/api/v1/companies/{company['id']}/monitor",
|
||||
json={"frequency_type": "daily"},
|
||||
headers=headers,
|
||||
)
|
||||
assert patch_resp.status_code == 200
|
||||
assert patch_resp.json()["frequency_type"] == "daily"
|
||||
|
||||
|
||||
def test_custom_schedule_below_minimum_interval_is_rejected(client, settings):
|
||||
headers = _register_and_login(client)
|
||||
resp = _create_company(
|
||||
client,
|
||||
headers,
|
||||
name="Too Frequent Co",
|
||||
frequency_type="custom",
|
||||
interval_minutes=settings.minimum_monitoring_interval_minutes - 1,
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_custom_schedule_with_valid_interval_is_accepted(client, settings):
|
||||
headers = _register_and_login(client)
|
||||
resp = _create_company(
|
||||
client,
|
||||
headers,
|
||||
name="Custom Interval Co",
|
||||
frequency_type="custom",
|
||||
interval_minutes=settings.minimum_monitoring_interval_minutes + 30,
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.json()["monitor_configuration"]["interval_minutes"] == (
|
||||
settings.minimum_monitoring_interval_minutes + 30
|
||||
)
|
||||
|
||||
|
||||
def test_custom_schedule_requires_interval_or_cron(client):
|
||||
headers = _register_and_login(client)
|
||||
resp = _create_company(client, headers, name="No Schedule Co", frequency_type="custom")
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_company_creation_requires_authentication(client):
|
||||
resp = client.post("/api/v1/companies", json={"name": "No Auth Co"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_max_companies_per_user_enforced(client, settings, monkeypatch):
|
||||
headers = _register_and_login(client)
|
||||
from app.core import config as config_module
|
||||
|
||||
limited_settings = settings.model_copy(update={"max_companies_per_user": 1})
|
||||
from app.main import app
|
||||
|
||||
app.dependency_overrides[config_module.get_settings] = lambda: limited_settings
|
||||
try:
|
||||
first = _create_company(client, headers, name="Company One")
|
||||
assert first.status_code == 201
|
||||
second = _create_company(client, headers, name="Company Two")
|
||||
assert second.status_code == 409
|
||||
finally:
|
||||
app.dependency_overrides.pop(config_module.get_settings, None)
|
||||
|
||||
|
||||
def test_create_company_enqueues_enrichment_when_a_key_is_configured(client, settings):
|
||||
"""The real regression guard is every OTHER test in this suite: none of
|
||||
them configure NINJAPEAR_API_KEY, so the whole rest of the suite proves
|
||||
the enqueue is skipped by default (see the "without a key" test below
|
||||
for the direct assertion)."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.core import config as config_module
|
||||
from app.main import app
|
||||
|
||||
key_settings = settings.model_copy(update={"ninjapear_api_key": "test-key"})
|
||||
app.dependency_overrides[config_module.get_settings] = lambda: key_settings
|
||||
try:
|
||||
headers = _register_and_login(client)
|
||||
with patch("app.tasks.enrichment.enrich_company.delay") as mock_delay:
|
||||
resp = _create_company(client, headers, name="Enriched Co")
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
mock_delay.assert_called_once_with(body["id"])
|
||||
assert body["enrichment"] == {
|
||||
"status": "pending",
|
||||
"data": {},
|
||||
"errors": {},
|
||||
"credits_spent": None,
|
||||
"fetched_at": None,
|
||||
}
|
||||
finally:
|
||||
app.dependency_overrides.pop(config_module.get_settings, None)
|
||||
|
||||
|
||||
def test_create_company_does_not_enqueue_enrichment_without_a_key(client):
|
||||
from unittest.mock import patch
|
||||
|
||||
headers = _register_and_login(client)
|
||||
with patch("app.tasks.enrichment.enrich_company.delay") as mock_delay:
|
||||
resp = _create_company(client, headers, name="Plain Co")
|
||||
assert resp.status_code == 201
|
||||
mock_delay.assert_not_called()
|
||||
assert resp.json()["enrichment"] is None
|
||||
Reference in New Issue
Block a user