test_export_api.py 12 KB

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