| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- """
- 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 = """<View>
- <Image name="image" value="$image"/>
- <Choices name="choice" toName="image">
- <Choice value="Cat"/>
- <Choice value="Dog"/>
- </Choices>
- </View>"""
-
- 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 = """<View>
- <Image name="image" value="$image"/>
- <Choices name="choice" toName="image">
- <Choice value="Cat"/>
- </Choices>
- </View>"""
-
- 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"])
|