""" Integration tests for External API. Tests the complete flow: project init → config → dispatch → annotate → export. """ import pytest from fastapi.testclient import TestClient from datetime import datetime import json import os import sys # Add backend to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from main import app from database import get_db_connection @pytest.fixture(scope="module") def client(): """Create test client.""" return TestClient(app) @pytest.fixture(scope="module") def admin_token(client): """Get admin authentication token.""" # Try to login as admin response = client.post("/api/auth/login", json={ "username": "admin", "password": "admin123" }) if response.status_code == 200: return response.json()["access_token"] # If admin doesn't exist, create one response = client.post("/api/auth/register", json={ "username": "admin", "email": "admin@test.com", "password": "admin123", "role": "admin" }) if response.status_code in [200, 201]: return response.json()["access_token"] # Try login again response = client.post("/api/auth/login", json={ "username": "admin", "password": "admin123" }) return response.json()["access_token"] @pytest.fixture(scope="module") def annotator_token(client): """Get annotator authentication token.""" # Try to login as annotator response = client.post("/api/auth/login", json={ "username": "annotator1", "password": "annotator123" }) if response.status_code == 200: return response.json()["access_token"] # If annotator doesn't exist, create one response = client.post("/api/auth/register", json={ "username": "annotator1", "email": "annotator1@test.com", "password": "annotator123", "role": "annotator" }) if response.status_code in [200, 201]: return response.json()["access_token"] # Try login again response = client.post("/api/auth/login", json={ "username": "annotator1", "password": "annotator123" }) return response.json()["access_token"] class TestExternalAPIAuthentication: """Test external API authentication.""" def test_init_project_without_token(self, client): """Test that init project requires authentication.""" response = client.post("/api/external/projects/init", json={ "name": "Test Project", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }) assert response.status_code == 401 def test_init_project_with_invalid_token(self, client): """Test that init project rejects invalid token.""" response = client.post( "/api/external/projects/init", json={ "name": "Test Project", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": "Bearer invalid_token"} ) assert response.status_code == 401 def test_init_project_with_annotator_token(self, client, annotator_token): """Test that init project requires admin role.""" response = client.post( "/api/external/projects/init", json={ "name": "Test Project", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": f"Bearer {annotator_token}"} ) assert response.status_code == 403 class TestExternalAPIProjectInit: """Test external API project initialization.""" def test_init_project_success(self, client, admin_token): """Test successful project initialization.""" response = client.post( "/api/external/projects/init", json={ "name": "External Test Project", "task_type": "image_classification", "data": [ {"content": "http://example.com/1.jpg"}, {"content": "http://example.com/2.jpg"}, {"content": "http://example.com/3.jpg"} ], "external_id": "ext_123" }, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 201 data = response.json() assert "project_id" in data assert data["task_count"] == 3 assert data["status"] == "draft" def test_init_project_with_description(self, client, admin_token): """Test project initialization with description.""" response = client.post( "/api/external/projects/init", json={ "name": "Project with Description", "description": "This is a test project", "task_type": "text_classification", "data": [ {"content": "Sample text 1"}, {"content": "Sample text 2"} ] }, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 201 data = response.json() assert data["task_count"] == 2 def test_init_project_empty_data(self, client, admin_token): """Test project initialization with empty data items.""" response = client.post( "/api/external/projects/init", json={ "name": "Empty Project", "task_type": "image_classification", "data": [] }, headers={"Authorization": f"Bearer {admin_token}"} ) # Should fail validation (min_length=1) assert response.status_code == 422 class TestExternalAPIProgress: """Test external API progress query.""" def test_get_progress_success(self, client, admin_token): """Test successful progress query.""" # First create a project init_response = client.post( "/api/external/projects/init", json={ "name": "Progress Test Project", "task_type": "image_classification", "data": [ {"content": "http://example.com/1.jpg"}, {"content": "http://example.com/2.jpg"} ] }, headers={"Authorization": f"Bearer {admin_token}"} ) assert init_response.status_code == 201 project_id = init_response.json()["project_id"] # Get progress response = client.get( f"/api/external/projects/{project_id}/progress", headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 200 data = response.json() assert data["project_id"] == project_id assert data["total_tasks"] == 2 assert data["completed_tasks"] == 0 assert data["completion_percentage"] == 0.0 def test_get_progress_not_found(self, client, admin_token): """Test progress query for non-existent project.""" response = client.get( "/api/external/projects/nonexistent_id/progress", headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 404 class TestExternalAPIExport: """Test external API data export.""" def test_export_json_format(self, client, admin_token): """Test JSON format export.""" # First create a project init_response = client.post( "/api/external/projects/init", json={ "name": "Export Test Project", "task_type": "image_classification", "data": [ {"content": "http://example.com/1.jpg"} ] }, headers={"Authorization": f"Bearer {admin_token}"} ) assert init_response.status_code == 201 project_id = init_response.json()["project_id"] # Export data response = client.post( f"/api/external/projects/{project_id}/export", json={ "format": "json", "completed_only": False }, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 200 data = response.json() assert "file_url" in data def test_export_invalid_format(self, client, admin_token): """Test export with invalid format.""" # First create a project init_response = client.post( "/api/external/projects/init", json={ "name": "Invalid Export Test", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": f"Bearer {admin_token}"} ) assert init_response.status_code == 201 project_id = init_response.json()["project_id"] # Try invalid format response = client.post( f"/api/external/projects/{project_id}/export", json={ "format": "invalid_format", "completed_only": False }, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 422 # Validation error class TestProjectStatusTransitions: """Test project status transitions.""" def test_valid_status_transition(self, client, admin_token): """Test valid status transition: draft → configuring.""" # Create project init_response = client.post( "/api/external/projects/init", json={ "name": "Status Test Project", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": f"Bearer {admin_token}"} ) project_id = init_response.json()["project_id"] # Update status to configuring response = client.put( f"/api/projects/{project_id}/status", json={"status": "configuring"}, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 200 assert response.json()["status"] == "configuring" def test_invalid_status_transition(self, client, admin_token): """Test invalid status transition: draft → completed.""" # Create project init_response = client.post( "/api/external/projects/init", json={ "name": "Invalid Status Test", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": f"Bearer {admin_token}"} ) project_id = init_response.json()["project_id"] # Try invalid transition response = client.put( f"/api/projects/{project_id}/status", json={"status": "completed"}, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 400 class TestProjectConfig: """Test project configuration.""" def test_update_config_success(self, client, admin_token): """Test successful config update.""" # Create project init_response = client.post( "/api/external/projects/init", json={ "name": "Config Test Project", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": f"Bearer {admin_token}"} ) project_id = init_response.json()["project_id"] # Update config new_config = """ """ response = client.put( f"/api/projects/{project_id}/config", json={"config": new_config}, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 200 assert response.json()["status"] == "configuring" def test_update_config_invalid_xml(self, client, admin_token): """Test config update with invalid XML.""" # Create project init_response = client.post( "/api/external/projects/init", json={ "name": "Invalid Config Test", "task_type": "image_classification", "data": [{"content": "http://example.com/1.jpg"}] }, headers={"Authorization": f"Bearer {admin_token}"} ) project_id = init_response.json()["project_id"] # Try invalid config response = client.put( f"/api/projects/{project_id}/config", json={"config": "not xml at all"}, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 400 class TestTaskDispatch: """Test task dispatch functionality.""" def test_preview_assignment(self, client, admin_token, annotator_token): """Test assignment preview.""" # Create project and set to ready init_response = client.post( "/api/external/projects/init", json={ "name": "Dispatch Test Project", "task_type": "image_classification", "data": [ {"content": "http://example.com/1.jpg"}, {"content": "http://example.com/2.jpg"}, {"content": "http://example.com/3.jpg"}, {"content": "http://example.com/4.jpg"} ] }, headers={"Authorization": f"Bearer {admin_token}"} ) project_id = init_response.json()["project_id"] # Update config config = """ """ client.put( f"/api/projects/{project_id}/config", json={"config": config}, headers={"Authorization": f"Bearer {admin_token}"} ) # Set to ready client.put( f"/api/projects/{project_id}/status", json={"status": "ready"}, headers={"Authorization": f"Bearer {admin_token}"} ) # Get annotator list annotators_response = client.get( "/api/users/annotators", headers={"Authorization": f"Bearer {admin_token}"} ) if annotators_response.status_code == 200 and len(annotators_response.json()) > 0: annotator_ids = [a["id"] for a in annotators_response.json()[:2]] # Preview assignment response = client.post( f"/api/tasks/preview-assignment/{project_id}", json={"user_ids": annotator_ids}, headers={"Authorization": f"Bearer {admin_token}"} ) assert response.status_code == 200 data = response.json() assert data["unassigned_tasks"] == 4 assert len(data["assignments"]) == len(annotator_ids) if __name__ == "__main__": pytest.main([__file__, "-v"])