"""Regression test for the collection + change-detection pipeline using the Acme Mobility Systems v1/v2 fixture HTML under tests/fixtures/acme_mobility/, read straight off disk and run through the real pipeline end-to-end - so a change to those fixtures, or a regression in the pipeline, breaks a test here rather than going unnoticed. No live network: respx mocks every HTTP call for a fictitious acme-mobility.example host.""" from __future__ import annotations import uuid from pathlib import Path from unittest.mock import patch import httpx import pytest import respx from sqlalchemy import select from sqlalchemy.orm import selectinload from app.models.company import Company from app.models.enums import ChangeType, SourceType from app.models.monitor_configuration import MonitorConfiguration from app.repositories.source_repository import SnapshotRepository, SourceRepository from app.services import collection_service from app.services.change_detection_service import detect_change_for_source _FIXTURE_ROOT = Path(__file__).resolve().parent.parent / "fixtures" / "acme_mobility" _HOST = "https://acme-mobility.example" _PAGES = ["about", "products", "careers", "press", "pricing"] def _page_html(version: str, page: str) -> str: return (_FIXTURE_ROOT / version / f"{page}.html").read_text(encoding="utf-8") @pytest.fixture(autouse=True) def _no_real_dns(): with patch("socket.getaddrinfo", return_value=[(2, 1, 6, "", ("93.184.216.34", 0))]): yield async def _make_company(db_session) -> Company: company = Company( id=uuid.uuid4(), user_id=uuid.uuid4(), name="Acme Mobility Systems (Demo)", slug=f"acme-mobility-demo-{uuid.uuid4().hex[:6]}", official_website=f"{_HOST}/about", monitoring_focus="leadership changes, pricing, hiring, expansion", ) db_session.add(company) db_session.add(MonitorConfiguration(company_id=company.id)) await db_session.commit() result = await db_session.execute( select(Company) .where(Company.id == company.id) .options( selectinload(Company.aliases), selectinload(Company.competitors), selectinload(Company.enrichment), ) ) return result.scalar_one() def _mock_version(version: str): respx.get(f"{_HOST}/robots.txt").mock(return_value=httpx.Response(404)) for page in _PAGES: respx.get(f"{_HOST}/{page}").mock( return_value=httpx.Response(200, html=_page_html(version, page)) ) @pytest.mark.asyncio async def test_acme_v1_to_v2_produces_the_expected_change_types(db_session, settings): company = await _make_company(db_session) repo = SourceRepository(db_session) sources = {} for page in _PAGES: sources[page] = await repo.create( company_id=company.id, source_type=SourceType.CUSTOM_URL, name=f"Acme Mobility - {page.title()}", base_url=f"{_HOST}/{page}", ) await db_session.commit() # Baseline run (v1): every source collects successfully, no prior # snapshot to diff against yet. with respx.mock: _mock_version("v1") for page in _PAGES: result = await collection_service.collect_source( db_session, settings, sources[page], company ) assert result.status.value == "active", f"{page} baseline collection failed" # Second run (v2): about/careers/press/pricing changed, products did not. with respx.mock: _mock_version("v2") for page in _PAGES: await collection_service.collect_source(db_session, settings, sources[page], company) snapshot_repo = SnapshotRepository(db_session) changes: dict[str, ChangeType | None] = {} for page in _PAGES: current = await snapshot_repo.latest_for_source(sources[page].id) change = await detect_change_for_source( db_session, sources[page], company, current, uuid.uuid4() ) changes[page] = change.change_type if change else None assert changes["about"] == ChangeType.LEADERSHIP_CHANGE assert changes["pricing"] == ChangeType.PRICE_CHANGE assert changes["products"] is None # identical content -> hash short-circuit, no change assert changes["careers"] is not None # new job listing -> detected as a real change assert changes["press"] is not None # new press entry -> detected as a real change