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:
2026-05-02 23:39:43 +02:00
parent 1285bc8571
commit e436727942
11 changed files with 144 additions and 164 deletions

View File

@@ -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