feat: implement security, error resilience, and query optimization proposals

Security Validation (enhance-security-validation):
- JWT secret validation with entropy checking and pattern detection
- CSRF protection middleware with token generation/validation
- Frontend CSRF token auto-injection for DELETE/PUT/PATCH requests
- MIME type validation with magic bytes detection for file uploads

Error Resilience (add-error-resilience):
- React ErrorBoundary component with fallback UI and retry functionality
- ErrorBoundaryWithI18n wrapper for internationalization support
- Page-level and section-level error boundaries in App.tsx

Query Performance (optimize-query-performance):
- Query monitoring utility with threshold warnings
- N+1 query fixes using joinedload/selectinload
- Optimized project members, tasks, and subtasks endpoints

Bug Fixes:
- WebSocket session management (P0): Return primitives instead of ORM objects
- LIKE query injection (P1): Escape special characters in search queries

Tests: 543 backend tests, 56 frontend tests passing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-11 18:41:19 +08:00
parent 2cb591ef23
commit 679b89ae4c
41 changed files with 3673 additions and 153 deletions

View File

@@ -0,0 +1,408 @@
"""
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