- Create ScanService class (src/server/services/scan_service.py) - Use 'key' as primary series identifier throughout - Include 'folder' as metadata only for display purposes - Implement scan progress tracking via ProgressService - Add callback classes for progress, error, and completion - Support scan event subscription and broadcasting - Maintain scan history with configurable limit - Provide cancellation support for in-progress scans - Create comprehensive unit tests (tests/unit/test_scan_service.py) - 38 tests covering all functionality - Test ScanProgress dataclass serialization - Test callback classes (progress, error, completion) - Test service lifecycle (start, cancel, status) - Test event subscription and broadcasting - Test key-based identification throughout - Test singleton pattern - Update infrastructure.md with ScanService documentation - Document service overview and key features - Document components and event types - Document integration points - Include usage example - Update instructions.md - Mark Task 3.4 as complete - Mark Phase 3 as fully complete - Remove finished task definition Task: Phase 3, Task 3.4 - Update ScanService to Use Key Completion Date: November 27, 2025
740 lines
25 KiB
Python
740 lines
25 KiB
Python
"""Unit tests for ScanService.
|
|
|
|
This module contains comprehensive tests for the scan service,
|
|
including scan lifecycle, progress callbacks, event handling,
|
|
and key-based identification.
|
|
"""
|
|
from datetime import datetime
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.core.interfaces.callbacks import (
|
|
CallbackManager,
|
|
CompletionContext,
|
|
ErrorContext,
|
|
OperationType,
|
|
ProgressContext,
|
|
ProgressPhase,
|
|
)
|
|
from src.server.services.scan_service import (
|
|
ScanProgress,
|
|
ScanService,
|
|
ScanServiceCompletionCallback,
|
|
ScanServiceError,
|
|
ScanServiceErrorCallback,
|
|
ScanServiceProgressCallback,
|
|
get_scan_service,
|
|
reset_scan_service,
|
|
)
|
|
|
|
|
|
class TestScanProgress:
|
|
"""Test ScanProgress class."""
|
|
|
|
def test_scan_progress_creation(self):
|
|
"""Test creating a scan progress instance."""
|
|
progress = ScanProgress("scan-123")
|
|
|
|
assert progress.scan_id == "scan-123"
|
|
assert progress.status == "started"
|
|
assert progress.current == 0
|
|
assert progress.total == 0
|
|
assert progress.percentage == 0.0
|
|
assert progress.message == "Initializing scan..."
|
|
assert progress.key is None
|
|
assert progress.folder is None
|
|
assert progress.series_found == 0
|
|
assert progress.errors == []
|
|
assert isinstance(progress.started_at, datetime)
|
|
assert isinstance(progress.updated_at, datetime)
|
|
|
|
def test_scan_progress_to_dict_basic(self):
|
|
"""Test converting scan progress to dictionary without key/folder."""
|
|
progress = ScanProgress("scan-123")
|
|
progress.current = 5
|
|
progress.total = 10
|
|
progress.percentage = 50.0
|
|
progress.status = "in_progress"
|
|
progress.message = "Scanning..."
|
|
|
|
result = progress.to_dict()
|
|
|
|
assert result["scan_id"] == "scan-123"
|
|
assert result["status"] == "in_progress"
|
|
assert result["current"] == 5
|
|
assert result["total"] == 10
|
|
assert result["percentage"] == 50.0
|
|
assert result["message"] == "Scanning..."
|
|
assert result["series_found"] == 0
|
|
assert result["errors"] == []
|
|
assert "started_at" in result
|
|
assert "updated_at" in result
|
|
# key and folder should not be present when None
|
|
assert "key" not in result
|
|
assert "folder" not in result
|
|
|
|
def test_scan_progress_to_dict_with_key_and_folder(self):
|
|
"""Test converting scan progress to dictionary with key and folder."""
|
|
progress = ScanProgress("scan-123")
|
|
progress.key = "attack-on-titan"
|
|
progress.folder = "Attack on Titan (2013)"
|
|
progress.series_found = 5
|
|
|
|
result = progress.to_dict()
|
|
|
|
assert result["key"] == "attack-on-titan"
|
|
assert result["folder"] == "Attack on Titan (2013)"
|
|
assert result["series_found"] == 5
|
|
|
|
def test_scan_progress_to_dict_with_errors(self):
|
|
"""Test scan progress with error messages."""
|
|
progress = ScanProgress("scan-123")
|
|
progress.errors = ["Error 1", "Error 2"]
|
|
|
|
result = progress.to_dict()
|
|
|
|
assert result["errors"] == ["Error 1", "Error 2"]
|
|
|
|
|
|
class TestScanServiceProgressCallback:
|
|
"""Test ScanServiceProgressCallback class."""
|
|
|
|
@pytest.fixture
|
|
def mock_service(self):
|
|
"""Create a mock ScanService."""
|
|
service = MagicMock(spec=ScanService)
|
|
service._handle_progress_update = AsyncMock()
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def scan_progress(self):
|
|
"""Create a scan progress instance."""
|
|
return ScanProgress("scan-123")
|
|
|
|
def test_on_progress_updates_progress(self, mock_service, scan_progress):
|
|
"""Test that on_progress updates scan progress correctly."""
|
|
callback = ScanServiceProgressCallback(mock_service, scan_progress)
|
|
|
|
context = ProgressContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
phase=ProgressPhase.IN_PROGRESS,
|
|
current=5,
|
|
total=10,
|
|
percentage=50.0,
|
|
message="Scanning: Test Folder",
|
|
key="test-series",
|
|
folder="Test Folder",
|
|
)
|
|
|
|
# Call directly - no event loop needed since we handle RuntimeError
|
|
callback.on_progress(context)
|
|
|
|
assert scan_progress.current == 5
|
|
assert scan_progress.total == 10
|
|
assert scan_progress.percentage == 50.0
|
|
assert scan_progress.message == "Scanning: Test Folder"
|
|
assert scan_progress.key == "test-series"
|
|
assert scan_progress.folder == "Test Folder"
|
|
assert scan_progress.status == "in_progress"
|
|
|
|
def test_on_progress_starting_phase(self, mock_service, scan_progress):
|
|
"""Test progress callback with STARTING phase."""
|
|
callback = ScanServiceProgressCallback(mock_service, scan_progress)
|
|
|
|
context = ProgressContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
phase=ProgressPhase.STARTING,
|
|
current=0,
|
|
total=0,
|
|
percentage=0.0,
|
|
message="Initializing...",
|
|
)
|
|
|
|
callback.on_progress(context)
|
|
|
|
assert scan_progress.status == "started"
|
|
|
|
def test_on_progress_completed_phase(self, mock_service, scan_progress):
|
|
"""Test progress callback with COMPLETED phase."""
|
|
callback = ScanServiceProgressCallback(mock_service, scan_progress)
|
|
|
|
context = ProgressContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
phase=ProgressPhase.COMPLETED,
|
|
current=10,
|
|
total=10,
|
|
percentage=100.0,
|
|
message="Scan completed",
|
|
)
|
|
|
|
callback.on_progress(context)
|
|
|
|
assert scan_progress.status == "completed"
|
|
|
|
|
|
class TestScanServiceErrorCallback:
|
|
"""Test ScanServiceErrorCallback class."""
|
|
|
|
@pytest.fixture
|
|
def mock_service(self):
|
|
"""Create a mock ScanService."""
|
|
service = MagicMock(spec=ScanService)
|
|
service._handle_scan_error = AsyncMock()
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def scan_progress(self):
|
|
"""Create a scan progress instance."""
|
|
return ScanProgress("scan-123")
|
|
|
|
def test_on_error_adds_error_message(self, mock_service, scan_progress):
|
|
"""Test that on_error adds error to scan progress."""
|
|
callback = ScanServiceErrorCallback(mock_service, scan_progress)
|
|
|
|
error = ValueError("Test error")
|
|
context = ErrorContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
error=error,
|
|
message="Failed to process folder",
|
|
recoverable=True,
|
|
key="test-series",
|
|
folder="Test Folder",
|
|
)
|
|
|
|
callback.on_error(context)
|
|
|
|
assert len(scan_progress.errors) == 1
|
|
assert "[Test Folder]" in scan_progress.errors[0]
|
|
assert "Failed to process folder" in scan_progress.errors[0]
|
|
|
|
def test_on_error_without_folder(self, mock_service, scan_progress):
|
|
"""Test error callback without folder information."""
|
|
callback = ScanServiceErrorCallback(mock_service, scan_progress)
|
|
|
|
error = ValueError("Test error")
|
|
context = ErrorContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
error=error,
|
|
message="Generic error",
|
|
recoverable=False,
|
|
)
|
|
|
|
callback.on_error(context)
|
|
|
|
assert len(scan_progress.errors) == 1
|
|
assert scan_progress.errors[0] == "Generic error"
|
|
|
|
|
|
class TestScanServiceCompletionCallback:
|
|
"""Test ScanServiceCompletionCallback class."""
|
|
|
|
@pytest.fixture
|
|
def mock_service(self):
|
|
"""Create a mock ScanService."""
|
|
service = MagicMock(spec=ScanService)
|
|
service._handle_scan_completion = AsyncMock()
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def scan_progress(self):
|
|
"""Create a scan progress instance."""
|
|
return ScanProgress("scan-123")
|
|
|
|
def test_on_completion_success(self, mock_service, scan_progress):
|
|
"""Test completion callback with success."""
|
|
callback = ScanServiceCompletionCallback(mock_service, scan_progress)
|
|
|
|
context = CompletionContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
success=True,
|
|
message="Scan completed successfully",
|
|
statistics={"series_found": 10, "total_folders": 15},
|
|
)
|
|
|
|
callback.on_completion(context)
|
|
|
|
assert scan_progress.status == "completed"
|
|
assert scan_progress.message == "Scan completed successfully"
|
|
assert scan_progress.series_found == 10
|
|
|
|
def test_on_completion_failure(self, mock_service, scan_progress):
|
|
"""Test completion callback with failure."""
|
|
callback = ScanServiceCompletionCallback(mock_service, scan_progress)
|
|
|
|
context = CompletionContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id="scan-123",
|
|
success=False,
|
|
message="Scan failed: critical error",
|
|
)
|
|
|
|
callback.on_completion(context)
|
|
|
|
assert scan_progress.status == "failed"
|
|
assert scan_progress.message == "Scan failed: critical error"
|
|
|
|
|
|
class TestScanService:
|
|
"""Test ScanService class."""
|
|
|
|
@pytest.fixture
|
|
def mock_progress_service(self):
|
|
"""Create a mock progress service."""
|
|
service = MagicMock()
|
|
service.start_progress = AsyncMock()
|
|
service.update_progress = AsyncMock()
|
|
service.complete_progress = AsyncMock()
|
|
service.fail_progress = AsyncMock()
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def service(self, mock_progress_service):
|
|
"""Create a ScanService instance for each test."""
|
|
return ScanService(progress_service=mock_progress_service)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_service_initialization(self, service):
|
|
"""Test ScanService initialization."""
|
|
assert service.is_scanning is False
|
|
assert service.current_scan is None
|
|
assert service._scan_history == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_scan(self, service):
|
|
"""Test starting a new scan."""
|
|
scanner_factory = MagicMock()
|
|
|
|
scan_id = await service.start_scan(scanner_factory)
|
|
|
|
assert scan_id is not None
|
|
assert len(scan_id) > 0
|
|
assert service.is_scanning is True
|
|
assert service.current_scan is not None
|
|
assert service.current_scan.scan_id == scan_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_scan_while_scanning(self, service):
|
|
"""Test starting scan while another is in progress raises error."""
|
|
scanner_factory = MagicMock()
|
|
|
|
await service.start_scan(scanner_factory)
|
|
|
|
with pytest.raises(ScanServiceError, match="already in progress"):
|
|
await service.start_scan(scanner_factory)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_scan(self, service):
|
|
"""Test cancelling a scan in progress."""
|
|
scanner_factory = MagicMock()
|
|
scan_id = await service.start_scan(scanner_factory)
|
|
|
|
result = await service.cancel_scan()
|
|
|
|
assert result is True
|
|
assert service.is_scanning is False
|
|
assert service.current_scan.status == "cancelled"
|
|
assert len(service._scan_history) == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_scan_no_scan_in_progress(self, service):
|
|
"""Test cancelling when no scan is in progress."""
|
|
result = await service.cancel_scan()
|
|
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_scan_status(self, service):
|
|
"""Test getting scan status."""
|
|
status = await service.get_scan_status()
|
|
|
|
assert status["is_scanning"] is False
|
|
assert status["current_scan"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_scan_status_while_scanning(self, service):
|
|
"""Test getting scan status while scanning."""
|
|
scanner_factory = MagicMock()
|
|
scan_id = await service.start_scan(scanner_factory)
|
|
|
|
status = await service.get_scan_status()
|
|
|
|
assert status["is_scanning"] is True
|
|
assert status["current_scan"] is not None
|
|
assert status["current_scan"]["scan_id"] == scan_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_scan_history_empty(self, service):
|
|
"""Test getting scan history when empty."""
|
|
history = await service.get_scan_history()
|
|
|
|
assert history == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_scan_history_with_entries(self, service):
|
|
"""Test getting scan history with entries."""
|
|
# Start and cancel multiple scans to populate history
|
|
scanner_factory = MagicMock()
|
|
|
|
await service.start_scan(scanner_factory)
|
|
await service.cancel_scan()
|
|
|
|
await service.start_scan(scanner_factory)
|
|
await service.cancel_scan()
|
|
|
|
history = await service.get_scan_history()
|
|
|
|
assert len(history) == 2
|
|
# Should be newest first
|
|
assert history[0]["status"] == "cancelled"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_scan_history_limit(self, service):
|
|
"""Test scan history respects limit."""
|
|
scanner_factory = MagicMock()
|
|
|
|
# Create 3 history entries
|
|
for _ in range(3):
|
|
await service.start_scan(scanner_factory)
|
|
await service.cancel_scan()
|
|
|
|
history = await service.get_scan_history(limit=2)
|
|
|
|
assert len(history) == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subscribe_to_scan_events(self, service):
|
|
"""Test subscribing to scan events."""
|
|
handler = MagicMock()
|
|
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
assert handler in service._scan_event_handlers
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unsubscribe_from_scan_events(self, service):
|
|
"""Test unsubscribing from scan events."""
|
|
handler = MagicMock()
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
service.unsubscribe_from_scan_events(handler)
|
|
|
|
assert handler not in service._scan_event_handlers
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_emit_scan_event(self, service):
|
|
"""Test emitting scan events to handlers."""
|
|
handler = AsyncMock()
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
await service._emit_scan_event({
|
|
"type": "scan_progress",
|
|
"key": "test-series",
|
|
"folder": "Test Folder",
|
|
})
|
|
|
|
handler.assert_called_once_with({
|
|
"type": "scan_progress",
|
|
"key": "test-series",
|
|
"folder": "Test Folder",
|
|
})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_emit_scan_event_sync_handler(self, service):
|
|
"""Test emitting scan events to sync handlers."""
|
|
handler = MagicMock()
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
await service._emit_scan_event({
|
|
"type": "scan_progress",
|
|
"data": {"key": "test-series"},
|
|
})
|
|
|
|
handler.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_callback_manager(self, service):
|
|
"""Test creating a callback manager."""
|
|
scanner_factory = MagicMock()
|
|
await service.start_scan(scanner_factory)
|
|
|
|
callback_manager = service.create_callback_manager()
|
|
|
|
assert callback_manager is not None
|
|
assert isinstance(callback_manager, CallbackManager)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_callback_manager_no_current_scan(self, service):
|
|
"""Test creating callback manager without current scan."""
|
|
callback_manager = service.create_callback_manager()
|
|
|
|
assert callback_manager is not None
|
|
assert service.current_scan is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_progress_update(
|
|
self, service, mock_progress_service
|
|
):
|
|
"""Test handling progress update."""
|
|
scanner_factory = MagicMock()
|
|
await service.start_scan(scanner_factory)
|
|
|
|
scan_progress = service.current_scan
|
|
scan_progress.current = 5
|
|
scan_progress.total = 10
|
|
scan_progress.percentage = 50.0
|
|
scan_progress.message = "Processing..."
|
|
scan_progress.key = "test-series"
|
|
scan_progress.folder = "Test Folder"
|
|
|
|
await service._handle_progress_update(scan_progress)
|
|
|
|
mock_progress_service.update_progress.assert_called_once()
|
|
call_kwargs = mock_progress_service.update_progress.call_args.kwargs
|
|
assert call_kwargs["key"] == "test-series"
|
|
assert call_kwargs["folder"] == "Test Folder"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_scan_error(self, service):
|
|
"""Test handling scan error."""
|
|
handler = AsyncMock()
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
scanner_factory = MagicMock()
|
|
await service.start_scan(scanner_factory)
|
|
|
|
scan_progress = service.current_scan
|
|
error_context = ErrorContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id=scan_progress.scan_id,
|
|
error=ValueError("Test error"),
|
|
message="Test error message",
|
|
recoverable=True,
|
|
key="test-series",
|
|
folder="Test Folder",
|
|
)
|
|
|
|
await service._handle_scan_error(scan_progress, error_context)
|
|
|
|
# Handler is called twice: once for start, once for error
|
|
assert handler.call_count == 2
|
|
# Get the error event (second call)
|
|
error_event = handler.call_args_list[1][0][0]
|
|
assert error_event["type"] == "scan_error"
|
|
assert error_event["key"] == "test-series"
|
|
assert error_event["folder"] == "Test Folder"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_scan_completion_success(
|
|
self, service, mock_progress_service
|
|
):
|
|
"""Test handling successful scan completion."""
|
|
handler = AsyncMock()
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
scanner_factory = MagicMock()
|
|
scan_id = await service.start_scan(scanner_factory)
|
|
|
|
scan_progress = service.current_scan
|
|
completion_context = CompletionContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id=scan_id,
|
|
success=True,
|
|
message="Scan completed",
|
|
statistics={"series_found": 5, "total_folders": 10},
|
|
)
|
|
|
|
await service._handle_scan_completion(
|
|
scan_progress, completion_context
|
|
)
|
|
|
|
assert service.is_scanning is False
|
|
assert len(service._scan_history) == 1
|
|
mock_progress_service.complete_progress.assert_called_once()
|
|
# Handler is called twice: once for start, once for completion
|
|
assert handler.call_count == 2
|
|
# Get the completion event (second call)
|
|
completion_event = handler.call_args_list[1][0][0]
|
|
assert completion_event["type"] == "scan_completed"
|
|
assert completion_event["success"] is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_scan_completion_failure(
|
|
self, service, mock_progress_service
|
|
):
|
|
"""Test handling failed scan completion."""
|
|
handler = AsyncMock()
|
|
service.subscribe_to_scan_events(handler)
|
|
|
|
scanner_factory = MagicMock()
|
|
scan_id = await service.start_scan(scanner_factory)
|
|
|
|
scan_progress = service.current_scan
|
|
completion_context = CompletionContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id=scan_id,
|
|
success=False,
|
|
message="Scan failed: critical error",
|
|
)
|
|
|
|
await service._handle_scan_completion(
|
|
scan_progress, completion_context
|
|
)
|
|
|
|
assert service.is_scanning is False
|
|
mock_progress_service.fail_progress.assert_called_once()
|
|
# Handler is called twice: once for start, once for failure
|
|
assert handler.call_count == 2
|
|
# Get the failure event (second call)
|
|
failure_event = handler.call_args_list[1][0][0]
|
|
assert failure_event["type"] == "scan_failed"
|
|
assert failure_event["success"] is False
|
|
|
|
|
|
class TestScanServiceSingleton:
|
|
"""Test ScanService singleton functions."""
|
|
|
|
def test_get_scan_service_returns_singleton(self):
|
|
"""Test that get_scan_service returns a singleton."""
|
|
reset_scan_service()
|
|
|
|
service1 = get_scan_service()
|
|
service2 = get_scan_service()
|
|
|
|
assert service1 is service2
|
|
|
|
def test_reset_scan_service(self):
|
|
"""Test that reset_scan_service clears the singleton."""
|
|
reset_scan_service()
|
|
service1 = get_scan_service()
|
|
|
|
reset_scan_service()
|
|
service2 = get_scan_service()
|
|
|
|
assert service1 is not service2
|
|
|
|
|
|
class TestScanServiceKeyIdentification:
|
|
"""Test that ScanService uses key as primary identifier."""
|
|
|
|
@pytest.fixture
|
|
def mock_progress_service(self):
|
|
"""Create a mock progress service."""
|
|
service = MagicMock()
|
|
service.start_progress = AsyncMock()
|
|
service.update_progress = AsyncMock()
|
|
service.complete_progress = AsyncMock()
|
|
service.fail_progress = AsyncMock()
|
|
return service
|
|
|
|
@pytest.fixture
|
|
def service(self, mock_progress_service):
|
|
"""Create a ScanService instance."""
|
|
return ScanService(progress_service=mock_progress_service)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_progress_update_includes_key(
|
|
self, service, mock_progress_service
|
|
):
|
|
"""Test that progress updates include key as primary identifier."""
|
|
scanner_factory = MagicMock()
|
|
await service.start_scan(scanner_factory)
|
|
|
|
scan_progress = service.current_scan
|
|
scan_progress.key = "attack-on-titan"
|
|
scan_progress.folder = "Attack on Titan (2013)"
|
|
|
|
await service._handle_progress_update(scan_progress)
|
|
|
|
call_kwargs = mock_progress_service.update_progress.call_args.kwargs
|
|
assert call_kwargs["key"] == "attack-on-titan"
|
|
assert call_kwargs["folder"] == "Attack on Titan (2013)"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_event_includes_key(self, service):
|
|
"""Test that scan events include key as primary identifier."""
|
|
events_received = []
|
|
|
|
async def capture_event(event):
|
|
events_received.append(event)
|
|
|
|
service.subscribe_to_scan_events(capture_event)
|
|
|
|
await service._emit_scan_event({
|
|
"type": "scan_progress",
|
|
"key": "my-hero-academia",
|
|
"folder": "My Hero Academia (2016)",
|
|
"data": {"status": "in_progress"},
|
|
})
|
|
|
|
assert len(events_received) == 1
|
|
assert events_received[0]["key"] == "my-hero-academia"
|
|
assert events_received[0]["folder"] == "My Hero Academia (2016)"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_event_includes_key(self, service):
|
|
"""Test that error events include key as primary identifier."""
|
|
events_received = []
|
|
|
|
async def capture_event(event):
|
|
events_received.append(event)
|
|
|
|
service.subscribe_to_scan_events(capture_event)
|
|
|
|
scanner_factory = MagicMock()
|
|
await service.start_scan(scanner_factory)
|
|
|
|
scan_progress = service.current_scan
|
|
error_context = ErrorContext(
|
|
operation_type=OperationType.SCAN,
|
|
operation_id=scan_progress.scan_id,
|
|
error=ValueError("Test"),
|
|
message="Error message",
|
|
key="demon-slayer",
|
|
folder="Demon Slayer (2019)",
|
|
)
|
|
|
|
await service._handle_scan_error(scan_progress, error_context)
|
|
|
|
assert len(events_received) == 2 # Started + error
|
|
error_event = events_received[1]
|
|
assert error_event["type"] == "scan_error"
|
|
assert error_event["key"] == "demon-slayer"
|
|
assert error_event["folder"] == "Demon Slayer (2019)"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_status_includes_key(self, service):
|
|
"""Test that scan status includes key in current scan."""
|
|
scanner_factory = MagicMock()
|
|
await service.start_scan(scanner_factory)
|
|
|
|
service.current_scan.key = "one-piece"
|
|
service.current_scan.folder = "One Piece (1999)"
|
|
|
|
status = await service.get_scan_status()
|
|
|
|
assert status["current_scan"]["key"] == "one-piece"
|
|
assert status["current_scan"]["folder"] == "One Piece (1999)"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scan_history_includes_key(self, service):
|
|
"""Test that scan history includes key in entries."""
|
|
scanner_factory = MagicMock()
|
|
|
|
await service.start_scan(scanner_factory)
|
|
service.current_scan.key = "naruto"
|
|
service.current_scan.folder = "Naruto (2002)"
|
|
await service.cancel_scan()
|
|
|
|
history = await service.get_scan_history()
|
|
|
|
assert len(history) == 1
|
|
assert history[0]["key"] == "naruto"
|
|
assert history[0]["folder"] == "Naruto (2002)"
|