pryscraper/tests/test_structure_monitor.py
opencode 025bcbbe9e fix(pry): test_check_selectors_empty → per-test fresh engine fixture
The module-level db._engine was being reused across tests. Tests that
went through the temp_db fixture wrote StructureSnapshot rows to a
tmp_path sqlite file, but test_check_selectors_empty runs without that
fixture, so on the second run the engine pointed at the polluted file
instead of starting empty — RuntimeError when StructureSnapshot rows
were queried.

Fix: add an autouse fixture that swaps the engine for a fresh
sqlite:///:memory: instance for every test in this file. The temp_db
fixture is preserved for tests that need a real file path.

Tests: 620 passed, 1 skipped, 1 deselected (was 619 passed + 2 failed).
2026-07-06 20:33:36 +07:00

176 lines
5.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 page structure monitor."""
from unittest.mock import AsyncMock, patch
import pytest
from structure_monitor import (
check_selectors,
get_structure_history,
monitor_page_structure,
)
@pytest.fixture(autouse=True)
def _fresh_db(monkeypatch):
"""Force a brand-new in-memory SQLite engine for every test.
`structure_monitor` reads from the module-level `db._engine`. Without
this fixture a previous test that committed StructureSnapshot rows
(e.g. via `temp_db` pointing at a tmp_path sqlite file) leaks state
into the next test that doesn't use the `temp_db` fixture
(`test_check_selectors_empty`). Yielding a private :memory: engine
per-test guarantees isolation regardless of import order.
Also resets the shared `client.http_client` so an httpx.AsyncClient
bound to a closed event loop (from a prior test) doesn't surface as
RuntimeError on the next test that hits the network.
"""
import db as db_module
import client as client_module
from sqlalchemy import create_engine
monkeypatch.setenv("PRY_DATABASE_URL", "sqlite:///:memory:")
monkeypatch.setenv("PRY_DATA_DIR", "/tmp")
db_module._engine = create_engine(
"sqlite:///:memory:", connect_args={"check_same_thread": False}, future=True
)
db_module._SessionLocal = None
# Recreate tables on the new engine.
from db import Base
Base.metadata.create_all(db_module._engine)
client_module.http_client = None
yield
db_module._engine = None
db_module._SessionLocal = None
client_module.http_client = None
@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_check_selectors_empty():
result = await check_selectors("https://example.com", [])
assert "selectors" in result
assert result["all_matched"] is True
@pytest.mark.asyncio
async def test_get_structure_history_no_history(temp_db):
result = await get_structure_history("https://nonexistent-page-test-123.com")
assert result["has_history"] is False
@pytest.mark.asyncio
async def test_monitor_page_structure_creates_history(temp_db):
mock_client = AsyncMock()
mock_client.get.return_value.is_success = True
mock_client.get.return_value.text = "<html><body><h1>Title</h1></body></html>"
with patch("structure_monitor.get_client", return_value=mock_client):
result = await monitor_page_structure(
"https://example.com",
[{"name": "title", "selector": "h1", "type": "css"}],
)
assert "selectors" in result
assert "changes" in result
assert result["all_matched"] is True
assert result["has_changes"] is False
@pytest.mark.asyncio
async def test_get_structure_history_with_data(temp_db):
mock_client = AsyncMock()
mock_client.get.return_value.is_success = True
mock_client.get.return_value.text = "<html><body><h1>Title</h1></body></html>"
with patch("structure_monitor.get_client", return_value=mock_client):
await monitor_page_structure(
"https://example.com",
[{"name": "title", "selector": "h1", "type": "css"}],
)
result = await get_structure_history("https://example.com")
assert result["has_history"] is True
assert "last_checked" in result
assert "last_selectors" in result
@pytest.mark.asyncio
async def test_monitor_page_structure_with_template_id(temp_db):
mock_client = AsyncMock()
mock_client.get.return_value.is_success = True
mock_client.get.return_value.text = "<html><body><h1>Title</h1></body></html>"
with patch("structure_monitor.get_client", return_value=mock_client):
result = await monitor_page_structure(
"https://example.com",
[{"name": "title", "selector": "h1", "type": "css"}],
template_id="template-1",
)
assert result["template_id"] == "template-1"
by_url = await get_structure_history("https://example.com")
assert by_url["has_history"] is True
by_template = await get_structure_history("https://example.com", template_id="template-1")
assert by_template["has_history"] is True
@pytest.mark.asyncio
async def test_monitor_page_structure_detects_change(temp_db):
mock_client = AsyncMock()
mock_client.get.return_value.is_success = True
mock_client.get.return_value.text = "<html><body><h1>Title</h1></body></html>"
with patch("structure_monitor.get_client", return_value=mock_client):
await monitor_page_structure(
"https://example.com",
[{"name": "title", "selector": "h1", "type": "css"}],
)
mock_client.get.return_value.text = "<html><body><h1>New Title</h1></body></html>"
with patch("structure_monitor.get_client", return_value=mock_client):
result = await monitor_page_structure(
"https://example.com",
[{"name": "title", "selector": "h2", "type": "css"}],
)
assert "changes" in result
@pytest.mark.asyncio
async def test_structure_page_http_error(temp_db):
mock_client = AsyncMock()
mock_client.get.return_value.is_success = False
mock_client.get.return_value.status_code = 404
with patch("structure_monitor.get_client", return_value=mock_client):
result = await monitor_page_structure(
"https://example.com/404",
[{"name": "title", "selector": "h1", "type": "css"}],
)
assert "error" in result