""" Tests for Task Dependencies (Gantt View Feature) Tests cover: - TaskDependency CRUD operations - Circular dependency detection - Date validation (start_date <= due_date) - Dependency constraint validation """ import pytest from datetime import datetime, timedelta from unittest.mock import MagicMock from app.models import Task, TaskDependency, Space, Project, TaskStatus from app.services.dependency_service import DependencyService, DependencyValidationError class TestDependencyService: """Test DependencyService validation logic.""" def test_self_reference_rejected(self, db): """Test that a task cannot depend on itself.""" # Create test data space = Space( id="test-space-dep-1", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-dep-1", space_id="test-space-dep-1", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-dep-1", project_id="test-project-dep-1", name="To Do", color="#808080", position=0, ) db.add(status) task = Task( id="task-self-ref", project_id="test-project-dep-1", title="Self Reference Task", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-dep-1", ) db.add(task) db.commit() # Attempt self-reference with pytest.raises(DependencyValidationError) as exc_info: DependencyService.validate_dependency( db, predecessor_id="task-self-ref", successor_id="task-self-ref" ) assert exc_info.value.error_type == "self_reference" assert "cannot depend on itself" in exc_info.value.message def test_cross_project_rejected(self, db): """Test that dependencies cannot be created across different projects.""" # Create test data space = Space( id="test-space-cross", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project1 = Project( id="test-project-cross-1", space_id="test-space-cross", title="Project 1", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) project2 = Project( id="test-project-cross-2", space_id="test-space-cross", title="Project 2", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add_all([project1, project2]) status1 = TaskStatus( id="test-status-cross-1", project_id="test-project-cross-1", name="To Do", color="#808080", position=0, ) status2 = TaskStatus( id="test-status-cross-2", project_id="test-project-cross-2", name="To Do", color="#808080", position=0, ) db.add_all([status1, status2]) task1 = Task( id="task-cross-1", project_id="test-project-cross-1", title="Task in Project 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-cross-1", ) task2 = Task( id="task-cross-2", project_id="test-project-cross-2", title="Task in Project 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-cross-2", ) db.add_all([task1, task2]) db.commit() # Attempt cross-project dependency with pytest.raises(DependencyValidationError) as exc_info: DependencyService.validate_dependency( db, predecessor_id="task-cross-1", successor_id="task-cross-2" ) assert exc_info.value.error_type == "cross_project" def test_duplicate_dependency_rejected(self, db): """Test that duplicate dependencies are rejected.""" # Create test data space = Space( id="test-space-dup", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-dup", space_id="test-space-dup", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-dup", project_id="test-project-dup", name="To Do", color="#808080", position=0, ) db.add(status) task1 = Task( id="task-dup-1", project_id="test-project-dup", title="Task 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-dup", ) task2 = Task( id="task-dup-2", project_id="test-project-dup", title="Task 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-dup", ) db.add_all([task1, task2]) # Create existing dependency dep = TaskDependency( id="dep-dup", predecessor_id="task-dup-1", successor_id="task-dup-2", dependency_type="FS", lag_days=0, ) db.add(dep) db.commit() # Attempt to create duplicate with pytest.raises(DependencyValidationError) as exc_info: DependencyService.validate_dependency( db, predecessor_id="task-dup-1", successor_id="task-dup-2" ) assert exc_info.value.error_type == "duplicate" def test_dependency_limit_exceeded(self, db): """Test that dependency limit (10 direct dependencies) is enforced.""" # Create test data space = Space( id="test-space-limit", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-limit", space_id="test-space-limit", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-limit", project_id="test-project-limit", name="To Do", color="#808080", position=0, ) db.add(status) # Create successor task successor = Task( id="task-limit-successor", project_id="test-project-limit", title="Successor Task", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-limit", ) db.add(successor) # Create 10 predecessor tasks with dependencies for i in range(10): pred = Task( id=f"task-limit-pred-{i}", project_id="test-project-limit", title=f"Predecessor Task {i}", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-limit", ) db.add(pred) dep = TaskDependency( id=f"dep-limit-{i}", predecessor_id=f"task-limit-pred-{i}", successor_id="task-limit-successor", dependency_type="FS", lag_days=0, ) db.add(dep) # Create one more predecessor extra_pred = Task( id="task-limit-pred-extra", project_id="test-project-limit", title="Extra Predecessor", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-limit", ) db.add(extra_pred) db.commit() # Attempt to add 11th dependency with pytest.raises(DependencyValidationError) as exc_info: DependencyService.validate_dependency( db, predecessor_id="task-limit-pred-extra", successor_id="task-limit-successor" ) assert exc_info.value.error_type == "limit_exceeded" class TestCircularDependencyDetection: """Test circular dependency detection.""" def test_simple_circular_dependency(self, db): """Test detection of A -> B -> A circular dependency.""" # Create test data space = Space( id="test-space-circ-1", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-circ-1", space_id="test-space-circ-1", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-circ-1", project_id="test-project-circ-1", name="To Do", color="#808080", position=0, ) db.add(status) taskA = Task( id="task-circ-A", project_id="test-project-circ-1", title="Task A", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-circ-1", ) taskB = Task( id="task-circ-B", project_id="test-project-circ-1", title="Task B", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-circ-1", ) db.add_all([taskA, taskB]) # A -> B (A is predecessor, B is successor) dep_AB = TaskDependency( id="dep-circ-AB", predecessor_id="task-circ-A", successor_id="task-circ-B", dependency_type="FS", lag_days=0, ) db.add(dep_AB) db.commit() # Attempt B -> A (would create cycle) with pytest.raises(DependencyValidationError) as exc_info: DependencyService.validate_dependency( db, predecessor_id="task-circ-B", successor_id="task-circ-A" ) assert exc_info.value.error_type == "circular" assert "cycle" in exc_info.value.details def test_transitive_circular_dependency(self, db): """Test detection of A -> B -> C -> A circular dependency.""" # Create test data space = Space( id="test-space-circ-2", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-circ-2", space_id="test-space-circ-2", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-circ-2", project_id="test-project-circ-2", name="To Do", color="#808080", position=0, ) db.add(status) taskA = Task( id="task-circ2-A", project_id="test-project-circ-2", title="Task A", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-circ-2", ) taskB = Task( id="task-circ2-B", project_id="test-project-circ-2", title="Task B", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-circ-2", ) taskC = Task( id="task-circ2-C", project_id="test-project-circ-2", title="Task C", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-circ-2", ) db.add_all([taskA, taskB, taskC]) # A -> B dep_AB = TaskDependency( id="dep-circ2-AB", predecessor_id="task-circ2-A", successor_id="task-circ2-B", dependency_type="FS", lag_days=0, ) # B -> C dep_BC = TaskDependency( id="dep-circ2-BC", predecessor_id="task-circ2-B", successor_id="task-circ2-C", dependency_type="FS", lag_days=0, ) db.add_all([dep_AB, dep_BC]) db.commit() # Attempt C -> A (would create cycle A -> B -> C -> A) with pytest.raises(DependencyValidationError) as exc_info: DependencyService.validate_dependency( db, predecessor_id="task-circ2-C", successor_id="task-circ2-A" ) assert exc_info.value.error_type == "circular" def test_valid_dependency_chain(self, db): """Test that valid dependency chains are accepted.""" # Create test data space = Space( id="test-space-valid", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-valid", space_id="test-space-valid", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-valid", project_id="test-project-valid", name="To Do", color="#808080", position=0, ) db.add(status) taskA = Task( id="task-valid-A", project_id="test-project-valid", title="Task A", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-valid", ) taskB = Task( id="task-valid-B", project_id="test-project-valid", title="Task B", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-valid", ) taskC = Task( id="task-valid-C", project_id="test-project-valid", title="Task C", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-valid", ) db.add_all([taskA, taskB, taskC]) # A -> B dep_AB = TaskDependency( id="dep-valid-AB", predecessor_id="task-valid-A", successor_id="task-valid-B", dependency_type="FS", lag_days=0, ) db.add(dep_AB) db.commit() # B -> C should be valid (no cycle) # This should NOT raise an exception DependencyService.validate_dependency( db, predecessor_id="task-valid-B", successor_id="task-valid-C" ) class TestDateValidation: """Test date validation logic.""" def test_start_date_after_due_date_rejected(self, db): """Test that start_date > due_date is rejected.""" # Create test data space = Space( id="test-space-date-1", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-date-1", space_id="test-space-date-1", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-date-1", project_id="test-project-date-1", name="To Do", color="#808080", position=0, ) db.add(status) now = datetime.now() task = Task( id="task-date-invalid", project_id="test-project-date-1", title="Invalid Date Task", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-date-1", start_date=now + timedelta(days=10), # Start after due due_date=now, ) db.add(task) db.commit() # Validate dates - start > due should fail violations = DependencyService.validate_date_constraints( task, start_date=now + timedelta(days=10), due_date=now, db=db ) assert len(violations) > 0 assert violations[0]["type"] == "date_order" def test_valid_date_range_accepted(self, db): """Test that valid date ranges are accepted.""" # Create test data space = Space( id="test-space-date-2", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-date-2", space_id="test-space-date-2", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-date-2", project_id="test-project-date-2", name="To Do", color="#808080", position=0, ) db.add(status) now = datetime.now() task = Task( id="task-date-valid", project_id="test-project-date-2", title="Valid Date Task", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-date-2", start_date=now, due_date=now + timedelta(days=10), ) db.add(task) db.commit() # Validate dates - start <= due should pass violations = DependencyService.validate_date_constraints( task, start_date=now, due_date=now + timedelta(days=10), db=db ) assert len(violations) == 0 def test_fs_dependency_date_constraint(self, db): """Test Finish-to-Start dependency date validation.""" # Create test data space = Space( id="test-space-fs", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-fs", space_id="test-space-fs", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-fs", project_id="test-project-fs", name="To Do", color="#808080", position=0, ) db.add(status) now = datetime.now() # Predecessor: ends on day 5 predecessor = Task( id="task-fs-pred", project_id="test-project-fs", title="Predecessor", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-fs", start_date=now, due_date=now + timedelta(days=5), ) # Successor: starts on day 3 (before predecessor ends - INVALID for FS) successor = Task( id="task-fs-succ", project_id="test-project-fs", title="Successor", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-fs", start_date=now + timedelta(days=3), due_date=now + timedelta(days=10), ) db.add_all([predecessor, successor]) # Create FS dependency dep = TaskDependency( id="dep-fs", predecessor_id="task-fs-pred", successor_id="task-fs-succ", dependency_type="FS", lag_days=0, ) db.add(dep) db.commit() # Validate successor's dates - should fail because start < predecessor's due violations = DependencyService.validate_date_constraints( successor, start_date=now + timedelta(days=3), due_date=now + timedelta(days=10), db=db ) assert len(violations) > 0 assert any(v["type"] == "dependency_constraint" for v in violations) def test_fs_dependency_with_lag(self, db): """Test Finish-to-Start dependency with lag days.""" # Create test data space = Space( id="test-space-fs-lag", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-fs-lag", space_id="test-space-fs-lag", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-fs-lag", project_id="test-project-fs-lag", name="To Do", color="#808080", position=0, ) db.add(status) now = datetime.now() # Predecessor: ends on day 5 predecessor = Task( id="task-fs-lag-pred", project_id="test-project-fs-lag", title="Predecessor", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-fs-lag", start_date=now, due_date=now + timedelta(days=5), ) # Successor: starts on day 6 (only 1 day after predecessor ends) # With 2 days lag, this is INVALID (should start on day 7) successor = Task( id="task-fs-lag-succ", project_id="test-project-fs-lag", title="Successor", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-fs-lag", start_date=now + timedelta(days=6), due_date=now + timedelta(days=10), ) db.add_all([predecessor, successor]) # Create FS dependency with 2 days lag dep = TaskDependency( id="dep-fs-lag", predecessor_id="task-fs-lag-pred", successor_id="task-fs-lag-succ", dependency_type="FS", lag_days=2, ) db.add(dep) db.commit() # Validate successor's dates - should fail because start < predecessor's due + lag violations = DependencyService.validate_date_constraints( successor, start_date=now + timedelta(days=6), due_date=now + timedelta(days=10), db=db ) assert len(violations) > 0 class TestDependencyCRUDAPI: """Test dependency CRUD API endpoints.""" def test_create_dependency(self, client, db, admin_token, csrf_token): """Test creating a dependency via API.""" # Create test data space = Space( id="test-space-api-1", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-api-1", space_id="test-space-api-1", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-api-1", project_id="test-project-api-1", name="To Do", color="#808080", position=0, ) db.add(status) task1 = Task( id="task-api-1", project_id="test-project-api-1", title="Task 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-1", ) task2 = Task( id="task-api-2", project_id="test-project-api-1", title="Task 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-1", ) db.add_all([task1, task2]) db.commit() # Create dependency via API response = client.post( "/api/tasks/task-api-2/dependencies", json={ "predecessor_id": "task-api-1", "dependency_type": "FS", "lag_days": 0 }, headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 201 data = response.json() assert data["predecessor_id"] == "task-api-1" assert data["successor_id"] == "task-api-2" assert data["dependency_type"] == "FS" def test_list_task_dependencies(self, client, db, admin_token): """Test listing dependencies for a task.""" # Create test data space = Space( id="test-space-api-2", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-api-2", space_id="test-space-api-2", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-api-2", project_id="test-project-api-2", name="To Do", color="#808080", position=0, ) db.add(status) task1 = Task( id="task-api-list-1", project_id="test-project-api-2", title="Task 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-2", ) task2 = Task( id="task-api-list-2", project_id="test-project-api-2", title="Task 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-2", ) db.add_all([task1, task2]) dep = TaskDependency( id="dep-api-list", predecessor_id="task-api-list-1", successor_id="task-api-list-2", dependency_type="FS", lag_days=0, ) db.add(dep) db.commit() # List dependencies response = client.get( "/api/tasks/task-api-list-2/dependencies", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] >= 1 assert any(d["predecessor_id"] == "task-api-list-1" for d in data["dependencies"]) def test_delete_dependency(self, client, db, admin_token, csrf_token): """Test deleting a dependency.""" # Create test data space = Space( id="test-space-api-3", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-api-3", space_id="test-space-api-3", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-api-3", project_id="test-project-api-3", name="To Do", color="#808080", position=0, ) db.add(status) task1 = Task( id="task-api-del-1", project_id="test-project-api-3", title="Task 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-3", ) task2 = Task( id="task-api-del-2", project_id="test-project-api-3", title="Task 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-3", ) db.add_all([task1, task2]) dep = TaskDependency( id="dep-api-del", predecessor_id="task-api-del-1", successor_id="task-api-del-2", dependency_type="FS", lag_days=0, ) db.add(dep) db.commit() # Delete dependency response = client.delete( "/api/task-dependencies/dep-api-del", headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 204 # Verify it's deleted dep_check = db.query(TaskDependency).filter( TaskDependency.id == "dep-api-del" ).first() assert dep_check is None def test_circular_dependency_rejected_via_api(self, client, db, admin_token, csrf_token): """Test that circular dependencies are rejected via API.""" # Create test data space = Space( id="test-space-api-circ", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-api-circ", space_id="test-space-api-circ", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-api-circ", project_id="test-project-api-circ", name="To Do", color="#808080", position=0, ) db.add(status) task1 = Task( id="task-api-circ-1", project_id="test-project-api-circ", title="Task 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-circ", ) task2 = Task( id="task-api-circ-2", project_id="test-project-api-circ", title="Task 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-api-circ", ) db.add_all([task1, task2]) # Create dependency: task1 -> task2 dep = TaskDependency( id="dep-api-circ", predecessor_id="task-api-circ-1", successor_id="task-api-circ-2", dependency_type="FS", lag_days=0, ) db.add(dep) db.commit() # Try to create circular dependency: task2 -> task1 response = client.post( "/api/tasks/task-api-circ-1/dependencies", json={ "predecessor_id": "task-api-circ-2", "dependency_type": "FS", "lag_days": 0 }, headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 400 data = response.json() assert data["detail"]["error_type"] == "circular" class TestTaskDateValidationAPI: """Test task date validation in task API.""" def test_create_task_with_invalid_dates_rejected(self, client, db, admin_token, csrf_token): """Test that creating a task with start_date > due_date is rejected.""" # Create test data space = Space( id="test-space-task-date-1", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-task-date-1", space_id="test-space-task-date-1", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-task-date-1", project_id="test-project-task-date-1", name="To Do", color="#808080", position=0, ) db.add(status) db.commit() now = datetime.now() # Try to create task with invalid dates response = client.post( "/api/projects/test-project-task-date-1/tasks", json={ "title": "Invalid Date Task", "start_date": (now + timedelta(days=10)).isoformat(), "due_date": now.isoformat(), }, headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 400 assert "Start date cannot be after due date" in response.json()["detail"] def test_update_task_with_invalid_dates_rejected(self, client, db, admin_token, csrf_token): """Test that updating a task to have start_date > due_date is rejected.""" # Create test data space = Space( id="test-space-task-date-2", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-task-date-2", space_id="test-space-task-date-2", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-task-date-2", project_id="test-project-task-date-2", name="To Do", color="#808080", position=0, ) db.add(status) now = datetime.now() task = Task( id="task-update-date", project_id="test-project-task-date-2", title="Valid Date Task", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-task-date-2", start_date=now, due_date=now + timedelta(days=10), ) db.add(task) db.commit() # Try to update with invalid dates response = client.patch( "/api/tasks/task-update-date", json={ "start_date": (now + timedelta(days=20)).isoformat(), }, headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 400 def test_create_task_with_valid_dates_accepted(self, client, db, admin_token, csrf_token): """Test that creating a task with valid dates is accepted.""" # Create test data space = Space( id="test-space-task-date-3", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-task-date-3", space_id="test-space-task-date-3", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-task-date-3", project_id="test-project-task-date-3", name="To Do", color="#808080", position=0, ) db.add(status) db.commit() now = datetime.now() # Create task with valid dates response = client.post( "/api/projects/test-project-task-date-3/tasks", json={ "title": "Valid Date Task", "start_date": now.isoformat(), "due_date": (now + timedelta(days=10)).isoformat(), }, headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 201 data = response.json() assert data["title"] == "Valid Date Task" class TestDependencyTypes: """Test different dependency types.""" def test_dependency_type_values(self): """Test that all dependency types are valid.""" from app.schemas.task_dependency import DependencyType assert DependencyType.FS.value == "FS" assert DependencyType.SS.value == "SS" assert DependencyType.FF.value == "FF" assert DependencyType.SF.value == "SF" def test_create_dependency_with_different_types(self, client, db, admin_token, csrf_token): """Test creating dependencies with different types via API.""" # Create test data space = Space( id="test-space-dep-types", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-dep-types", space_id="test-space-dep-types", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-dep-types", project_id="test-project-dep-types", name="To Do", color="#808080", position=0, ) db.add(status) # Create multiple tasks for i in range(5): task = Task( id=f"task-dep-type-{i}", project_id="test-project-dep-types", title=f"Task {i}", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-dep-types", ) db.add(task) db.commit() # Test each dependency type dep_types = ["FS", "SS", "FF", "SF"] for i, dep_type in enumerate(dep_types): response = client.post( f"/api/tasks/task-dep-type-{i+1}/dependencies", json={ "predecessor_id": "task-dep-type-0", "dependency_type": dep_type, "lag_days": i }, headers={"Authorization": f"Bearer {admin_token}", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 201 data = response.json() assert data["dependency_type"] == dep_type assert data["lag_days"] == i class TestTransitiveDependencies: """Test transitive dependency queries.""" def test_get_all_predecessors(self, db): """Test getting all transitive predecessors of a task.""" # Create test data space = Space( id="test-space-trans", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-trans", space_id="test-space-trans", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-trans", project_id="test-project-trans", name="To Do", color="#808080", position=0, ) db.add(status) # Create chain: A -> B -> C -> D for task_id in ["A", "B", "C", "D"]: task = Task( id=f"task-trans-{task_id}", project_id="test-project-trans", title=f"Task {task_id}", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-trans", ) db.add(task) # A -> B dep1 = TaskDependency( id="dep-trans-AB", predecessor_id="task-trans-A", successor_id="task-trans-B", dependency_type="FS", lag_days=0, ) # B -> C dep2 = TaskDependency( id="dep-trans-BC", predecessor_id="task-trans-B", successor_id="task-trans-C", dependency_type="FS", lag_days=0, ) # C -> D dep3 = TaskDependency( id="dep-trans-CD", predecessor_id="task-trans-C", successor_id="task-trans-D", dependency_type="FS", lag_days=0, ) db.add_all([dep1, dep2, dep3]) db.commit() # Get all predecessors of D predecessors = DependencyService.get_all_predecessors(db, "task-trans-D") # D depends on C, C depends on B, B depends on A assert "task-trans-C" in predecessors assert "task-trans-B" in predecessors assert "task-trans-A" in predecessors assert len(predecessors) == 3 def test_get_all_successors(self, db): """Test getting all transitive successors of a task.""" # Create test data space = Space( id="test-space-succ", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-succ", space_id="test-space-succ", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-succ", project_id="test-project-succ", name="To Do", color="#808080", position=0, ) db.add(status) # Create chain: A -> B -> C -> D for task_id in ["A", "B", "C", "D"]: task = Task( id=f"task-succ-{task_id}", project_id="test-project-succ", title=f"Task {task_id}", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-succ", ) db.add(task) # A -> B dep1 = TaskDependency( id="dep-succ-AB", predecessor_id="task-succ-A", successor_id="task-succ-B", dependency_type="FS", lag_days=0, ) # B -> C dep2 = TaskDependency( id="dep-succ-BC", predecessor_id="task-succ-B", successor_id="task-succ-C", dependency_type="FS", lag_days=0, ) # C -> D dep3 = TaskDependency( id="dep-succ-CD", predecessor_id="task-succ-C", successor_id="task-succ-D", dependency_type="FS", lag_days=0, ) db.add_all([dep1, dep2, dep3]) db.commit() # Get all successors of A successors = DependencyService.get_all_successors(db, "task-succ-A") # A is predecessor of B, B is predecessor of C, C is predecessor of D assert "task-succ-B" in successors assert "task-succ-C" in successors assert "task-succ-D" in successors assert len(successors) == 3