test_property_project_creation.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """
  2. Property-based tests for Project creation.
  3. Tests universal properties that should hold for all valid project data.
  4. Feature: annotation-platform
  5. Property 1: Project creation adds to list
  6. Validates: Requirements 1.3
  7. """
  8. import os
  9. import pytest
  10. from hypothesis import given, strategies as st, settings, HealthCheck
  11. from fastapi.testclient import TestClient
  12. # Use a test database
  13. TEST_DB_PATH = "test_property_annotation_platform.db"
  14. @pytest.fixture(scope="function", autouse=True)
  15. def setup_test_db():
  16. """Setup test database before each test and cleanup after."""
  17. # Set test database path
  18. original_db_path = os.environ.get("DATABASE_PATH")
  19. os.environ["DATABASE_PATH"] = TEST_DB_PATH
  20. # Remove existing test database
  21. if os.path.exists(TEST_DB_PATH):
  22. os.remove(TEST_DB_PATH)
  23. # Import after setting env var
  24. from database import init_database
  25. init_database()
  26. yield
  27. # Cleanup
  28. if os.path.exists(TEST_DB_PATH):
  29. os.remove(TEST_DB_PATH)
  30. # Restore original path
  31. if original_db_path:
  32. os.environ["DATABASE_PATH"] = original_db_path
  33. elif "DATABASE_PATH" in os.environ:
  34. del os.environ["DATABASE_PATH"]
  35. def get_test_client():
  36. """Create a test client."""
  37. from main import app
  38. return TestClient(app)
  39. # Strategy for generating valid project names (non-empty strings)
  40. valid_project_names = st.text(
  41. alphabet=st.characters(
  42. whitelist_categories=("Lu", "Ll", "Nd", "P", "Zs"),
  43. min_codepoint=32,
  44. max_codepoint=126
  45. ),
  46. min_size=1,
  47. max_size=100
  48. ).filter(lambda x: x.strip() != "") # Ensure not just whitespace
  49. # Strategy for generating project descriptions
  50. project_descriptions = st.text(
  51. alphabet=st.characters(
  52. whitelist_categories=("Lu", "Ll", "Nd", "P", "Zs"),
  53. min_codepoint=32,
  54. max_codepoint=126
  55. ),
  56. max_size=500
  57. )
  58. # Strategy for generating valid Label Studio configs (non-empty strings)
  59. valid_configs = st.text(
  60. alphabet=st.characters(
  61. whitelist_categories=("Lu", "Ll", "Nd", "P", "Zs"),
  62. min_codepoint=32,
  63. max_codepoint=126
  64. ),
  65. min_size=1,
  66. max_size=200
  67. ).filter(lambda x: x.strip() != "") # Ensure not just whitespace
  68. @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture])
  69. @given(
  70. name=valid_project_names,
  71. description=project_descriptions,
  72. config=valid_configs
  73. )
  74. def test_property_1_project_creation_adds_to_list(
  75. name: str,
  76. description: str,
  77. config: str
  78. ):
  79. """
  80. Feature: annotation-platform, Property 1: Project creation adds to list
  81. Property: For any valid project data (non-empty name and config),
  82. creating a project should result in the project appearing in the
  83. projects list with a unique ID.
  84. Validates: Requirements 1.3
  85. This property tests that:
  86. 1. Creating a project with valid data succeeds
  87. 2. The created project has a unique ID
  88. 3. The project appears in the list of all projects
  89. 4. The project data is preserved correctly
  90. """
  91. # Create test client for this example
  92. test_client = get_test_client()
  93. # Get initial project count
  94. initial_response = test_client.get("/api/projects")
  95. assert initial_response.status_code == 200
  96. initial_projects = initial_response.json()
  97. initial_count = len(initial_projects)
  98. # Create project with generated data
  99. project_data = {
  100. "name": name,
  101. "description": description,
  102. "config": config
  103. }
  104. create_response = test_client.post("/api/projects", json=project_data)
  105. # Verify creation succeeded
  106. assert create_response.status_code == 201, \
  107. f"Project creation failed with status {create_response.status_code}"
  108. created_project = create_response.json()
  109. # Verify project has a unique ID
  110. assert "id" in created_project, "Created project should have an ID"
  111. assert created_project["id"] is not None, "Project ID should not be None"
  112. assert created_project["id"] != "", "Project ID should not be empty"
  113. assert created_project["id"].startswith("proj_"), \
  114. "Project ID should start with 'proj_' prefix"
  115. # Verify project data is preserved
  116. assert created_project["name"] == name, \
  117. "Created project name should match input"
  118. assert created_project["description"] == description, \
  119. "Created project description should match input"
  120. assert created_project["config"] == config, \
  121. "Created project config should match input"
  122. # Verify project appears in list
  123. list_response = test_client.get("/api/projects")
  124. assert list_response.status_code == 200
  125. projects_list = list_response.json()
  126. # Verify list grew by exactly one
  127. assert len(projects_list) == initial_count + 1, \
  128. "Project list should grow by exactly one after creation"
  129. # Verify the created project is in the list
  130. project_ids = [p["id"] for p in projects_list]
  131. assert created_project["id"] in project_ids, \
  132. "Created project should appear in projects list"
  133. # Find the created project in the list and verify data
  134. created_in_list = next(
  135. (p for p in projects_list if p["id"] == created_project["id"]),
  136. None
  137. )
  138. assert created_in_list is not None, \
  139. "Created project should be findable in list"
  140. assert created_in_list["name"] == name, \
  141. "Project name in list should match input"
  142. assert created_in_list["description"] == description, \
  143. "Project description in list should match input"
  144. assert created_in_list["config"] == config, \
  145. "Project config in list should match input"
  146. # Verify task_count is initialized to 0
  147. assert created_project["task_count"] == 0, \
  148. "New project should have task_count of 0"
  149. assert created_in_list["task_count"] == 0, \
  150. "New project in list should have task_count of 0"
  151. @settings(max_examples=50, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture])
  152. @given(
  153. name1=valid_project_names,
  154. name2=valid_project_names,
  155. config=valid_configs
  156. )
  157. def test_property_1_multiple_projects_have_unique_ids(
  158. name1: str,
  159. name2: str,
  160. config: str
  161. ):
  162. """
  163. Feature: annotation-platform, Property 1: Project creation adds to list
  164. Property: Creating multiple projects should result in each project
  165. having a unique ID, even if they have the same name.
  166. Validates: Requirements 1.3
  167. This property tests that:
  168. 1. Multiple projects can be created
  169. 2. Each project gets a unique ID
  170. 3. Projects with the same name still get different IDs
  171. """
  172. # Create test client for this example
  173. test_client = get_test_client()
  174. # Create first project
  175. project1_data = {
  176. "name": name1,
  177. "description": "First project",
  178. "config": config
  179. }
  180. response1 = test_client.post("/api/projects", json=project1_data)
  181. assert response1.status_code == 201
  182. project1 = response1.json()
  183. # Create second project
  184. project2_data = {
  185. "name": name2,
  186. "description": "Second project",
  187. "config": config
  188. }
  189. response2 = test_client.post("/api/projects", json=project2_data)
  190. assert response2.status_code == 201
  191. project2 = response2.json()
  192. # Verify both projects have IDs
  193. assert "id" in project1
  194. assert "id" in project2
  195. # Verify IDs are unique
  196. assert project1["id"] != project2["id"], \
  197. "Different projects should have unique IDs"
  198. # Verify both projects appear in list
  199. list_response = test_client.get("/api/projects")
  200. assert list_response.status_code == 200
  201. projects_list = list_response.json()
  202. project_ids = [p["id"] for p in projects_list]
  203. assert project1["id"] in project_ids, \
  204. "First project should be in list"
  205. assert project2["id"] in project_ids, \
  206. "Second project should be in list"
  207. # Verify we can retrieve each project individually
  208. get_response1 = test_client.get(f"/api/projects/{project1['id']}")
  209. assert get_response1.status_code == 200
  210. assert get_response1.json()["id"] == project1["id"]
  211. get_response2 = test_client.get(f"/api/projects/{project2['id']}")
  212. assert get_response2.status_code == 200
  213. assert get_response2.json()["id"] == project2["id"]