fix: atomic upsert for import runs (Issue #12)
Replace check-then-insert race condition with INSERT ON CONFLICT. - upsert_pending uses RETURNING id for atomic upsert - UNIQUE(source_id, content_hash) constraint from migration 6 - blocklist_import_workflow updated to use upsert_pending - test_import_source_success fixed for async mock patterns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -259,6 +259,8 @@ class TestBlocklistImportWorkflow:
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_source_success(self) -> None:
|
||||
"""Test successful import workflow."""
|
||||
from app.repositories import import_run_repo
|
||||
|
||||
http_session = MagicMock(spec=aiohttp.ClientSession)
|
||||
response = AsyncMock()
|
||||
response.status = 200
|
||||
@@ -280,7 +282,27 @@ class TestBlocklistImportWorkflow:
|
||||
)
|
||||
|
||||
db = MagicMock()
|
||||
result = await workflow.import_source(source, "/var/run/fail2ban/fail2ban.sock", db)
|
||||
db.commit = AsyncMock()
|
||||
|
||||
# Mock get_by_source_and_hash to return None (no existing run)
|
||||
mock_existing_cursor = MagicMock()
|
||||
mock_existing_cursor.fetchone = AsyncMock(return_value=None)
|
||||
|
||||
async def mock_existing_aenter(self):
|
||||
return mock_existing_cursor
|
||||
|
||||
async def mock_existing_aexit(self, *args):
|
||||
return None
|
||||
|
||||
mock_existing_cursor.__aenter__ = mock_existing_aenter
|
||||
mock_existing_cursor.__aexit__ = mock_existing_aexit
|
||||
|
||||
# Patch upsert_pending to return a run_id without touching real DB
|
||||
with patch.object(import_run_repo, "upsert_pending", new=AsyncMock(return_value=42)):
|
||||
with patch.object(import_run_repo, "mark_completed", new=AsyncMock()):
|
||||
# Mock get_by_source_and_hash to return None (no existing run)
|
||||
with patch.object(import_run_repo, "get_by_source_and_hash", new=AsyncMock(return_value=None)):
|
||||
result = await workflow.import_source(source, "/var/run/fail2ban/fail2ban.sock", db)
|
||||
|
||||
assert result.source_id == 1
|
||||
assert result.ips_imported == 2
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.models.blocklist import (
|
||||
ScheduleConfig,
|
||||
ScheduleFrequency,
|
||||
)
|
||||
from app.repositories import import_log_repo
|
||||
from app.services import blocklist_service
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -115,6 +116,27 @@ class TestSourceCRUD:
|
||||
result = await blocklist_service.delete_source(db, 9999)
|
||||
assert result is False
|
||||
|
||||
@patch("app.utils.ip_utils.validate_blocklist_url")
|
||||
async def test_delete_source_with_logs_raises_error(
|
||||
self, mock_validate: AsyncMock, db: aiosqlite.Connection
|
||||
) -> None:
|
||||
"""delete_source raises BlocklistSourceHasLogsError when source has import logs."""
|
||||
mock_validate.return_value = None
|
||||
source = await blocklist_service.create_source(db, "HasLogs", "https://haslogs.test/")
|
||||
# Create an import log for this source
|
||||
await import_log_repo.add_log(
|
||||
db,
|
||||
source_id=source.id,
|
||||
source_url="https://haslogs.test/",
|
||||
ips_imported=10,
|
||||
ips_skipped=0,
|
||||
errors=None,
|
||||
)
|
||||
from app.exceptions import BlocklistSourceHasLogsError
|
||||
with pytest.raises(BlocklistSourceHasLogsError) as exc_info:
|
||||
await blocklist_service.delete_source(db, source.id)
|
||||
assert exc_info.value.source_id == source.id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preview
|
||||
|
||||
Reference in New Issue
Block a user