pryscraper/tests/test_monitor.py
cryptorugmunch 85dea0cb4c
All checks were successful
CI / lint (push) Successful in 48s
CI / typecheck (push) Successful in 1m11s
CI / test (push) Successful in 2m15s
CI / Secret scan (gitleaks) (push) Successful in 3m48s
CI / Security audit (bandit) (push) Successful in 2m39s
feat(sql): migrate intelligence, monitor, and referrals to SQLAlchemy (#4)
2026-07-03 01:29:59 +02:00

170 lines
4.8 KiB
Python

# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Rug Munch Media LLC
#
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
# Licensed under MIT. See LICENSE.
"""Tests for content monitors."""
from unittest.mock import AsyncMock, patch
import pytest
from monitor import ChangeJudger, create_monitor, delete_monitor, list_monitors, run_monitor
@pytest.fixture
def temp_db(monkeypatch, tmp_path):
import db as db_module
db_path = tmp_path / "test.db"
monkeypatch.setenv("PRY_DATABASE_URL", f"sqlite:///{db_path}")
monkeypatch.setenv("PRY_DATA_DIR", str(tmp_path))
db_module._engine = None
db_module._SessionLocal = None
db_module.get_engine()
yield
db_module._engine = None
db_module._SessionLocal = None
@pytest.mark.asyncio
async def test_create_monitor(temp_db):
result = await create_monitor("test-monitor", "https://example.com")
assert result["name"] == "test-monitor"
assert result["target_url"] == "https://example.com"
assert len(result["id"]) == 12
assert result["status"] == "active"
@pytest.mark.asyncio
async def test_create_monitor_with_all_options(temp_db):
result = await create_monitor(
"full-monitor",
"https://example.com",
schedule_cron="0 */12 * * *",
goal="find price changes",
webhook_url="https://hooks.example.com/notify",
use_llm_judge=True,
)
assert result["name"] == "full-monitor"
assert result["schedule_cron"] == "0 */12 * * *"
assert result["goal"] == "find price changes"
assert result["webhook_url"] == "https://hooks.example.com/notify"
assert result["use_llm_judge"] is True
@pytest.mark.asyncio
async def test_list_monitors(temp_db):
await create_monitor("m1", "https://example.com")
await create_monitor("m2", "https://example.org")
monitors = await list_monitors()
assert len(monitors) == 2
@pytest.mark.asyncio
async def test_list_monitors_empty(temp_db):
monitors = await list_monitors()
assert monitors == []
@pytest.mark.asyncio
async def test_delete_monitor(temp_db):
m = await create_monitor("to-delete", "https://example.com")
assert await delete_monitor(m["id"]) is True
monitors = await list_monitors()
assert len(monitors) == 0
@pytest.mark.asyncio
async def test_delete_nonexistent_monitor(temp_db):
assert await delete_monitor("nonexistent") is False
@pytest.mark.asyncio
async def test_run_monitor_with_change(temp_db):
m = await create_monitor("change-test", "https://example.com")
mock_scraper = AsyncMock()
mock_scraper.scrape.return_value = {"content": "new content here"}
with patch("scraper.PryScraper", return_value=mock_scraper):
result = await run_monitor(m["id"])
assert result["monitor_id"] == m["id"]
assert result["current_content_length"] > 0
@pytest.mark.asyncio
async def test_run_monitor_no_change(temp_db):
m = await create_monitor("no-change", "https://example.com")
mock_scraper = AsyncMock()
mock_scraper.scrape.return_value = {"content": ""}
with patch("scraper.PryScraper", return_value=mock_scraper):
result = await run_monitor(m["id"])
assert result["changed"] is False
@pytest.mark.asyncio
async def test_run_monitor_not_found(temp_db):
result = await run_monitor("nonexistent")
assert "error" in result
@pytest.mark.asyncio
async def test_run_monitor_scrape_error(temp_db):
m = await create_monitor("scrape-error", "https://example.com")
mock_scraper = AsyncMock()
mock_scraper.scrape.side_effect = ValueError("connection refused")
with patch("scraper.PryScraper", return_value=mock_scraper):
result = await run_monitor(m["id"])
assert "error" in result
async def _judge(previous: str, current: str, goal: str = "") -> dict:
j = ChangeJudger()
return await j.judge(previous, current, goal)
def test_judger_no_goal_no_change() -> None:
import asyncio
result = asyncio.run(_judge("same content", "same content"))
assert result["meaningful"] is False
def test_judger_no_goal_with_change() -> None:
import asyncio
result = asyncio.run(_judge("old content", "new content"))
assert result["meaningful"] is True
def test_judger_with_goal_relevant() -> None:
import asyncio
result = asyncio.run(
_judge("Company has 10 employees", "Company has 500 employees", goal="employee count")
)
assert result["meaningful"] is True
def test_judger_with_goal_irrelevant() -> None:
import asyncio
result = asyncio.run(
_judge("Company has 10 employees", "Company is located in NY", goal="employee count")
)
assert result["meaningful"] is False
def test_judger_empty_previous() -> None:
import asyncio
result = asyncio.run(_judge("", "New content here"))
assert result["meaningful"] is True