import pytest from unittest.mock import MagicMock from fastapi.testclient import TestClient from app.main import app from app.models import Task from app.middleware.auth import check_task_access, check_task_edit_access client = TestClient(app) def get_mock_user(is_admin=False): user = MagicMock() user.id = "test-user-id" user.is_system_admin = is_admin user.department_id = "dept-1" return user def get_mock_project(owner_id="owner-id"): project = MagicMock() project.id = "project-id" project.owner_id = owner_id project.security_level = "public" project.department_id = "dept-1" return project def get_mock_task(created_by="creator-id", assignee_id=None): task = MagicMock() task.id = "task-id" task.created_by = created_by task.assignee_id = assignee_id return task class TestTaskModel: """Test Task model.""" def test_task_creation(self): """Test Task model can be instantiated.""" task = Task( id="test-id", project_id="project-id", title="Test Task", priority="medium", created_by="user-id", ) assert task.title == "Test Task" assert task.priority == "medium" class TestTaskRoutes: """Test task routes exist.""" def test_task_routes_exist(self): """Test that all task routes are registered.""" routes = [route.path for route in app.routes if hasattr(route, 'path')] assert "/api/projects/{project_id}/tasks" in routes assert "/api/tasks/{task_id}" in routes assert "/api/tasks/{task_id}/status" in routes assert "/api/tasks/{task_id}/assign" in routes class TestTaskPermissions: """Test task permission logic.""" def test_admin_has_full_access(self): """Test that admin has full access to all tasks.""" admin = get_mock_user(is_admin=True) project = get_mock_project() task = get_mock_task() assert check_task_access(admin, task, project) == True assert check_task_edit_access(admin, task, project) == True def test_project_owner_can_edit_any_task(self): """Test that project owner can edit any task in the project.""" user = get_mock_user() project = get_mock_project(owner_id=user.id) task = get_mock_task(created_by="other-user") assert check_task_edit_access(user, task, project) == True def test_creator_can_edit_own_task(self): """Test that task creator can edit their own task.""" user = get_mock_user() project = get_mock_project(owner_id="other-user") task = get_mock_task(created_by=user.id) assert check_task_edit_access(user, task, project) == True def test_assignee_can_edit_assigned_task(self): """Test that assignee can edit their assigned task.""" user = get_mock_user() project = get_mock_project(owner_id="other-user") task = get_mock_task(created_by="other-user", assignee_id=user.id) assert check_task_edit_access(user, task, project) == True def test_unrelated_user_cannot_edit(self): """Test that unrelated user cannot edit task.""" user = get_mock_user() project = get_mock_project(owner_id="project-owner") task = get_mock_task(created_by="creator", assignee_id="assignee") assert check_task_edit_access(user, task, project) == False class TestSubtaskDepth: """Test subtask depth limiting.""" def test_max_depth_constant(self): """Test that MAX_SUBTASK_DEPTH is defined.""" from app.api.tasks.router import MAX_SUBTASK_DEPTH assert MAX_SUBTASK_DEPTH == 2 class TestDateRangeFilter: """Test date range filter for calendar view.""" def test_due_after_filter(self, client, db, admin_token): """Test filtering tasks with due_date >= due_after.""" from datetime import datetime, timedelta from app.models import Space, Project, Task, TaskStatus # Create test data space = Space( id="test-space-id", name="Test Space", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-id", space_id="test-space-id", title="Test Project", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-id", project_id="test-project-id", name="To Do", color="#808080", position=0, ) db.add(status) # Create tasks with different due dates now = datetime.now() task1 = Task( id="task-1", project_id="test-project-id", title="Task Due Yesterday", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id", due_date=now - timedelta(days=1), ) task2 = Task( id="task-2", project_id="test-project-id", title="Task Due Today", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id", due_date=now, ) task3 = Task( id="task-3", project_id="test-project-id", title="Task Due Tomorrow", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id", due_date=now + timedelta(days=1), ) task4 = Task( id="task-4", project_id="test-project-id", title="Task No Due Date", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id", due_date=None, ) db.add_all([task1, task2, task3, task4]) db.commit() # Filter tasks due today or later due_after = now.isoformat() response = client.get( f"/api/projects/test-project-id/tasks?due_after={due_after}", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() # Should return task2 and task3 (due today and tomorrow) assert data["total"] == 2 task_ids = [t["id"] for t in data["tasks"]] assert "task-2" in task_ids assert "task-3" in task_ids assert "task-1" not in task_ids assert "task-4" not in task_ids def test_due_before_filter(self, client, db, admin_token): """Test filtering tasks with due_date <= due_before.""" from datetime import datetime, timedelta from app.models import Space, Project, Task, TaskStatus # Create test data space = Space( id="test-space-id-2", name="Test Space 2", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-id-2", space_id="test-space-id-2", title="Test Project 2", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-id-2", project_id="test-project-id-2", name="To Do", color="#808080", position=0, ) db.add(status) # Create tasks with different due dates now = datetime.now() task1 = Task( id="task-b-1", project_id="test-project-id-2", title="Task Due Yesterday", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-2", due_date=now - timedelta(days=1), ) task2 = Task( id="task-b-2", project_id="test-project-id-2", title="Task Due Today", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-2", due_date=now, ) task3 = Task( id="task-b-3", project_id="test-project-id-2", title="Task Due Tomorrow", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-2", due_date=now + timedelta(days=1), ) db.add_all([task1, task2, task3]) db.commit() # Filter tasks due today or earlier due_before = now.isoformat() response = client.get( f"/api/projects/test-project-id-2/tasks?due_before={due_before}", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() # Should return task1 and task2 (due yesterday and today) assert data["total"] == 2 task_ids = [t["id"] for t in data["tasks"]] assert "task-b-1" in task_ids assert "task-b-2" in task_ids assert "task-b-3" not in task_ids def test_date_range_filter_combined(self, client, db, admin_token): """Test filtering tasks within a date range (due_after AND due_before).""" from datetime import datetime, timedelta from app.models import Space, Project, Task, TaskStatus # Create test data space = Space( id="test-space-id-3", name="Test Space 3", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-id-3", space_id="test-space-id-3", title="Test Project 3", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-id-3", project_id="test-project-id-3", name="To Do", color="#808080", position=0, ) db.add(status) # Create tasks spanning a week now = datetime.now() start_of_week = now - timedelta(days=now.weekday()) # Monday end_of_week = start_of_week + timedelta(days=6) # Sunday task_before = Task( id="task-c-before", project_id="test-project-id-3", title="Task Before Week", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-3", due_date=start_of_week - timedelta(days=1), ) task_in_week = Task( id="task-c-in-week", project_id="test-project-id-3", title="Task In Week", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-3", due_date=start_of_week + timedelta(days=3), ) task_after = Task( id="task-c-after", project_id="test-project-id-3", title="Task After Week", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-3", due_date=end_of_week + timedelta(days=1), ) db.add_all([task_before, task_in_week, task_after]) db.commit() # Filter tasks within this week due_after = start_of_week.isoformat() due_before = end_of_week.isoformat() response = client.get( f"/api/projects/test-project-id-3/tasks?due_after={due_after}&due_before={due_before}", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() # Should only return the task in the week assert data["total"] == 1 assert data["tasks"][0]["id"] == "task-c-in-week" def test_date_filter_with_no_due_date(self, client, db, admin_token): """Test that tasks without due_date are excluded from date range filters.""" from datetime import datetime, timedelta from app.models import Space, Project, Task, TaskStatus # Create test data space = Space( id="test-space-id-4", name="Test Space 4", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-id-4", space_id="test-space-id-4", title="Test Project 4", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-id-4", project_id="test-project-id-4", name="To Do", color="#808080", position=0, ) db.add(status) # Create tasks - some with due_date, some without now = datetime.now() task_with_date = Task( id="task-d-with-date", project_id="test-project-id-4", title="Task With Due Date", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-4", due_date=now, ) task_without_date = Task( id="task-d-without-date", project_id="test-project-id-4", title="Task Without Due Date", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-4", due_date=None, ) db.add_all([task_with_date, task_without_date]) db.commit() # When using date filter, tasks without due_date should be excluded due_after = (now - timedelta(days=1)).isoformat() due_before = (now + timedelta(days=1)).isoformat() response = client.get( f"/api/projects/test-project-id-4/tasks?due_after={due_after}&due_before={due_before}", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() # Should only return the task with due_date assert data["total"] == 1 assert data["tasks"][0]["id"] == "task-d-with-date" def test_date_filter_backward_compatibility(self, client, db, admin_token): """Test that not providing date filters returns all tasks (backward compatibility).""" from datetime import datetime, timedelta from app.models import Space, Project, Task, TaskStatus # Create test data space = Space( id="test-space-id-5", name="Test Space 5", owner_id="00000000-0000-0000-0000-000000000001", ) db.add(space) project = Project( id="test-project-id-5", space_id="test-space-id-5", title="Test Project 5", owner_id="00000000-0000-0000-0000-000000000001", security_level="public", ) db.add(project) status = TaskStatus( id="test-status-id-5", project_id="test-project-id-5", name="To Do", color="#808080", position=0, ) db.add(status) # Create tasks with and without due dates now = datetime.now() task1 = Task( id="task-e-1", project_id="test-project-id-5", title="Task 1", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-5", due_date=now, ) task2 = Task( id="task-e-2", project_id="test-project-id-5", title="Task 2", priority="medium", created_by="00000000-0000-0000-0000-000000000001", status_id="test-status-id-5", due_date=None, ) db.add_all([task1, task2]) db.commit() # Request without date filters - should return all tasks response = client.get( "/api/projects/test-project-id-5/tasks", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() # Should return both tasks assert data["total"] == 2