test_export_api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. """
  2. Tests for Export API endpoints.
  3. Tests data export functionality in various formats.
  4. """
  5. import pytest
  6. import json
  7. import os
  8. import uuid
  9. from fastapi.testclient import TestClient
  10. from main import app
  11. from database import get_db_connection, init_database
  12. from test.auth_test_helper import create_test_token
  13. # Test client
  14. client = TestClient(app)
  15. # Test data
  16. TEST_PROJECT = {
  17. "name": "Export Test Project",
  18. "description": "Project for testing export functionality",
  19. "config": "<View><Image name='image' value='$image'/><Choices name='choice' toName='image'><Choice value='Cat'/><Choice value='Dog'/></Choices></View>"
  20. }
  21. @pytest.fixture(scope="module")
  22. def setup_database():
  23. """Initialize database before tests."""
  24. init_database()
  25. yield
  26. @pytest.fixture(scope="module")
  27. def admin_token(setup_database):
  28. """Create admin user via token cache and get token."""
  29. user_id = str(uuid.uuid4())
  30. username = f"export_admin_{uuid.uuid4().hex[:8]}"
  31. email = f"{username}@test.com"
  32. # Ensure user exists in local DB
  33. with get_db_connection() as conn:
  34. cursor = conn.cursor()
  35. cursor.execute(
  36. "INSERT OR IGNORE INTO users (id, username, email, role) VALUES (?, ?, ?, ?)",
  37. (user_id, username, email, "admin"),
  38. )
  39. return create_test_token({
  40. "id": user_id,
  41. "username": username,
  42. "email": email,
  43. "role": "admin",
  44. })
  45. @pytest.fixture(scope="module")
  46. def test_project(admin_token):
  47. """Create a test project with tasks and annotations."""
  48. headers = {"Authorization": f"Bearer {admin_token}"}
  49. # Create project
  50. response = client.post("/api/projects", json=TEST_PROJECT, headers=headers)
  51. assert response.status_code == 201
  52. project = response.json()
  53. project_id = project["id"]
  54. # Create tasks
  55. task_ids = []
  56. for i in range(3):
  57. task_data = {
  58. "project_id": project_id,
  59. "name": f"Test Task {i+1}",
  60. "data": {"image": f"https://example.com/image{i+1}.jpg"}
  61. }
  62. response = client.post("/api/tasks", json=task_data, headers=headers)
  63. assert response.status_code == 201
  64. task_ids.append(response.json()["id"])
  65. # Create annotations for first two tasks
  66. for i, task_id in enumerate(task_ids[:2]):
  67. annotation_data = {
  68. "task_id": task_id,
  69. "user_id": "test_user",
  70. "result": {
  71. "annotations": [
  72. {
  73. "id": f"ann_{i}",
  74. "type": "rectanglelabels",
  75. "value": {
  76. "x": 10 + i * 5,
  77. "y": 20 + i * 5,
  78. "width": 30,
  79. "height": 40,
  80. "rectanglelabels": ["Cat" if i == 0 else "Dog"]
  81. }
  82. }
  83. ]
  84. }
  85. }
  86. response = client.post("/api/annotations", json=annotation_data, headers=headers)
  87. assert response.status_code == 201
  88. # Update first task to completed
  89. response = client.put(
  90. f"/api/tasks/{task_ids[0]}",
  91. json={"status": "completed"},
  92. headers=headers
  93. )
  94. yield {
  95. "project_id": project_id,
  96. "task_ids": task_ids
  97. }
  98. # Cleanup: delete project (cascades to tasks and annotations)
  99. client.delete(f"/api/projects/{project_id}", headers=headers)
  100. class TestExportAPI:
  101. """Test cases for Export API."""
  102. def test_export_json_format(self, admin_token, test_project):
  103. """Test exporting data in JSON format."""
  104. headers = {"Authorization": f"Bearer {admin_token}"}
  105. project_id = test_project["project_id"]
  106. response = client.post(
  107. f"/api/projects/{project_id}/export",
  108. json={
  109. "format": "json",
  110. "status_filter": "all",
  111. "include_metadata": True
  112. },
  113. headers=headers
  114. )
  115. assert response.status_code == 200
  116. data = response.json()
  117. assert data["project_id"] == project_id
  118. assert data["format"] == "json"
  119. assert data["status"] == "completed"
  120. assert data["total_tasks"] == 3
  121. assert data["download_url"] is not None
  122. def test_export_csv_format(self, admin_token, test_project):
  123. """Test exporting data in CSV format."""
  124. headers = {"Authorization": f"Bearer {admin_token}"}
  125. project_id = test_project["project_id"]
  126. response = client.post(
  127. f"/api/projects/{project_id}/export",
  128. json={
  129. "format": "csv",
  130. "status_filter": "all",
  131. "include_metadata": True
  132. },
  133. headers=headers
  134. )
  135. assert response.status_code == 200
  136. data = response.json()
  137. assert data["format"] == "csv"
  138. assert data["status"] == "completed"
  139. def test_export_coco_format(self, admin_token, test_project):
  140. """Test exporting data in COCO format."""
  141. headers = {"Authorization": f"Bearer {admin_token}"}
  142. project_id = test_project["project_id"]
  143. response = client.post(
  144. f"/api/projects/{project_id}/export",
  145. json={
  146. "format": "coco",
  147. "status_filter": "all",
  148. "include_metadata": True
  149. },
  150. headers=headers
  151. )
  152. assert response.status_code == 200
  153. data = response.json()
  154. assert data["format"] == "coco"
  155. assert data["status"] == "completed"
  156. def test_export_yolo_format(self, admin_token, test_project):
  157. """Test exporting data in YOLO format."""
  158. headers = {"Authorization": f"Bearer {admin_token}"}
  159. project_id = test_project["project_id"]
  160. response = client.post(
  161. f"/api/projects/{project_id}/export",
  162. json={
  163. "format": "yolo",
  164. "status_filter": "all",
  165. "include_metadata": True
  166. },
  167. headers=headers
  168. )
  169. assert response.status_code == 200
  170. data = response.json()
  171. assert data["format"] == "yolo"
  172. assert data["status"] == "completed"
  173. def test_export_with_status_filter(self, admin_token, test_project):
  174. """Test exporting with status filter."""
  175. headers = {"Authorization": f"Bearer {admin_token}"}
  176. project_id = test_project["project_id"]
  177. # Export only completed tasks
  178. response = client.post(
  179. f"/api/projects/{project_id}/export",
  180. json={
  181. "format": "json",
  182. "status_filter": "completed",
  183. "include_metadata": True
  184. },
  185. headers=headers
  186. )
  187. assert response.status_code == 200
  188. data = response.json()
  189. assert data["status_filter"] == "completed"
  190. # Should have fewer tasks than total
  191. assert data["total_tasks"] <= 3
  192. def test_export_nonexistent_project(self, admin_token):
  193. """Test exporting from non-existent project."""
  194. headers = {"Authorization": f"Bearer {admin_token}"}
  195. response = client.post(
  196. "/api/projects/nonexistent_project/export",
  197. json={
  198. "format": "json",
  199. "status_filter": "all",
  200. "include_metadata": True
  201. },
  202. headers=headers
  203. )
  204. assert response.status_code == 404
  205. def test_get_export_status(self, admin_token, test_project):
  206. """Test getting export job status."""
  207. headers = {"Authorization": f"Bearer {admin_token}"}
  208. project_id = test_project["project_id"]
  209. # First create an export
  210. response = client.post(
  211. f"/api/projects/{project_id}/export",
  212. json={
  213. "format": "json",
  214. "status_filter": "all",
  215. "include_metadata": True
  216. },
  217. headers=headers
  218. )
  219. assert response.status_code == 200
  220. export_id = response.json()["id"]
  221. # Get status
  222. response = client.get(f"/api/exports/{export_id}/status", headers=headers)
  223. assert response.status_code == 200
  224. data = response.json()
  225. assert data["id"] == export_id
  226. assert data["status"] == "completed"
  227. assert data["progress"] == 1.0
  228. def test_download_export(self, admin_token, test_project):
  229. """Test downloading exported file."""
  230. headers = {"Authorization": f"Bearer {admin_token}"}
  231. project_id = test_project["project_id"]
  232. # First create an export
  233. response = client.post(
  234. f"/api/projects/{project_id}/export",
  235. json={
  236. "format": "json",
  237. "status_filter": "all",
  238. "include_metadata": True
  239. },
  240. headers=headers
  241. )
  242. assert response.status_code == 200
  243. export_id = response.json()["id"]
  244. # Download
  245. response = client.get(f"/api/exports/{export_id}/download", headers=headers)
  246. assert response.status_code == 200
  247. assert response.headers["content-type"] == "application/json"
  248. # Verify content is valid JSON
  249. content = response.json()
  250. assert "project_id" in content
  251. assert "tasks" in content
  252. def test_get_export_job_details(self, admin_token, test_project):
  253. """Test getting export job details."""
  254. headers = {"Authorization": f"Bearer {admin_token}"}
  255. project_id = test_project["project_id"]
  256. # First create an export
  257. response = client.post(
  258. f"/api/projects/{project_id}/export",
  259. json={
  260. "format": "json",
  261. "status_filter": "all",
  262. "include_metadata": True
  263. },
  264. headers=headers
  265. )
  266. assert response.status_code == 200
  267. export_id = response.json()["id"]
  268. # Get details
  269. response = client.get(f"/api/exports/{export_id}", headers=headers)
  270. assert response.status_code == 200
  271. data = response.json()
  272. assert data["id"] == export_id
  273. assert data["project_id"] == project_id
  274. assert data["format"] == "json"
  275. assert data["status"] == "completed"
  276. class TestExportPermissions:
  277. """Test cases for export permissions."""
  278. def test_non_admin_cannot_export(self, setup_database, test_project):
  279. """Test that non-admin users cannot export data."""
  280. user_id = str(uuid.uuid4())
  281. username = f"regular_user_{uuid.uuid4().hex[:8]}"
  282. email = f"{username}@test.com"
  283. # Ensure user exists in local DB as annotator
  284. with get_db_connection() as conn:
  285. cursor = conn.cursor()
  286. cursor.execute(
  287. "INSERT OR IGNORE INTO users (id, username, email, role) VALUES (?, ?, ?, ?)",
  288. (user_id, username, email, "annotator"),
  289. )
  290. token = create_test_token({
  291. "id": user_id,
  292. "username": username,
  293. "email": email,
  294. "role": "annotator",
  295. })
  296. # Try to export
  297. headers = {"Authorization": f"Bearer {token}"}
  298. project_id = test_project["project_id"]
  299. response = client.post(
  300. f"/api/projects/{project_id}/export",
  301. json={
  302. "format": "json",
  303. "status_filter": "all",
  304. "include_metadata": True
  305. },
  306. headers=headers
  307. )
  308. assert response.status_code == 403