You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cbmc/codedetect/tests/unit/test_ui_api.py

932 lines
32 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
UI API模块单元测试
本模块为UI API模块提供全面的单元测试覆盖包括
- REST API端点测试
- WebSocket事件处理测试
- 工作流编排测试
- 作业管理测试
- 文件上传测试
- 错误处理测试
- 认证授权测试
"""
import pytest
import asyncio
import json
import tempfile
import os
from pathlib import Path
from unittest.mock import Mock, patch, AsyncMock, MagicMock
from flask import Flask
from flask_socketio import SocketIO
from typing import Dict, List, Any, Optional
from src.ui.api import (
FileUploadAPI, CodeParseAPI, SpecGenerationAPI, MutationAPI, VerificationAPI,
WorkflowAPI, JobListAPI, JobDetailAPI, WorkflowListAPI, SystemStatusAPI
)
from src.ui.websocket import WebSocketHandler, WebSocketManager, EventType
from src.ui.workflow import WorkflowManager, WorkflowConfig, WorkflowMode
from src.ui.job_manager import JobManager, JobStatus, JobType
from src.ui.exceptions import (
ValidationError, AuthenticationError, AuthorizationError,
ResourceNotFoundError, WorkflowError, JobError, TimeoutError,
ConcurrencyError, ConfigurationError, ExternalServiceError, APIError
)
from src.utils.config import ConfigManager
class TestFileUploadAPI:
"""FileUploadAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
self.app.config['SECRET_KEY'] = 'test-secret'
self.app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()
self.app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
self.app.config['ALLOWED_EXTENSIONS'] = {'.c', '.cpp', '.h', '.hpp'}
with self.app.test_request_context():
self.api = FileUploadAPI()
def test_post_with_valid_file(self):
"""测试上传有效文件"""
# 创建临时文件
with tempfile.NamedTemporaryFile(suffix='.c', delete=False) as f:
f.write(b'int test() { return 0; }')
f.flush()
# 模拟文件上传
with open(f.name, 'rb') as upload_file:
from werkzeug.datastructures import FileStorage
file_storage = FileStorage(
stream=upload_file,
filename='test.c',
content_type='text/plain'
)
with self.app.test_request_context(method='POST', data={'file': file_storage}):
response = self.api.post()
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'job_id' in data['data']
os.unlink(f.name)
def test_post_with_pasted_code(self):
"""测试粘贴代码上传"""
code_content = 'int pasted_function() { return 42; }'
with self.app.test_request_context(
method='POST',
data={'code_content': code_content}
):
response = self.api.post()
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'job_id' in data['data']
def test_post_no_file_or_code(self):
"""测试无文件或代码上传"""
with self.app.test_request_context(method='POST'):
response = self.api.post()
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert 'error' in data
def test_post_invalid_file_extension(self):
"""测试无效文件扩展名"""
with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as f:
f.write(b'invalid content')
f.flush()
with open(f.name, 'rb') as upload_file:
from werkzeug.datastructures import FileStorage
file_storage = FileStorage(
stream=upload_file,
filename='test.txt',
content_type='text/plain'
)
with self.app.test_request_context(method='POST', data={'file': file_storage}):
response = self.api.post()
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
os.unlink(f.name)
def test_post_file_too_large(self):
"""测试文件过大"""
# 创建一个超过限制的大文件
large_content = b'0' * (17 * 1024 * 1024) # 17MB
with tempfile.NamedTemporaryFile(suffix='.c', delete=False) as f:
f.write(large_content)
f.flush()
try:
with open(f.name, 'rb') as upload_file:
from werkzeug.datastructures import FileStorage
file_storage = FileStorage(
stream=upload_file,
filename='large.c',
content_type='text/plain'
)
with self.app.test_request_context(method='POST', data={'file': file_storage}):
response = self.api.post()
assert response.status_code == 413
finally:
os.unlink(f.name)
class TestCodeParseAPI:
"""CodeParseAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = CodeParseAPI()
def test_post_with_valid_data(self):
"""测试使用有效数据解析"""
data = {
'file_path': '/tmp/test.c',
'options': {
'include_functions': True,
'include_variables': True,
'complexity_analysis': True
}
}
with patch('src.ui.api.JobManager') as mock_job_manager:
mock_job = Mock()
mock_job.job_id = 'test-job-id'
mock_job_manager.return_value.create_job.return_value = mock_job
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert 'job_id' in response_data['data']
def test_post_missing_file_path(self):
"""测试缺少文件路径"""
data = {'options': {'include_functions': True}}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
assert 'file_path' in response_data['error']
def test_post_invalid_options(self):
"""测试无效选项"""
data = {
'file_path': '/tmp/test.c',
'options': 'invalid_options' # 应该是字典
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
class TestSpecGenerationAPI:
"""SpecGenerationAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = SpecGenerationAPI()
def test_post_with_valid_metadata(self):
"""测试使用有效元数据生成规范"""
data = {
'functions': [
{
'name': 'test_function',
'return_type': 'int',
'parameters': [
{'name': 'x', 'type': 'int'},
{'name': 'y', 'type': 'int'}
],
'complexity_score': 0.5
}
],
'options': {
'verification_types': ['memory_safety', 'overflow_detection'],
'include_comments': True,
'style_guide': 'cbmc'
}
}
with patch('src.ui.api.JobManager') as mock_job_manager:
mock_job = Mock()
mock_job.job_id = 'spec-job-id'
mock_job_manager.return_value.create_job.return_value = mock_job
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert 'job_id' in response_data['data']
def test_post_empty_functions_list(self):
"""测试空函数列表"""
data = {
'functions': [],
'options': {'verification_types': ['memory_safety']}
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
assert 'functions' in response_data['error']
def test_post_invalid_function_metadata(self):
"""测试无效函数元数据"""
data = {
'functions': [
{
'name': '', # 空名称
'return_type': 'int'
# 缺少必要字段
}
]
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
class TestMutationAPI:
"""MutationAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = MutationAPI()
def test_post_with_valid_specification(self):
"""测试使用有效规范进行突变"""
data = {
'specification': 'void test(int x) { __CPROVER_assume(x > 0); }',
'function_name': 'test',
'options': {
'mutation_types': ['predicate', 'boundary'],
'max_mutations': 10,
'quality_threshold': 0.7
}
}
with patch('src.ui.api.JobManager') as mock_job_manager:
mock_job = Mock()
mock_job.job_id = 'mutation-job-id'
mock_job_manager.return_value.create_job.return_value = mock_job
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert 'job_id' in response_data['data']
def test_post_missing_specification(self):
"""测试缺少规范"""
data = {
'function_name': 'test',
'options': {'mutation_types': ['predicate']}
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
assert 'specification' in response_data['error']
def test_post_invalid_specification_syntax(self):
"""测试无效规范语法"""
data = {
'specification': 'invalid syntax here {',
'function_name': 'test'
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
class TestVerificationAPI:
"""VerificationAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = VerificationAPI()
def test_post_with_valid_specification(self):
"""测试使用有效规范进行验证"""
data = {
'specification': 'void test(int x) { __CPROVER_assert(x > 0, "positive"); }',
'function_name': 'test',
'options': {
'verification_types': ['memory_safety'],
'timeout': 300,
'depth': 20
}
}
with patch('src.ui.api.JobManager') as mock_job_manager:
mock_job = Mock()
mock_job.job_id = 'verification-job-id'
mock_job_manager.return_value.create_job.return_value = mock_job
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert 'job_id' in response_data['data']
def test_post_missing_specification(self):
"""测试缺少规范"""
data = {
'function_name': 'test',
'options': {'verification_types': ['memory_safety']}
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
assert 'specification' in response_data['error']
def test_post_invalid_verification_options(self):
"""测试无效验证选项"""
data = {
'specification': 'void test(int x) { }',
'function_name': 'test',
'options': {
'timeout': -1 # 无效超时
}
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
class TestWorkflowAPI:
"""WorkflowAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = WorkflowAPI()
def test_post_start_workflow(self):
"""测试启动工作流"""
data = {
'source_files': ['/tmp/test.c'],
'target_functions': ['test_function'],
'workflow_config': {
'mode': 'standard',
'enable_parsing': True,
'enable_generation': True,
'enable_mutation': True,
'enable_verification': True
}
}
with patch('src.ui.api._workflow_manager') as mock_workflow_manager:
mock_workflow_manager.start_workflow.return_value = 'workflow-123'
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert 'workflow_id' in response_data
assert response_data['workflow_id'] == 'workflow-123'
def test_post_missing_source_files(self):
"""测试缺少源文件"""
data = {
'target_functions': ['test_function'],
'workflow_config': {'mode': 'standard'}
}
with self.app.test_request_context(json=data):
response = self.api.post()
assert response.status_code == 400
response_data = response.get_json()
assert response_data['success'] is False
assert 'source_files' in response_data['error']
def test_get_workflow_status(self):
"""测试获取工作流状态"""
workflow_id = 'workflow-123'
with patch('src.ui.api._workflow_manager') as mock_workflow_manager:
mock_status = {
'workflow_id': workflow_id,
'status': 'running',
'progress': 50.0,
'current_step': 'mutation'
}
mock_workflow_manager.get_workflow_status.return_value = mock_status
with self.app.test_request_context():
response = self.api.get(workflow_id)
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert response_data['workflow_id'] == workflow_id
assert response_data['status'] == 'running'
def test_delete_workflow(self):
"""测试删除工作流"""
workflow_id = 'workflow-123'
with patch('src.ui.api._workflow_manager') as mock_workflow_manager:
mock_workflow_manager.cancel_workflow.return_value = True
with self.app.test_request_context():
response = self.api.delete(workflow_id)
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert response_data['message'] == 'Workflow cancelled successfully'
class TestJobListAPI:
"""JobListAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = JobListAPI()
def test_get_job_list(self):
"""测试获取作业列表"""
with patch('src.ui.api._job_manager') as mock_job_manager:
mock_jobs = [
Mock(job_id='job-1', job_type=JobType.CODE_PARSING, status=JobStatus.COMPLETED),
Mock(job_id='job-2', job_type=JobType.SPEC_GENERATION, status=JobStatus.RUNNING)
]
mock_job_manager.get_jobs.return_value = mock_jobs
with self.app.test_request_context():
response = self.api.get()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert len(response_data['jobs']) == 2
def test_get_job_list_with_filters(self):
"""测试带过滤器的作业列表"""
with patch('src.ui.api._job_manager') as mock_job_manager:
mock_jobs = [Mock(job_id='job-1', job_type=JobType.CODE_PARSING, status=JobStatus.COMPLETED)]
mock_job_manager.get_jobs.return_value = mock_jobs
with self.app.test_request_context(query_string='status=completed&type=code_parsing'):
response = self.api.get()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert len(response_data['jobs']) == 1
class TestJobDetailAPI:
"""JobDetailAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = JobDetailAPI()
def test_get_job_details(self):
"""测试获取作业详情"""
job_id = 'job-123'
with patch('src.ui.api._job_manager') as mock_job_manager:
mock_job = Mock(
job_id=job_id,
job_type=JobType.CODE_PARSING,
status=JobStatus.COMPLETED,
progress=Mock(percentage=100.0, message='Completed'),
created_at='2023-01-01T00:00:00',
completed_at='2023-01-01T00:05:00'
)
mock_job_manager.get_job.return_value = mock_job
with self.app.test_request_context():
response = self.api.get(job_id)
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert response_data['job']['job_id'] == job_id
assert response_data['job']['status'] == 'completed'
def test_get_nonexistent_job(self):
"""测试获取不存在的作业"""
job_id = 'nonexistent-job'
with patch('src.ui.api._job_manager') as mock_job_manager:
mock_job_manager.get_job.return_value = None
with self.app.test_request_context():
response = self.api.get(job_id)
assert response.status_code == 404
response_data = response.get_json()
assert response_data['success'] is False
assert 'not found' in response_data['error'].lower()
def test_delete_job(self):
"""测试删除作业"""
job_id = 'job-123'
with patch('src.ui.api._job_manager') as mock_job_manager:
mock_job_manager.cancel_job.return_value = True
with self.app.test_request_context():
response = self.api.delete(job_id)
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
class TestSystemStatusAPI:
"""SystemStatusAPI类测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
self.api = SystemStatusAPI()
def test_get_system_status(self):
"""测试获取系统状态"""
with patch('src.ui.api._job_manager') as mock_job_manager:
mock_job_manager.get_statistics.return_value = {
'total_jobs': 10,
'active_jobs': 2,
'completed_jobs': 8
}
with patch('src.ui.api._workflow_manager') as mock_workflow_manager:
mock_workflow_manager.get_statistics.return_value = {
'total_workflows': 5,
'active_workflows': 1
}
with self.app.test_request_context():
response = self.api.get()
assert response.status_code == 200
response_data = response.get_json()
assert response_data['success'] is True
assert 'status' in response_data
assert 'version' in response_data
assert 'statistics' in response_data
class TestWebSocketIntegration:
"""WebSocket集成测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
self.socketio = SocketIO(self.app)
self.websocket_handler = WebSocketHandler(self.socketio, Mock())
def test_websocket_event_broadcast(self):
"""测试WebSocket事件广播"""
with patch.object(self.websocket_handler, 'send_job_progress') as mock_send:
self.websocket_handler.send_job_progress('job-123', {
'percentage': 50.0,
'message': 'Processing...'
})
mock_send.assert_called_once()
def test_websocket_workflow_update(self):
"""测试WebSocket工作流更新"""
with patch.object(self.websocket_handler, 'send_workflow_update') as mock_send:
self.websocket_handler.send_workflow_update('session-123', {
'workflow_id': 'workflow-123',
'status': 'completed'
})
mock_send.assert_called_once()
def test_websocket_error_notification(self):
"""测试WebSocket错误通知"""
with patch.object(self.websocket_handler, 'send_error_notification') as mock_send:
self.websocket_handler.send_error_notification(
'Test error message',
'ValidationError',
{'field': 'test_field'}
)
mock_send.assert_called_once()
class TestAPIErrorHandling:
"""API错误处理测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
with self.app.test_request_context():
pass
def test_validation_error_response(self):
"""测试验证错误响应"""
error = ValidationError("Invalid input", field="test_field")
response = {
"success": False,
"error": str(error),
"error_type": "ValidationError",
"field": "test_field"
}
assert response["success"] is False
assert "Invalid input" in response["error"]
assert response["error_type"] == "ValidationError"
def test_authentication_error_response(self):
"""测试认证错误响应"""
error = AuthenticationError("Authentication required")
response = {
"success": False,
"error": str(error),
"error_type": "AuthenticationError"
}
assert response["success"] is False
assert "Authentication required" in response["error"]
def test_resource_not_found_error_response(self):
"""测试资源未找到错误响应"""
error = ResourceNotFoundError("Resource not found", resource_id="123")
response = {
"success": False,
"error": str(error),
"error_type": "ResourceNotFoundError",
"resource_id": "123"
}
assert response["success"] is False
assert "Resource not found" in response["error"]
def test_job_error_response(self):
"""测试作业错误响应"""
error = JobError("Job failed", job_id="job-123")
response = {
"success": False,
"error": str(error),
"error_type": "JobError",
"job_id": "job-123"
}
assert response["success"] is False
assert "Job failed" in response["error"]
def test_workflow_error_response(self):
"""测试工作流错误响应"""
error = WorkflowError("Workflow error", workflow_step="mutation")
response = {
"success": False,
"error": str(error),
"error_type": "WorkflowError",
"workflow_step": "mutation"
}
assert response["success"] is False
assert "Workflow error" in response["error"]
def test_timeout_error_response(self):
"""测试超时错误响应"""
error = TimeoutError("Operation timed out", operation="verification", timeout_seconds=300)
response = {
"success": False,
"error": str(error),
"error_type": "TimeoutError",
"operation": "verification",
"timeout_seconds": 300
}
assert response["success"] is False
assert "Operation timed out" in response["error"]
def test_concurrency_error_response(self):
"""测试并发错误响应"""
error = ConcurrencyError("Too many concurrent operations")
response = {
"success": False,
"error": str(error),
"error_type": "ConcurrencyError"
}
assert response["success"] is False
assert "Too many concurrent operations" in response["error"]
def test_configuration_error_response(self):
"""测试配置错误响应"""
error = ConfigurationError("Invalid configuration")
response = {
"success": False,
"error": str(error),
"error_type": "ConfigurationError"
}
assert response["success"] is False
assert "Invalid configuration" in response["error"]
def test_external_service_error_response(self):
"""测试外部服务错误响应"""
error = ExternalServiceError("Service unavailable", service="llm_provider")
response = {
"success": False,
"error": str(error),
"error_type": "ExternalServiceError",
"service": "llm_provider"
}
assert response["success"] is False
assert "Service unavailable" in response["error"]
class TestAPIIntegration:
"""API集成测试"""
def setup_method(self):
"""测试方法设置"""
self.app = Flask(__name__)
self.app.config['TESTING'] = True
self.app.config['SECRET_KEY'] = 'test-secret'
# 模拟依赖项
self.mock_job_manager = Mock()
self.mock_workflow_manager = Mock()
self.mock_websocket_handler = Mock()
# 设置应用上下文
with self.app.app_context():
from src.ui.api import init_api
init_api(
config=Mock(),
job_manager=self.mock_job_manager,
workflow_manager=self.mock_workflow_manager,
websocket_handler=self.mock_websocket_handler
)
def test_complete_workflow_integration(self):
"""测试完整工作流集成"""
# 设置模拟响应
self.mock_workflow_manager.start_workflow.return_value = 'workflow-123'
self.mock_job_manager.create_job.return_value = Mock(job_id='job-123')
with self.app.test_client() as client:
# 测试文件上传
with tempfile.NamedTemporaryFile(suffix='.c', delete=False) as f:
f.write(b'int test() { return 0; }')
f.flush()
try:
with open(f.name, 'rb') as upload_file:
response = client.post('/api/upload', data={'file': upload_file})
assert response.status_code == 200
# 测试工作流启动
workflow_data = {
'source_files': [f.name],
'target_functions': ['test'],
'workflow_config': {'mode': 'standard'}
}
response = client.post('/api/workflow', json=workflow_data)
assert response.status_code == 200
# 测试作业列表
response = client.get('/api/jobs')
assert response.status_code == 200
# 测试系统状态
response = client.get('/api/status')
assert response.status_code == 200
finally:
os.unlink(f.name)
def test_error_propagation(self):
"""测试错误传播"""
# 设置模拟错误
self.mock_job_manager.create_job.side_effect = JobError("Database error")
with self.app.test_client() as client:
with tempfile.NamedTemporaryFile(suffix='.c', delete=False) as f:
f.write(b'int test() { return 0; }')
f.flush()
try:
with open(f.name, 'rb') as upload_file:
response = client.post('/api/upload', data={'file': upload_file})
assert response.status_code == 500
data = response.get_json()
assert data['success'] is False
assert 'Database error' in data['error']
finally:
os.unlink(f.name)
def test_request_validation(self):
"""测试请求验证"""
with self.app.test_client() as client:
# 测试无效JSON
response = client.post('/api/workflow', data='invalid json', content_type='application/json')
assert response.status_code == 400
# 测试缺少必需字段
response = client.post('/api/workflow', json={})
assert response.status_code == 400
if __name__ == "__main__":
pytest.main([__file__, "-v"])