"""
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"])