Files
DeepAudit/backend/tests/test_api_tasks.py
lintsinghuaandClaude Opus 4.6 51d94acb4c test: add comprehensive test suite for production readiness
Backend (951 tests):
- Core security: JWT, password hashing, encryption
- Agent core: state, config, validation, retry, circuit breaker,
  rate limiter, persistence, fallback, context, executor
- API endpoints: tasks, projects, rules, prompts, auth deps
- Services: json_parser, event_manager, stream_handler, tools,
  tokenizer, prompt_cache, report generator, scanner utils
- Models, schemas, LLM factory/base adapter

Frontend (331 tests, vitest + @testing-library/react):
- API clients: serverClient, agentTasks, prompts, rules
- Utilities: fetchWrapper, errorHandler, logger, projectUtils,
  zipStorage, performanceMonitor, utils
- Hooks: useAsync, useDebounce, useLocalStorage
- Config, services, AgentAudit utils
- Test infra: vitest.config.ts, setup.ts, coverage-v8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 09:19:45 +08:00

436 lines
14 KiB
Python

"""
Tests for app/api/v1/endpoints/tasks.py
Covers:
- list_tasks: listing tasks, project filtering, empty results
- read_task: get single task, 404, 403 (wrong user)
- cancel_task: successful cancel, 404, 403, 400 (wrong status)
- read_task_issues: list issues, 404, 403
- update_issue: resolve issue, 404
"""
import pytest
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import HTTPException
from app.api.v1.endpoints.tasks import (
list_tasks,
read_task,
cancel_task,
read_task_issues,
update_issue,
IssueUpdateSchema,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
USER_ID = "user-001"
OTHER_USER_ID = "user-002"
PROJECT_ID = "proj-001"
TASK_ID = "task-001"
def _make_user(user_id=USER_ID, is_active=True, is_superuser=False):
user = MagicMock()
user.id = user_id
user.is_active = is_active
user.is_superuser = is_superuser
return user
def _make_task(
task_id=TASK_ID,
project_id=PROJECT_ID,
created_by=USER_ID,
status="completed",
with_project=True,
):
task = MagicMock()
task.id = task_id
task.project_id = project_id
task.task_type = "repository"
task.status = status
task.branch_name = "main"
task.exclude_patterns = "[]"
task.scan_config = "{}"
task.total_files = 10
task.scanned_files = 10
task.total_lines = 500
task.issues_count = 3
task.quality_score = 85.0
task.started_at = datetime.now(timezone.utc)
task.completed_at = datetime.now(timezone.utc)
task.created_by = created_by
task.created_at = datetime.now(timezone.utc)
if with_project:
task.project = MagicMock()
task.project.id = project_id
task.project.name = "Test Project"
else:
task.project = None
return task
def _make_issue(issue_id="issue-001", task_id=TASK_ID, status="open"):
issue = MagicMock()
issue.id = issue_id
issue.task_id = task_id
issue.file_path = "src/main.py"
issue.line_number = 42
issue.column_number = None
issue.issue_type = "sql_injection"
issue.severity = "high"
issue.title = "SQL Injection"
issue.message = "Potential SQL injection"
issue.description = "Direct string interpolation in SQL query"
issue.suggestion = "Use parameterized queries"
issue.code_snippet = 'query = f"SELECT * FROM users WHERE id = {user_id}"'
issue.ai_explanation = None
issue.status = status
issue.resolved_by = None
issue.resolved_at = None
issue.created_at = datetime.now(timezone.utc)
return issue
def _db_with_project_ids(project_ids):
"""Build db mock that returns project IDs from the first query."""
db = AsyncMock()
# First call: select(Project.id).where(owner_id == ...)
proj_result = MagicMock()
proj_result.fetchall.return_value = [(pid,) for pid in project_ids]
return db
def _db_with_tasks(db, tasks):
"""Configure db.execute to return the given tasks on the second call."""
task_result = MagicMock()
scalars_mock = MagicMock()
scalars_mock.all.return_value = tasks
task_result.scalars.return_value = scalars_mock
return task_result
def _db_with_scalar_first(db, obj):
"""Configure db.execute to return a single object via scalars().first()."""
result = MagicMock()
scalars = MagicMock()
scalars.first.return_value = obj
result.scalars.return_value = scalars
return result
# ===================================================================
# list_tasks
# ===================================================================
class TestListTasks:
@pytest.mark.asyncio
async def test_list_tasks_returns_user_tasks(self):
"""Tasks belonging to the current user's projects are returned."""
db = _db_with_project_ids([PROJECT_ID])
task = _make_task()
task_result = _db_with_tasks(db, [task])
# First execute: project IDs; second: tasks
proj_result = MagicMock()
proj_result.fetchall.return_value = [(PROJECT_ID,)]
db.execute.side_effect = [proj_result, task_result]
user = _make_user()
result = await list_tasks(db=db, current_user=user)
assert len(result) == 1
assert result[0].id == TASK_ID
@pytest.mark.asyncio
async def test_list_tasks_with_project_filter(self):
"""When project_id is provided, results are filtered."""
db = _db_with_project_ids([PROJECT_ID, "proj-002"])
task1 = _make_task(task_id="t1", project_id=PROJECT_ID)
task2 = _make_task(task_id="t2", project_id="proj-002")
task_result = _db_with_tasks(db, [task1])
proj_result = MagicMock()
proj_result.fetchall.return_value = [(PROJECT_ID, "proj-002")]
db.execute.side_effect = [proj_result, task_result]
user = _make_user()
result = await list_tasks(project_id=PROJECT_ID, db=db, current_user=user)
# The function was called; we verify the filter was applied
assert len(result) == 1
@pytest.mark.asyncio
async def test_list_tasks_no_projects_returns_empty(self):
"""When user has no projects, return an empty list."""
db = AsyncMock()
proj_result = MagicMock()
proj_result.fetchall.return_value = [] # no projects
# No project IDs means the query uses .where(False), returning nothing
task_result = MagicMock()
scalars_mock = MagicMock()
scalars_mock.all.return_value = []
task_result.scalars.return_value = scalars_mock
db.execute.side_effect = [proj_result, task_result]
user = _make_user()
result = await list_tasks(db=db, current_user=user)
assert result == []
# ===================================================================
# read_task
# ===================================================================
class TestReadTask:
@pytest.mark.asyncio
async def test_read_task_success(self):
"""Returns task when it exists and belongs to current user."""
task = _make_task()
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, task)
user = _make_user()
result = await read_task(id=TASK_ID, db=db, current_user=user)
assert result.id == TASK_ID
@pytest.mark.asyncio
async def test_read_task_not_found(self):
"""Raises 404 when task does not exist."""
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, None)
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await read_task(id="nonexistent", db=db, current_user=user)
assert exc_info.value.status_code == 404
assert "任务不存在" in exc_info.value.detail
@pytest.mark.asyncio
async def test_read_task_wrong_user_forbidden(self):
"""Raises 403 when task belongs to another user."""
task = _make_task(created_by=OTHER_USER_ID)
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, task)
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await read_task(id=TASK_ID, db=db, current_user=user)
assert exc_info.value.status_code == 403
assert "无权" in exc_info.value.detail
# ===================================================================
# cancel_task
# ===================================================================
class TestCancelTask:
@pytest.mark.asyncio
async def test_cancel_pending_task_success(self):
"""Successfully cancel a pending task."""
task = _make_task(status="pending")
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, task)
user = _make_user()
with patch("app.api.v1.endpoints.tasks.task_control") as mock_tc:
result = await cancel_task(id=TASK_ID, db=db, current_user=user)
assert result["message"] == "任务已取消"
assert result["task_id"] == TASK_ID
assert task.status == "cancelled"
mock_tc.cancel_task.assert_called_once_with(TASK_ID)
@pytest.mark.asyncio
async def test_cancel_running_task_success(self):
"""Successfully cancel a running task."""
task = _make_task(status="running")
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, task)
user = _make_user()
with patch("app.api.v1.endpoints.tasks.task_control") as mock_tc:
result = await cancel_task(id=TASK_ID, db=db, current_user=user)
assert result["task_id"] == TASK_ID
@pytest.mark.asyncio
async def test_cancel_task_not_found(self):
"""Raises 404 when task does not exist."""
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, None)
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await cancel_task(id="nonexistent", db=db, current_user=user)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_cancel_task_wrong_user_forbidden(self):
"""Raises 403 when trying to cancel another user's task."""
task = _make_task(created_by=OTHER_USER_ID, status="pending")
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, task)
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await cancel_task(id=TASK_ID, db=db, current_user=user)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_cancel_completed_task_raises_400(self):
"""Raises 400 when trying to cancel an already completed task."""
task = _make_task(status="completed")
db = AsyncMock()
db.execute.return_value = _db_with_scalar_first(db, task)
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await cancel_task(id=TASK_ID, db=db, current_user=user)
assert exc_info.value.status_code == 400
assert "只能取消待处理或运行中的任务" in exc_info.value.detail
# ===================================================================
# read_task_issues
# ===================================================================
class TestReadTaskIssues:
@pytest.mark.asyncio
async def test_read_issues_success(self):
"""Returns issues for a task owned by the current user."""
task = _make_task(with_project=False)
issue1 = _make_issue(issue_id="i1")
issue2 = _make_issue(issue_id="i2")
db = AsyncMock()
# First execute: fetch task
task_result = _db_with_scalar_first(db, task)
# Second execute: fetch issues
issue_result = MagicMock()
issue_scalars = MagicMock()
issue_scalars.all.return_value = [issue1, issue2]
issue_result.scalars.return_value = issue_scalars
db.execute.side_effect = [task_result, issue_result]
user = _make_user()
result = await read_task_issues(id=TASK_ID, db=db, current_user=user)
assert len(result) == 2
@pytest.mark.asyncio
async def test_read_issues_task_not_found(self):
"""Raises 404 when task does not exist."""
db = AsyncMock()
task_result = _db_with_scalar_first(db, None)
db.execute.return_value = task_result
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await read_task_issues(id="nonexistent", db=db, current_user=user)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_read_issues_wrong_user_forbidden(self):
"""Raises 403 when task belongs to another user."""
task = _make_task(created_by=OTHER_USER_ID, with_project=False)
db = AsyncMock()
task_result = _db_with_scalar_first(db, task)
db.execute.return_value = task_result
user = _make_user()
with pytest.raises(HTTPException) as exc_info:
await read_task_issues(id=TASK_ID, db=db, current_user=user)
assert exc_info.value.status_code == 403
# ===================================================================
# update_issue
# ===================================================================
class TestUpdateIssue:
@pytest.mark.asyncio
async def test_resolve_issue_success(self):
"""Resolve an issue and set resolved_by/resolved_at."""
issue = _make_issue(status="open")
db = AsyncMock()
issue_result = MagicMock()
issue_scalars = MagicMock()
issue_scalars.first.return_value = issue
issue_result.scalars.return_value = issue_scalars
db.execute.return_value = issue_result
user = _make_user()
update = IssueUpdateSchema(status="resolved")
result = await update_issue(
task_id=TASK_ID, issue_id="issue-001",
issue_update=update, db=db, current_user=user
)
assert issue.status == "resolved"
assert issue.resolved_by == USER_ID
assert issue.resolved_at is not None
@pytest.mark.asyncio
async def test_update_issue_not_found(self):
"""Raises 404 when issue does not exist."""
db = AsyncMock()
issue_result = MagicMock()
issue_scalars = MagicMock()
issue_scalars.first.return_value = None
issue_result.scalars.return_value = issue_scalars
db.execute.return_value = issue_result
user = _make_user()
update = IssueUpdateSchema(status="resolved")
with pytest.raises(HTTPException) as exc_info:
await update_issue(
task_id=TASK_ID, issue_id="nonexistent",
issue_update=update, db=db, current_user=user
)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_update_issue_status_false_positive(self):
"""Update issue status to false_positive (not resolved, so no resolved_by)."""
issue = _make_issue(status="open")
db = AsyncMock()
issue_result = MagicMock()
issue_scalars = MagicMock()
issue_scalars.first.return_value = issue
issue_result.scalars.return_value = issue_scalars
db.execute.return_value = issue_result
user = _make_user()
update = IssueUpdateSchema(status="false_positive")
await update_issue(
task_id=TASK_ID, issue_id="issue-001",
issue_update=update, db=db, current_user=user
)
assert issue.status == "false_positive"
# resolved_by should NOT be set since status is not "resolved"
assert issue.resolved_by is None