WIP
This commit is contained in:
266
tests/web/test_async_service.py
Normal file
266
tests/web/test_async_service.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Tests for the AsyncProcessingService class.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.data.async_request_db import AsyncRequest
|
||||
from src.web.workers.async_queue import AsyncTask, AsyncTaskQueue
|
||||
from src.web.services.async_processing import AsyncProcessingService, AsyncSubmitResult
|
||||
from src.web.config import AsyncConfig, StorageConfig
|
||||
from src.web.rate_limiter import RateLimiter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_service(mock_db, mock_inference_service, rate_limiter, storage_config):
|
||||
"""Create an AsyncProcessingService for testing."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
async_config = AsyncConfig(
|
||||
queue_max_size=10,
|
||||
worker_count=1,
|
||||
task_timeout_seconds=30,
|
||||
result_retention_days=7,
|
||||
temp_upload_dir=Path(tmpdir) / "async",
|
||||
max_file_size_mb=10,
|
||||
)
|
||||
|
||||
queue = AsyncTaskQueue(max_size=10, worker_count=1)
|
||||
|
||||
service = AsyncProcessingService(
|
||||
inference_service=mock_inference_service,
|
||||
db=mock_db,
|
||||
queue=queue,
|
||||
rate_limiter=rate_limiter,
|
||||
async_config=async_config,
|
||||
storage_config=storage_config,
|
||||
)
|
||||
|
||||
yield service
|
||||
|
||||
# Cleanup
|
||||
if service._queue._started:
|
||||
service.stop()
|
||||
|
||||
|
||||
class TestAsyncProcessingService:
|
||||
"""Tests for AsyncProcessingService."""
|
||||
|
||||
def test_submit_request_success(self, async_service, mock_db):
|
||||
"""Test successful request submission."""
|
||||
mock_db.create_request.return_value = "test-request-id"
|
||||
|
||||
result = async_service.submit_request(
|
||||
api_key="test-api-key",
|
||||
file_content=b"fake pdf content",
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.request_id is not None
|
||||
assert result.estimated_wait_seconds >= 0
|
||||
assert result.error is None
|
||||
|
||||
def test_submit_request_creates_db_record(self, async_service, mock_db):
|
||||
"""Test that submission creates database record."""
|
||||
async_service.submit_request(
|
||||
api_key="test-api-key",
|
||||
file_content=b"fake pdf content",
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
)
|
||||
|
||||
mock_db.create_request.assert_called_once()
|
||||
call_kwargs = mock_db.create_request.call_args[1]
|
||||
assert call_kwargs["api_key"] == "test-api-key"
|
||||
assert call_kwargs["filename"] == "test.pdf"
|
||||
assert call_kwargs["content_type"] == "application/pdf"
|
||||
|
||||
def test_submit_request_saves_file(self, async_service, mock_db):
|
||||
"""Test that submission saves file to temp directory."""
|
||||
content = b"fake pdf content"
|
||||
|
||||
result = async_service.submit_request(
|
||||
api_key="test-api-key",
|
||||
file_content=content,
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
)
|
||||
|
||||
# File should exist in temp directory
|
||||
temp_dir = async_service._async_config.temp_upload_dir
|
||||
files = list(temp_dir.iterdir())
|
||||
|
||||
# Note: file may be cleaned up quickly if queue processes it
|
||||
# So we just check that the operation succeeded
|
||||
assert result.success is True
|
||||
|
||||
def test_submit_request_records_rate_limit(self, async_service, mock_db, rate_limiter):
|
||||
"""Test that submission records rate limit event."""
|
||||
async_service.submit_request(
|
||||
api_key="test-api-key",
|
||||
file_content=b"fake pdf content",
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
)
|
||||
|
||||
# Rate limiter should have recorded the request
|
||||
mock_db.record_rate_limit_event.assert_called()
|
||||
|
||||
def test_start_and_stop(self, async_service):
|
||||
"""Test starting and stopping the service."""
|
||||
async_service.start()
|
||||
|
||||
assert async_service._queue._started is True
|
||||
assert async_service._cleanup_thread is not None
|
||||
assert async_service._cleanup_thread.is_alive()
|
||||
|
||||
async_service.stop()
|
||||
|
||||
assert async_service._queue._started is False
|
||||
|
||||
def test_process_task_success(self, async_service, mock_db, mock_inference_service, sample_task):
|
||||
"""Test successful task processing."""
|
||||
async_service._process_task(sample_task)
|
||||
|
||||
# Should update status to processing
|
||||
mock_db.update_status.assert_called_with(sample_task.request_id, "processing")
|
||||
|
||||
# Should complete the request
|
||||
mock_db.complete_request.assert_called_once()
|
||||
call_kwargs = mock_db.complete_request.call_args[1]
|
||||
assert call_kwargs["request_id"] == sample_task.request_id
|
||||
assert "document_id" in call_kwargs
|
||||
|
||||
def test_process_task_pdf(self, async_service, mock_db, mock_inference_service, sample_task):
|
||||
"""Test processing a PDF task."""
|
||||
async_service._process_task(sample_task)
|
||||
|
||||
# Should call process_pdf for .pdf files
|
||||
mock_inference_service.process_pdf.assert_called_once()
|
||||
|
||||
def test_process_task_image(self, async_service, mock_db, mock_inference_service):
|
||||
"""Test processing an image task."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
||||
f.write(b"fake image content")
|
||||
task = AsyncTask(
|
||||
request_id="image-task",
|
||||
api_key="test-api-key",
|
||||
file_path=Path(f.name),
|
||||
filename="test.png",
|
||||
)
|
||||
|
||||
async_service._process_task(task)
|
||||
|
||||
# Should call process_image for image files
|
||||
mock_inference_service.process_image.assert_called_once()
|
||||
|
||||
def test_process_task_failure(self, async_service, mock_db, mock_inference_service, sample_task):
|
||||
"""Test task processing failure."""
|
||||
mock_inference_service.process_pdf.side_effect = Exception("Processing failed")
|
||||
|
||||
async_service._process_task(sample_task)
|
||||
|
||||
# Should update status to failed
|
||||
mock_db.update_status.assert_called()
|
||||
last_call = mock_db.update_status.call_args_list[-1]
|
||||
assert last_call[0][1] == "failed" # status
|
||||
assert "Processing failed" in last_call[1]["error_message"]
|
||||
|
||||
def test_process_task_file_not_found(self, async_service, mock_db):
|
||||
"""Test task processing with missing file."""
|
||||
task = AsyncTask(
|
||||
request_id="missing-file-task",
|
||||
api_key="test-api-key",
|
||||
file_path=Path("/nonexistent/file.pdf"),
|
||||
filename="test.pdf",
|
||||
)
|
||||
|
||||
async_service._process_task(task)
|
||||
|
||||
# Should fail with file not found
|
||||
mock_db.update_status.assert_called()
|
||||
last_call = mock_db.update_status.call_args_list[-1]
|
||||
assert last_call[0][1] == "failed"
|
||||
|
||||
def test_process_task_cleans_up_file(self, async_service, mock_db, mock_inference_service):
|
||||
"""Test that task processing cleans up the uploaded file."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
|
||||
f.write(b"fake pdf content")
|
||||
file_path = Path(f.name)
|
||||
|
||||
task = AsyncTask(
|
||||
request_id="cleanup-task",
|
||||
api_key="test-api-key",
|
||||
file_path=file_path,
|
||||
filename="test.pdf",
|
||||
)
|
||||
|
||||
async_service._process_task(task)
|
||||
|
||||
# File should be deleted
|
||||
assert not file_path.exists()
|
||||
|
||||
def test_estimate_wait(self, async_service):
|
||||
"""Test wait time estimation."""
|
||||
# Empty queue
|
||||
wait = async_service._estimate_wait()
|
||||
assert wait == 0
|
||||
|
||||
def test_cleanup_orphan_files(self, async_service, mock_db):
|
||||
"""Test cleanup of orphan files."""
|
||||
# Create an orphan file
|
||||
temp_dir = async_service._async_config.temp_upload_dir
|
||||
orphan_file = temp_dir / "orphan-request.pdf"
|
||||
orphan_file.write_bytes(b"orphan content")
|
||||
|
||||
# Set file mtime to old
|
||||
import os
|
||||
old_time = time.time() - 7200
|
||||
os.utime(orphan_file, (old_time, old_time))
|
||||
|
||||
# Mock database to say file doesn't exist
|
||||
mock_db.get_request.return_value = None
|
||||
|
||||
count = async_service._cleanup_orphan_files()
|
||||
|
||||
assert count == 1
|
||||
assert not orphan_file.exists()
|
||||
|
||||
def test_save_upload(self, async_service):
|
||||
"""Test saving uploaded file."""
|
||||
content = b"test content"
|
||||
|
||||
file_path = async_service._save_upload(
|
||||
request_id="test-save",
|
||||
filename="test.pdf",
|
||||
content=content,
|
||||
)
|
||||
|
||||
assert file_path.exists()
|
||||
assert file_path.read_bytes() == content
|
||||
assert file_path.suffix == ".pdf"
|
||||
|
||||
# Cleanup
|
||||
file_path.unlink()
|
||||
|
||||
def test_save_upload_preserves_extension(self, async_service):
|
||||
"""Test that save_upload preserves file extension."""
|
||||
content = b"test content"
|
||||
|
||||
# Test various extensions
|
||||
for ext in [".pdf", ".png", ".jpg", ".jpeg"]:
|
||||
file_path = async_service._save_upload(
|
||||
request_id=f"test-{ext}",
|
||||
filename=f"test{ext}",
|
||||
content=content,
|
||||
)
|
||||
|
||||
assert file_path.suffix == ext
|
||||
file_path.unlink()
|
||||
Reference in New Issue
Block a user