test_template_api.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. """
  2. Template API tests.
  3. Tests for template management and XML validation endpoints.
  4. """
  5. import pytest
  6. import uuid
  7. from fastapi.testclient import TestClient
  8. from main import app
  9. from database import init_database
  10. from services.jwt_service import JWTService
  11. import bcrypt
  12. from database import get_db_connection
  13. # 测试客户端
  14. client = TestClient(app)
  15. @pytest.fixture(scope="module")
  16. def setup_database():
  17. """初始化测试数据库"""
  18. init_database()
  19. yield
  20. @pytest.fixture
  21. def auth_token(setup_database):
  22. """创建测试用户并返回 token"""
  23. user_id = f"user_{uuid.uuid4().hex[:8]}"
  24. password_hash = bcrypt.hashpw("test123".encode(), bcrypt.gensalt()).decode()
  25. with get_db_connection() as conn:
  26. cursor = conn.cursor()
  27. cursor.execute("""
  28. INSERT INTO users (id, username, email, password_hash, role)
  29. VALUES (?, ?, ?, ?, 'annotator')
  30. """, (user_id, f"test_user_{user_id}", f"user_{user_id}@test.com", password_hash))
  31. user_data = {
  32. "id": user_id,
  33. "username": f"test_user_{user_id}",
  34. "email": f"user_{user_id}@test.com",
  35. "role": "annotator"
  36. }
  37. token = JWTService.create_access_token(user_data)
  38. yield token
  39. # 清理
  40. with get_db_connection() as conn:
  41. cursor = conn.cursor()
  42. cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
  43. class TestTemplateList:
  44. """模板列表测试"""
  45. def test_list_templates_without_auth(self, setup_database):
  46. """未认证时应返回 401"""
  47. response = client.get("/api/templates")
  48. assert response.status_code == 401
  49. def test_list_templates(self, auth_token):
  50. """获取模板列表"""
  51. headers = {"Authorization": f"Bearer {auth_token}"}
  52. response = client.get("/api/templates", headers=headers)
  53. assert response.status_code == 200
  54. data = response.json()
  55. assert "templates" in data
  56. assert "total" in data
  57. assert len(data["templates"]) > 0
  58. assert data["total"] > 0
  59. def test_list_templates_by_category(self, auth_token):
  60. """按类别筛选模板"""
  61. headers = {"Authorization": f"Bearer {auth_token}"}
  62. response = client.get("/api/templates?category=image_classification", headers=headers)
  63. assert response.status_code == 200
  64. data = response.json()
  65. # 所有返回的模板都应该是 image_classification 类别
  66. for template in data["templates"]:
  67. assert template["category"] == "image_classification"
  68. def test_list_templates_with_search(self, auth_token):
  69. """搜索模板"""
  70. headers = {"Authorization": f"Bearer {auth_token}"}
  71. response = client.get("/api/templates?search=图像", headers=headers)
  72. assert response.status_code == 200
  73. data = response.json()
  74. # 搜索结果应该包含匹配的模板
  75. assert len(data["templates"]) > 0
  76. class TestTemplateCategories:
  77. """模板类别测试"""
  78. def test_list_categories_without_auth(self, setup_database):
  79. """未认证时应返回 401"""
  80. response = client.get("/api/templates/categories")
  81. assert response.status_code == 401
  82. def test_list_categories(self, auth_token):
  83. """获取模板类别列表"""
  84. headers = {"Authorization": f"Bearer {auth_token}"}
  85. response = client.get("/api/templates/categories", headers=headers)
  86. assert response.status_code == 200
  87. data = response.json()
  88. assert "categories" in data
  89. assert len(data["categories"]) > 0
  90. # 检查类别结构
  91. category = data["categories"][0]
  92. assert "id" in category
  93. assert "name" in category
  94. assert "description" in category
  95. class TestTemplateDetail:
  96. """模板详情测试"""
  97. def test_get_template_without_auth(self, setup_database):
  98. """未认证时应返回 401"""
  99. response = client.get("/api/templates/image_classification_basic")
  100. assert response.status_code == 401
  101. def test_get_template(self, auth_token):
  102. """获取模板详情"""
  103. headers = {"Authorization": f"Bearer {auth_token}"}
  104. response = client.get("/api/templates/image_classification_basic", headers=headers)
  105. assert response.status_code == 200
  106. data = response.json()
  107. assert data["id"] == "image_classification_basic"
  108. assert "name" in data
  109. assert "category" in data
  110. assert "config" in data
  111. assert "description" in data
  112. def test_get_nonexistent_template(self, auth_token):
  113. """获取不存在的模板应返回 404"""
  114. headers = {"Authorization": f"Bearer {auth_token}"}
  115. response = client.get("/api/templates/nonexistent_template", headers=headers)
  116. assert response.status_code == 404
  117. class TestConfigValidation:
  118. """配置验证测试"""
  119. def test_validate_config_without_auth(self, setup_database):
  120. """未认证时应返回 401"""
  121. response = client.post(
  122. "/api/templates/validate",
  123. json={"config": "<View></View>"}
  124. )
  125. assert response.status_code == 401
  126. def test_validate_valid_config(self, auth_token):
  127. """验证有效的 XML 配置"""
  128. headers = {"Authorization": f"Bearer {auth_token}"}
  129. valid_config = """<View>
  130. <Image name="image" value="$image"/>
  131. <Choices name="choice" toName="image">
  132. <Choice value="类别1"/>
  133. </Choices>
  134. </View>"""
  135. response = client.post(
  136. "/api/templates/validate",
  137. json={"config": valid_config},
  138. headers=headers
  139. )
  140. assert response.status_code == 200
  141. data = response.json()
  142. assert data["valid"] is True
  143. assert len(data["errors"]) == 0
  144. def test_validate_invalid_config(self, auth_token):
  145. """验证无效的 XML 配置"""
  146. headers = {"Authorization": f"Bearer {auth_token}"}
  147. invalid_config = """<View>
  148. <Image name="image" value="$image">
  149. <Choices name="choice" toName="image">
  150. </View>"""
  151. response = client.post(
  152. "/api/templates/validate",
  153. json={"config": invalid_config},
  154. headers=headers
  155. )
  156. assert response.status_code == 200
  157. data = response.json()
  158. assert data["valid"] is False
  159. assert len(data["errors"]) > 0
  160. # 检查错误结构
  161. error = data["errors"][0]
  162. assert "line" in error
  163. assert "column" in error
  164. assert "message" in error
  165. def test_validate_empty_config(self, auth_token):
  166. """验证空配置"""
  167. headers = {"Authorization": f"Bearer {auth_token}"}
  168. response = client.post(
  169. "/api/templates/validate",
  170. json={"config": ""},
  171. headers=headers
  172. )
  173. # 空字符串应该被 Pydantic 验证拒绝
  174. assert response.status_code == 422
  175. def test_validate_whitespace_config(self, auth_token):
  176. """验证只有空白的配置"""
  177. headers = {"Authorization": f"Bearer {auth_token}"}
  178. response = client.post(
  179. "/api/templates/validate",
  180. json={"config": " "},
  181. headers=headers
  182. )
  183. assert response.status_code == 200
  184. data = response.json()
  185. assert data["valid"] is False
  186. class TestTemplateContent:
  187. """模板内容测试"""
  188. def test_all_templates_have_valid_config(self, auth_token):
  189. """所有预设模板的配置都应该是有效的 XML"""
  190. headers = {"Authorization": f"Bearer {auth_token}"}
  191. # 获取所有模板
  192. response = client.get("/api/templates", headers=headers)
  193. assert response.status_code == 200
  194. templates = response.json()["templates"]
  195. # 验证每个模板的配置
  196. for template in templates:
  197. validate_response = client.post(
  198. "/api/templates/validate",
  199. json={"config": template["config"]},
  200. headers=headers
  201. )
  202. assert validate_response.status_code == 200
  203. validation_result = validate_response.json()
  204. assert validation_result["valid"] is True, \
  205. f"模板 {template['id']} 的配置无效: {validation_result['errors']}"