""" Property-based tests for Project creation. Tests universal properties that should hold for all valid project data. Feature: annotation-platform Property 1: Project creation adds to list Validates: Requirements 1.3 """ import os import pytest from hypothesis import given, strategies as st, settings, HealthCheck from fastapi.testclient import TestClient # Use a test database TEST_DB_PATH = "test_property_annotation_platform.db" @pytest.fixture(scope="function", autouse=True) def setup_test_db(): """Setup test database before each test and cleanup after.""" # Set test database path original_db_path = os.environ.get("DATABASE_PATH") os.environ["DATABASE_PATH"] = TEST_DB_PATH # Remove existing test database if os.path.exists(TEST_DB_PATH): os.remove(TEST_DB_PATH) # Import after setting env var from database import init_database init_database() yield # Cleanup if os.path.exists(TEST_DB_PATH): os.remove(TEST_DB_PATH) # Restore original path if original_db_path: os.environ["DATABASE_PATH"] = original_db_path elif "DATABASE_PATH" in os.environ: del os.environ["DATABASE_PATH"] def get_test_client(): """Create a test client.""" from main import app return TestClient(app) # Strategy for generating valid project names (non-empty strings) valid_project_names = st.text( alphabet=st.characters( whitelist_categories=("Lu", "Ll", "Nd", "P", "Zs"), min_codepoint=32, max_codepoint=126 ), min_size=1, max_size=100 ).filter(lambda x: x.strip() != "") # Ensure not just whitespace # Strategy for generating project descriptions project_descriptions = st.text( alphabet=st.characters( whitelist_categories=("Lu", "Ll", "Nd", "P", "Zs"), min_codepoint=32, max_codepoint=126 ), max_size=500 ) # Strategy for generating valid Label Studio configs (non-empty strings) valid_configs = st.text( alphabet=st.characters( whitelist_categories=("Lu", "Ll", "Nd", "P", "Zs"), min_codepoint=32, max_codepoint=126 ), min_size=1, max_size=200 ).filter(lambda x: x.strip() != "") # Ensure not just whitespace @settings(max_examples=100, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture]) @given( name=valid_project_names, description=project_descriptions, config=valid_configs ) def test_property_1_project_creation_adds_to_list( name: str, description: str, config: str ): """ Feature: annotation-platform, Property 1: Project creation adds to list Property: For any valid project data (non-empty name and config), creating a project should result in the project appearing in the projects list with a unique ID. Validates: Requirements 1.3 This property tests that: 1. Creating a project with valid data succeeds 2. The created project has a unique ID 3. The project appears in the list of all projects 4. The project data is preserved correctly """ # Create test client for this example test_client = get_test_client() # Get initial project count initial_response = test_client.get("/api/projects") assert initial_response.status_code == 200 initial_projects = initial_response.json() initial_count = len(initial_projects) # Create project with generated data project_data = { "name": name, "description": description, "config": config } create_response = test_client.post("/api/projects", json=project_data) # Verify creation succeeded assert create_response.status_code == 201, \ f"Project creation failed with status {create_response.status_code}" created_project = create_response.json() # Verify project has a unique ID assert "id" in created_project, "Created project should have an ID" assert created_project["id"] is not None, "Project ID should not be None" assert created_project["id"] != "", "Project ID should not be empty" assert created_project["id"].startswith("proj_"), \ "Project ID should start with 'proj_' prefix" # Verify project data is preserved assert created_project["name"] == name, \ "Created project name should match input" assert created_project["description"] == description, \ "Created project description should match input" assert created_project["config"] == config, \ "Created project config should match input" # Verify project appears in list list_response = test_client.get("/api/projects") assert list_response.status_code == 200 projects_list = list_response.json() # Verify list grew by exactly one assert len(projects_list) == initial_count + 1, \ "Project list should grow by exactly one after creation" # Verify the created project is in the list project_ids = [p["id"] for p in projects_list] assert created_project["id"] in project_ids, \ "Created project should appear in projects list" # Find the created project in the list and verify data created_in_list = next( (p for p in projects_list if p["id"] == created_project["id"]), None ) assert created_in_list is not None, \ "Created project should be findable in list" assert created_in_list["name"] == name, \ "Project name in list should match input" assert created_in_list["description"] == description, \ "Project description in list should match input" assert created_in_list["config"] == config, \ "Project config in list should match input" # Verify task_count is initialized to 0 assert created_project["task_count"] == 0, \ "New project should have task_count of 0" assert created_in_list["task_count"] == 0, \ "New project in list should have task_count of 0" @settings(max_examples=50, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture]) @given( name1=valid_project_names, name2=valid_project_names, config=valid_configs ) def test_property_1_multiple_projects_have_unique_ids( name1: str, name2: str, config: str ): """ Feature: annotation-platform, Property 1: Project creation adds to list Property: Creating multiple projects should result in each project having a unique ID, even if they have the same name. Validates: Requirements 1.3 This property tests that: 1. Multiple projects can be created 2. Each project gets a unique ID 3. Projects with the same name still get different IDs """ # Create test client for this example test_client = get_test_client() # Create first project project1_data = { "name": name1, "description": "First project", "config": config } response1 = test_client.post("/api/projects", json=project1_data) assert response1.status_code == 201 project1 = response1.json() # Create second project project2_data = { "name": name2, "description": "Second project", "config": config } response2 = test_client.post("/api/projects", json=project2_data) assert response2.status_code == 201 project2 = response2.json() # Verify both projects have IDs assert "id" in project1 assert "id" in project2 # Verify IDs are unique assert project1["id"] != project2["id"], \ "Different projects should have unique IDs" # Verify both projects appear in list list_response = test_client.get("/api/projects") assert list_response.status_code == 200 projects_list = list_response.json() project_ids = [p["id"] for p in projects_list] assert project1["id"] in project_ids, \ "First project should be in list" assert project2["id"] in project_ids, \ "Second project should be in list" # Verify we can retrieve each project individually get_response1 = test_client.get(f"/api/projects/{project1['id']}") assert get_response1.status_code == 200 assert get_response1.json()["id"] == project1["id"] get_response2 = test_client.get(f"/api/projects/{project2['id']}") assert get_response2.status_code == 200 assert get_response2.json()["id"] == project2["id"]