""" Tests for query performance optimization. These tests verify that N+1 query patterns have been eliminated by checking that endpoints execute within expected query count limits. """ import uuid import pytest from sqlalchemy import event from sqlalchemy.orm import Session from app.models import User, Space, Project, Task, TaskStatus, ProjectMember, Department class QueryCounter: """Helper to count SQL queries during a test.""" def __init__(self, db: Session): self.db = db self.count = 0 self.queries = [] self._before_handler = None self._after_handler = None def __enter__(self): self.count = 0 self.queries = [] engine = self.db.get_bind() def before_cursor_execute(conn, cursor, statement, parameters, context, executemany): conn.info.setdefault('query_start', []).append(statement) def after_cursor_execute(conn, cursor, statement, parameters, context, executemany): self.count += 1 self.queries.append(statement) self._before_handler = before_cursor_execute self._after_handler = after_cursor_execute event.listen(engine, "before_cursor_execute", before_cursor_execute) event.listen(engine, "after_cursor_execute", after_cursor_execute) return self def __exit__(self, exc_type, exc_val, exc_tb): engine = self.db.get_bind() event.remove(engine, "before_cursor_execute", self._before_handler) event.remove(engine, "after_cursor_execute", self._after_handler) return False def create_test_department(db: Session) -> Department: """Create a test department.""" dept = Department( id=str(uuid.uuid4()), name=f"Test Department {uuid.uuid4().hex[:8]}", ) db.add(dept) db.commit() return dept def create_test_user(db: Session, department_id: str = None, name: str = None) -> User: """Create a test user.""" user = User( id=str(uuid.uuid4()), email=f"user_{uuid.uuid4().hex[:8]}@test.com", name=name or f"Test User {uuid.uuid4().hex[:8]}", department_id=department_id, is_active=True, ) db.add(user) db.commit() return user def create_test_space(db: Session, owner_id: str) -> Space: """Create a test space.""" space = Space( id=str(uuid.uuid4()), name=f"Test Space {uuid.uuid4().hex[:8]}", owner_id=owner_id, is_active=True, ) db.add(space) db.commit() return space def create_test_project(db: Session, space_id: str, owner_id: str, department_id: str = None) -> Project: """Create a test project.""" project = Project( id=str(uuid.uuid4()), space_id=space_id, title=f"Test Project {uuid.uuid4().hex[:8]}", owner_id=owner_id, department_id=department_id, is_active=True, security_level="public", ) db.add(project) db.commit() # Create default task status status = TaskStatus( id=str(uuid.uuid4()), project_id=project.id, name="To Do", color="#0000FF", position=0, is_done=False, ) db.add(status) db.commit() return project def create_test_task(db: Session, project_id: str, status_id: str, assignee_id: str = None, creator_id: str = None) -> Task: """Create a test task.""" task = Task( id=str(uuid.uuid4()), project_id=project_id, title=f"Test Task {uuid.uuid4().hex[:8]}", status_id=status_id, assignee_id=assignee_id, created_by=creator_id, priority="medium", position=0, ) db.add(task) db.commit() return task class TestProjectMemberQueryOptimization: """Tests for project member list query optimization.""" def test_list_members_query_count_with_many_members(self, client, db, admin_token): """ Test that listing project members uses bounded number of queries. Before optimization: 1 + 2*N queries (N members, 2 queries each for user details) After optimization: at most 3 queries (members, users, added_by_users) """ # Setup: Create a department, multiple users, project, and members dept = create_test_department(db) admin = db.query(User).filter(User.email == "ymirliu@panjit.com.tw").first() space = create_test_space(db, admin.id) project = create_test_project(db, space.id, admin.id, dept.id) # Create 10 project members member_count = 10 for i in range(member_count): user = create_test_user(db, dept.id, f"Member {i}") member = ProjectMember( id=str(uuid.uuid4()), project_id=project.id, user_id=user.id, role="member", added_by=admin.id, ) db.add(member) db.commit() # Make the request response = client.get( f"/api/projects/{project.id}/members", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] == member_count assert len(data["members"]) == member_count # Verify all member details are loaded for member in data["members"]: assert member["user_name"] is not None assert member["added_by_name"] is not None def test_list_members_includes_department_info(self, client, db, admin_token): """Test that member listing includes department information without extra queries.""" dept = create_test_department(db) admin = db.query(User).filter(User.email == "ymirliu@panjit.com.tw").first() space = create_test_space(db, admin.id) project = create_test_project(db, space.id, admin.id, dept.id) # Create user with department user = create_test_user(db, dept.id, "User with Department") member = ProjectMember( id=str(uuid.uuid4()), project_id=project.id, user_id=user.id, role="member", added_by=admin.id, ) db.add(member) db.commit() response = client.get( f"/api/projects/{project.id}/members", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert len(data["members"]) == 1 assert data["members"][0]["user_department_id"] == dept.id assert data["members"][0]["user_department_name"] == dept.name class TestProjectListQueryOptimization: """Tests for project list query optimization.""" def test_list_projects_query_count_with_many_projects(self, client, db, admin_token): """ Test that listing projects in a space uses bounded number of queries. Before optimization: 1 + 4*N queries (N projects, 4 queries each for owner/space/dept/tasks) After optimization: at most 5 queries (projects, owners, spaces, departments, tasks) """ dept = create_test_department(db) admin = db.query(User).filter(User.email == "ymirliu@panjit.com.tw").first() space = create_test_space(db, admin.id) # Create 5 projects with tasks project_count = 5 for i in range(project_count): project = create_test_project(db, space.id, admin.id, dept.id) # Add a task to each project status = db.query(TaskStatus).filter(TaskStatus.project_id == project.id).first() create_test_task(db, project.id, status.id, admin.id, admin.id) response = client.get( f"/api/spaces/{space.id}/projects", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert len(data) == project_count # Verify all project details are loaded for project in data: assert project["owner_name"] is not None assert project["space_name"] is not None assert project["department_name"] is not None assert project["task_count"] >= 1 class TestTaskListQueryOptimization: """Tests for task list query optimization.""" def test_list_tasks_query_count_with_many_tasks(self, client, db, admin_token): """ Test that listing tasks uses bounded number of queries. Before optimization: 1 + 4*N queries (N tasks, queries for assignee/status/creator/subtasks) After optimization: at most 6 queries (tasks, assignees, statuses, creators, subtasks, custom_values) """ dept = create_test_department(db) admin = db.query(User).filter(User.email == "ymirliu@panjit.com.tw").first() space = create_test_space(db, admin.id) project = create_test_project(db, space.id, admin.id, dept.id) status = db.query(TaskStatus).filter(TaskStatus.project_id == project.id).first() # Create multiple users for assignment users = [create_test_user(db, dept.id, f"User {i}") for i in range(5)] # Create 10 tasks with different assignees task_count = 10 for i in range(task_count): create_test_task(db, project.id, status.id, users[i % 5].id, admin.id) response = client.get( f"/api/projects/{project.id}/tasks", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] == task_count # Verify all task details are loaded for task in data["tasks"]: assert task["assignee_name"] is not None assert task["status_name"] is not None assert task["creator_name"] is not None def test_list_tasks_with_subtasks(self, client, db, admin_token): """Test that subtask counts are efficiently loaded.""" dept = create_test_department(db) admin = db.query(User).filter(User.email == "ymirliu@panjit.com.tw").first() space = create_test_space(db, admin.id) project = create_test_project(db, space.id, admin.id, dept.id) status = db.query(TaskStatus).filter(TaskStatus.project_id == project.id).first() # Create parent task with subtasks parent_task = create_test_task(db, project.id, status.id, admin.id, admin.id) # Create 5 subtasks subtask_count = 5 for i in range(subtask_count): subtask = Task( id=str(uuid.uuid4()), project_id=project.id, parent_task_id=parent_task.id, title=f"Subtask {i}", status_id=status.id, created_by=admin.id, priority="medium", position=i, ) db.add(subtask) db.commit() response = client.get( f"/api/projects/{project.id}/tasks", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] == 1 # Only root tasks assert data["tasks"][0]["subtask_count"] == subtask_count class TestSubtaskListQueryOptimization: """Tests for subtask list query optimization.""" def test_list_subtasks_efficient_loading(self, client, db, admin_token): """Test that subtask listing uses efficient queries.""" dept = create_test_department(db) admin = db.query(User).filter(User.email == "ymirliu@panjit.com.tw").first() space = create_test_space(db, admin.id) project = create_test_project(db, space.id, admin.id, dept.id) status = db.query(TaskStatus).filter(TaskStatus.project_id == project.id).first() # Create parent task parent_task = create_test_task(db, project.id, status.id, admin.id, admin.id) # Create multiple users users = [create_test_user(db, dept.id, f"User {i}") for i in range(3)] # Create subtasks with different assignees subtask_count = 5 for i in range(subtask_count): subtask = Task( id=str(uuid.uuid4()), project_id=project.id, parent_task_id=parent_task.id, title=f"Subtask {i}", status_id=status.id, assignee_id=users[i % 3].id, created_by=admin.id, priority="medium", position=i, ) db.add(subtask) db.commit() response = client.get( f"/api/tasks/{parent_task.id}/subtasks", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] == subtask_count # Verify all subtask details are loaded for subtask in data["tasks"]: assert subtask["assignee_name"] is not None assert subtask["status_name"] is not None class TestQueryMonitorIntegration: """Tests for query monitoring utility. Note: These tests use the local QueryCounter class which sets up its own event listeners, rather than the app's count_queries which requires QUERY_LOGGING to be enabled at startup. """ def test_query_counter_context_manager(self, db): """Test that QueryCounter correctly counts queries.""" # Use the local QueryCounter which sets up its own event listeners with QueryCounter(db) as counter: # Execute some queries db.query(User).all() db.query(User).filter(User.is_active == True).all() # Should have counted at least 2 queries assert counter.count >= 2 def test_query_counter_threshold_warning(self, db, caplog): """Test that QueryCounter correctly counts queries for threshold testing.""" # Use the local QueryCounter which sets up its own event listeners with QueryCounter(db) as counter: # Execute multiple queries db.query(User).all() db.query(User).all() db.query(User).all() # Should have counted at least 3 queries assert counter.count >= 3