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,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from app.collectors.base import CompanyContext, SourceConfig
|
||||
from app.collectors.rss import RssCollector
|
||||
from app.models.enums import SourceStatus, SourceType
|
||||
|
||||
FEED_XML = """<?xml version="1.0"?>
|
||||
<rss version="2.0"><channel>
|
||||
<title>Acme News</title>
|
||||
<item>
|
||||
<title>Acme opens new facility</title>
|
||||
<link>https://example.com/news/1</link>
|
||||
<description>Acme Corp opened a new manufacturing facility this week.</description>
|
||||
<pubDate>Mon, 01 Jun 2026 10:00:00 GMT</pubDate>
|
||||
</item>
|
||||
<item>
|
||||
<title>Acme hires new VP</title>
|
||||
<link>https://example.com/news/2</link>
|
||||
<description>Acme Corp announced a new VP of Engineering.</description>
|
||||
</item>
|
||||
</channel></rss>
|
||||
"""
|
||||
|
||||
COMPANY = CompanyContext(id="c1", name="Acme Corp", official_website=None, monitoring_focus=None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discover_builds_a_google_news_search_url():
|
||||
discovered = await RssCollector().discover(COMPANY)
|
||||
|
||||
assert len(discovered) == 1
|
||||
assert discovered[0].source_type == SourceType.RSS
|
||||
assert discovered[0].base_url.startswith("https://news.google.com/rss/search?q=")
|
||||
assert "Acme%20Corp" in discovered[0].base_url
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discover_url_encodes_ampersands_in_the_name():
|
||||
company = CompanyContext(id="c1", name="AT&T", official_website=None, monitoring_focus=None)
|
||||
discovered = await RssCollector().discover(company)
|
||||
|
||||
# A raw "&" from the company name must not leak into the query string,
|
||||
# since it would be parsed as a new URL parameter separator (splitting
|
||||
# the query in two) rather than part of the search text.
|
||||
query_part = discovered[0].base_url.split("q=", 1)[1].split("&hl=", 1)[0]
|
||||
assert "&" not in query_part
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_parses_feed_items():
|
||||
source = SourceConfig(
|
||||
id="s1", source_type=SourceType.RSS, name="Acme RSS", base_url="https://example.com/feed"
|
||||
)
|
||||
with respx.mock:
|
||||
respx.get("https://example.com/feed").mock(return_value=httpx.Response(200, text=FEED_XML))
|
||||
result = await RssCollector().collect(source, COMPANY)
|
||||
|
||||
assert result.status == SourceStatus.ACTIVE
|
||||
assert len(result.documents) == 2
|
||||
assert result.documents[0].title == "Acme opens new facility"
|
||||
assert result.documents[0].publication_date is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_fails_gracefully_on_http_error():
|
||||
source = SourceConfig(
|
||||
id="s1", source_type=SourceType.RSS, name="Acme RSS", base_url="https://example.com/feed"
|
||||
)
|
||||
with respx.mock:
|
||||
respx.get("https://example.com/feed").mock(return_value=httpx.Response(500))
|
||||
result = await RssCollector().collect(source, COMPANY)
|
||||
|
||||
assert result.status == SourceStatus.FAILED
|
||||
assert result.documents == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_without_base_url_fails_without_network_call():
|
||||
source = SourceConfig(id="s1", source_type=SourceType.RSS, name="Acme RSS", base_url=None)
|
||||
result = await RssCollector().collect(source, COMPANY)
|
||||
assert result.status == SourceStatus.FAILED
|
||||
Reference in New Issue
Block a user