mirror of
https://github.com/lintsinghua/DeepAudit.git
synced 2026-10-25 18:11:29 +08:00
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>
436 lines
14 KiB
Python
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
|