""" API integration tests for PP-StructureV3 parameter customization """ import pytest import json from fastapi.testclient import TestClient from pathlib import Path from unittest.mock import Mock, patch from app.main import app from app.core.database import get_db from app.models.user import User from app.models.task import Task, TaskStatus, TaskFile @pytest.fixture def client(): """Create test client""" return TestClient(app) @pytest.fixture def test_user(db_session): """Create test user""" user = User( email="test@example.com", hashed_password="test_hash", is_active=True ) db_session.add(user) db_session.commit() db_session.refresh(user) return user @pytest.fixture def test_task(db_session, test_user): """Create test task with uploaded file""" task = Task( user_id=test_user.id, task_id="test-task-123", filename="test.pdf", status=TaskStatus.PENDING ) db_session.add(task) db_session.commit() db_session.refresh(task) # Add task file task_file = TaskFile( task_id=task.id, original_name="test.pdf", stored_path="/tmp/test.pdf", file_size=1024, mime_type="application/pdf" ) db_session.add(task_file) db_session.commit() return task @pytest.fixture def auth_headers(test_user): """Create auth headers for API calls""" # Mock JWT token return {"Authorization": "Bearer test_token"} class TestProcessingOptionsSchema: """Test ProcessingOptions schema validation""" def test_processing_options_accepts_pp_structure_params(self): """Verify ProcessingOptions schema accepts pp_structure_params""" from app.schemas.task import ProcessingOptions, PPStructureV3Params # Valid params params = PPStructureV3Params( layout_detection_threshold=0.15, layout_nms_threshold=0.2, text_det_thresh=0.25, layout_merge_bboxes_mode='small' ) options = ProcessingOptions( use_dual_track=True, language='ch', pp_structure_params=params ) assert options.pp_structure_params is not None assert options.pp_structure_params.layout_detection_threshold == 0.15 def test_ppstructure_params_validation_min_max(self): """Verify parameter validation (min/max constraints)""" from app.schemas.task import PPStructureV3Params from pydantic import ValidationError # Invalid: threshold > 1 with pytest.raises(ValidationError): PPStructureV3Params(layout_detection_threshold=1.5) # Invalid: threshold < 0 with pytest.raises(ValidationError): PPStructureV3Params(layout_nms_threshold=-0.1) # Valid: within range params = PPStructureV3Params( layout_detection_threshold=0.5, layout_nms_threshold=0.3 ) assert params.layout_detection_threshold == 0.5 def test_ppstructure_params_merge_mode_validation(self): """Verify merge mode validation""" from app.schemas.task import PPStructureV3Params from pydantic import ValidationError # Valid modes for mode in ['small', 'large', 'union']: params = PPStructureV3Params(layout_merge_bboxes_mode=mode) assert params.layout_merge_bboxes_mode == mode # Invalid mode with pytest.raises(ValidationError): PPStructureV3Params(layout_merge_bboxes_mode='invalid') def test_ppstructure_params_optional_fields(self): """Verify all fields are optional""" from app.schemas.task import PPStructureV3Params # Empty params should be valid params = PPStructureV3Params() assert params.model_dump(exclude_none=True) == {} # Partial params should be valid params = PPStructureV3Params(layout_detection_threshold=0.2) data = params.model_dump(exclude_none=True) assert 'layout_detection_threshold' in data assert 'layout_nms_threshold' not in data class TestStartTaskEndpoint: """Test /tasks/{task_id}/start endpoint with PP-StructureV3 params""" @patch('app.routers.tasks.process_task_ocr') def test_start_task_with_custom_params(self, mock_process_ocr, client, test_task, auth_headers, db_session): """Verify custom PP-StructureV3 params are accepted and passed to OCR service""" # Override get_db dependency def override_get_db(): try: yield db_session finally: pass # Override auth dependency def override_get_current_user(): return test_task.user app.dependency_overrides[get_db] = override_get_db from app.core.deps import get_current_user app.dependency_overrides[get_current_user] = override_get_current_user # Request body with custom params request_body = { "use_dual_track": True, "language": "ch", "pp_structure_params": { "layout_detection_threshold": 0.15, "layout_nms_threshold": 0.2, "text_det_thresh": 0.25, "layout_merge_bboxes_mode": "small" } } # Make API call response = client.post( f"/api/v2/tasks/{test_task.task_id}/start", json=request_body ) # Verify response assert response.status_code == 200 data = response.json() assert data['status'] == 'processing' # Verify background task was called with custom params mock_process_ocr.assert_called_once() call_kwargs = mock_process_ocr.call_args[1] assert 'pp_structure_params' in call_kwargs assert call_kwargs['pp_structure_params']['layout_detection_threshold'] == 0.15 assert call_kwargs['pp_structure_params']['text_det_thresh'] == 0.25 # Clean up app.dependency_overrides.clear() @patch('app.routers.tasks.process_task_ocr') def test_start_task_without_custom_params(self, mock_process_ocr, client, test_task, auth_headers, db_session): """Verify task can start without custom params (backward compatibility)""" # Override dependencies def override_get_db(): try: yield db_session finally: pass def override_get_current_user(): return test_task.user app.dependency_overrides[get_db] = override_get_db from app.core.deps import get_current_user app.dependency_overrides[get_current_user] = override_get_current_user # Request without pp_structure_params request_body = { "use_dual_track": True, "language": "ch" } response = client.post( f"/api/v2/tasks/{test_task.task_id}/start", json=request_body ) assert response.status_code == 200 # Verify background task was called mock_process_ocr.assert_called_once() call_kwargs = mock_process_ocr.call_args[1] # pp_structure_params should be None (not provided) assert call_kwargs['pp_structure_params'] is None app.dependency_overrides.clear() @patch('app.routers.tasks.process_task_ocr') def test_start_task_with_partial_params(self, mock_process_ocr, client, test_task, auth_headers, db_session): """Verify partial custom params are accepted""" # Override dependencies def override_get_db(): try: yield db_session finally: pass def override_get_current_user(): return test_task.user app.dependency_overrides[get_db] = override_get_db from app.core.deps import get_current_user app.dependency_overrides[get_current_user] = override_get_current_user # Request with only some params request_body = { "use_dual_track": True, "pp_structure_params": { "layout_detection_threshold": 0.1 # Other params omitted } } response = client.post( f"/api/v2/tasks/{test_task.task_id}/start", json=request_body ) assert response.status_code == 200 # Verify only specified param was included mock_process_ocr.assert_called_once() call_kwargs = mock_process_ocr.call_args[1] pp_params = call_kwargs['pp_structure_params'] assert 'layout_detection_threshold' in pp_params assert 'layout_nms_threshold' not in pp_params app.dependency_overrides.clear() def test_start_task_with_invalid_params(self, client, test_task, db_session): """Verify invalid params return 422 validation error""" # Override dependencies def override_get_db(): try: yield db_session finally: pass def override_get_current_user(): return test_task.user app.dependency_overrides[get_db] = override_get_db from app.core.deps import get_current_user app.dependency_overrides[get_current_user] = override_get_current_user # Request with invalid threshold (> 1) request_body = { "use_dual_track": True, "pp_structure_params": { "layout_detection_threshold": 1.5 # Invalid! } } response = client.post( f"/api/v2/tasks/{test_task.task_id}/start", json=request_body ) # Should return validation error assert response.status_code == 422 app.dependency_overrides.clear() class TestOpenAPISchema: """Test OpenAPI schema includes PP-StructureV3 params""" def test_openapi_schema_includes_ppstructure_params(self, client): """Verify OpenAPI schema documents PP-StructureV3 parameters""" response = client.get("/openapi.json") assert response.status_code == 200 schema = response.json() # Check PPStructureV3Params schema exists assert 'PPStructureV3Params' in schema['components']['schemas'] params_schema = schema['components']['schemas']['PPStructureV3Params'] # Verify all 7 parameters are documented assert 'layout_detection_threshold' in params_schema['properties'] assert 'layout_nms_threshold' in params_schema['properties'] assert 'layout_merge_bboxes_mode' in params_schema['properties'] assert 'layout_unclip_ratio' in params_schema['properties'] assert 'text_det_thresh' in params_schema['properties'] assert 'text_det_box_thresh' in params_schema['properties'] assert 'text_det_unclip_ratio' in params_schema['properties'] # Verify ProcessingOptions includes pp_structure_params options_schema = schema['components']['schemas']['ProcessingOptions'] assert 'pp_structure_params' in options_schema['properties'] if __name__ == '__main__': pytest.main([__file__, '-v'])