第一次提交

This commit is contained in:
rjb
2026-01-19 00:09:36 +08:00
parent de4b5059e9
commit 6674060f2f
191 changed files with 40940 additions and 0 deletions

141
backend/tests/README.md Normal file
View File

@@ -0,0 +1,141 @@
# 单元测试说明
## 测试框架
本项目使用 `pytest` 作为测试框架,支持异步测试和数据库测试。
## 运行测试
### 运行所有测试
```bash
cd backend
pytest
```
### 运行特定测试文件
```bash
pytest tests/test_auth.py
```
### 运行特定测试类或函数
```bash
pytest tests/test_auth.py::TestAuth::test_register_user
```
### 运行带标记的测试
```bash
# 只运行单元测试
pytest -m unit
# 只运行工作流相关测试
pytest -m workflow
# 只运行认证相关测试
pytest -m auth
```
### 运行并显示覆盖率
```bash
pytest --cov=app --cov-report=html
```
## 测试标记
- `@pytest.mark.unit` - 单元测试
- `@pytest.mark.integration` - 集成测试
- `@pytest.mark.slow` - 慢速测试(需要网络或数据库)
- `@pytest.mark.api` - API测试
- `@pytest.mark.workflow` - 工作流测试
- `@pytest.mark.auth` - 认证测试
## 测试结构
```
tests/
├── __init__.py
├── conftest.py # 共享fixtures和配置
├── test_auth.py # 认证API测试
├── test_workflows.py # 工作流API测试
├── test_workflow_engine.py # 工作流引擎测试
└── test_workflow_validator.py # 工作流验证器测试
```
## Fixtures
### `db_session`
创建测试数据库会话,每个测试函数都会获得一个独立的会话。
### `client`
创建FastAPI测试客户端用于API测试。
### `authenticated_client`
创建已认证的测试客户端,自动注册用户并登录。
### `test_user_data`
提供测试用户数据。
### `sample_workflow_data`
提供示例工作流数据。
## 测试数据库
测试使用SQLite内存数据库每个测试函数都会
1. 创建所有表
2. 执行测试
3. 删除所有表
这样可以确保测试之间的隔离性。
## 编写新测试
### 示例API测试
```python
@pytest.mark.unit
@pytest.mark.api
class TestMyAPI:
def test_my_endpoint(self, authenticated_client):
response = authenticated_client.get("/api/v1/my-endpoint")
assert response.status_code == 200
```
### 示例:服务测试
```python
@pytest.mark.unit
class TestMyService:
@pytest.mark.asyncio
async def test_my_service_method(self):
service = MyService()
result = await service.my_method()
assert result is not None
```
## 注意事项
1. **测试隔离**:每个测试函数都应该独立,不依赖其他测试的执行顺序。
2. **数据库清理**:使用 `db_session` fixture 确保每个测试都有干净的数据库。
3. **异步测试**:使用 `@pytest.mark.asyncio` 标记异步测试函数。
4. **标记测试**:使用适当的标记(`@pytest.mark.unit` 等)来组织测试。
5. **测试数据**:使用 fixtures 提供测试数据,避免硬编码。
## CI/CD集成
在CI/CD流程中运行测试
```yaml
# .github/workflows/test.yml
- name: Run tests
run: |
cd backend
pytest --cov=app --cov-report=xml
```

View File

@@ -0,0 +1,3 @@
"""
测试包
"""

136
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,136 @@
"""
Pytest配置和共享fixtures
"""
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
from app.core.database import Base, get_db, SessionLocal
from app.main import app
from app.core.config import settings
import os
# 测试数据库URL使用SQLite内存数据库
TEST_DATABASE_URL = "sqlite:///:memory:"
# 创建测试数据库引擎
test_engine = create_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False}
)
# 创建测试会话工厂
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
@pytest.fixture(scope="function")
def db_session():
"""创建测试数据库会话"""
# 创建所有表
Base.metadata.create_all(bind=test_engine)
# 创建会话
session = TestingSessionLocal()
try:
yield session
finally:
session.close()
# 删除所有表
Base.metadata.drop_all(bind=test_engine)
@pytest.fixture(scope="function")
def client(db_session):
"""创建测试客户端"""
def override_get_db():
try:
yield db_session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
@pytest.fixture
def test_user_data():
"""测试用户数据"""
return {
"username": "testuser",
"email": "test@example.com",
"password": "testpassword123"
}
@pytest.fixture
def authenticated_client(client, test_user_data):
"""创建已认证的测试客户端"""
# 注册用户
response = client.post("/api/v1/auth/register", json=test_user_data)
assert response.status_code == 201
# 登录获取token
login_response = client.post(
"/api/v1/auth/login",
data={
"username": test_user_data["username"],
"password": test_user_data["password"]
}
)
assert login_response.status_code == 200
token = login_response.json()["access_token"]
# 设置认证头
client.headers.update({"Authorization": f"Bearer {token}"})
return client
@pytest.fixture
def sample_workflow_data():
"""示例工作流数据"""
return {
"name": "测试工作流",
"description": "这是一个测试工作流",
"nodes": [
{
"id": "start-1",
"type": "start",
"position": {"x": 0, "y": 0},
"data": {"label": "开始"}
},
{
"id": "llm-1",
"type": "llm",
"position": {"x": 200, "y": 0},
"data": {
"label": "LLM节点",
"provider": "deepseek",
"prompt": "请回答:{input}",
"model": "deepseek-chat"
}
},
{
"id": "end-1",
"type": "end",
"position": {"x": 400, "y": 0},
"data": {"label": "结束"}
}
],
"edges": [
{
"id": "e1",
"source": "start-1",
"target": "llm-1"
},
{
"id": "e2",
"source": "llm-1",
"target": "end-1"
}
]
}

100
backend/tests/test_auth.py Normal file
View File

@@ -0,0 +1,100 @@
"""
用户认证API测试
"""
import pytest
from fastapi import status
@pytest.mark.unit
@pytest.mark.auth
class TestAuth:
"""认证相关测试"""
def test_register_user(self, client, test_user_data):
"""测试用户注册"""
response = client.post("/api/v1/auth/register", json=test_user_data)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert "id" in data
assert data["username"] == test_user_data["username"]
assert data["email"] == test_user_data["email"]
assert "password_hash" not in data # 密码哈希不应该返回
def test_register_duplicate_username(self, client, test_user_data):
"""测试重复用户名注册"""
# 第一次注册
response1 = client.post("/api/v1/auth/register", json=test_user_data)
assert response1.status_code == status.HTTP_201_CREATED
# 第二次注册相同用户名
response2 = client.post("/api/v1/auth/register", json=test_user_data)
assert response2.status_code == status.HTTP_400_BAD_REQUEST
def test_register_duplicate_email(self, client, test_user_data):
"""测试重复邮箱注册"""
# 第一次注册
response1 = client.post("/api/v1/auth/register", json=test_user_data)
assert response1.status_code == status.HTTP_201_CREATED
# 使用相同邮箱但不同用户名
duplicate_data = test_user_data.copy()
duplicate_data["username"] = "another_user"
response2 = client.post("/api/v1/auth/register", json=duplicate_data)
assert response2.status_code == status.HTTP_400_BAD_REQUEST
def test_login_success(self, client, test_user_data):
"""测试登录成功"""
# 先注册
client.post("/api/v1/auth/register", json=test_user_data)
# 登录
response = client.post(
"/api/v1/auth/login",
data={
"username": test_user_data["username"],
"password": test_user_data["password"]
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
def test_login_wrong_password(self, client, test_user_data):
"""测试错误密码登录"""
# 先注册
client.post("/api/v1/auth/register", json=test_user_data)
# 使用错误密码登录
response = client.post(
"/api/v1/auth/login",
data={
"username": test_user_data["username"],
"password": "wrongpassword"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_login_nonexistent_user(self, client):
"""测试不存在的用户登录"""
response = client.post(
"/api/v1/auth/login",
data={
"username": "nonexistent",
"password": "password123"
}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_current_user(self, authenticated_client, test_user_data):
"""测试获取当前用户信息"""
response = authenticated_client.get("/api/v1/auth/me")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["username"] == test_user_data["username"]
assert data["email"] == test_user_data["email"]
def test_get_current_user_unauthorized(self, client):
"""测试未授权访问当前用户信息"""
response = client.get("/api/v1/auth/me")
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View File

@@ -0,0 +1,152 @@
"""
工作流执行引擎测试
"""
import pytest
from app.services.workflow_engine import WorkflowEngine
@pytest.mark.unit
@pytest.mark.workflow
class TestWorkflowEngine:
"""工作流引擎测试"""
def test_build_execution_graph(self):
"""测试构建执行图"""
workflow_data = {
"nodes": [
{"id": "start-1", "type": "start"},
{"id": "llm-1", "type": "llm"},
{"id": "end-1", "type": "end"}
],
"edges": [
{"id": "e1", "source": "start-1", "target": "llm-1"},
{"id": "e2", "source": "llm-1", "target": "end-1"}
]
}
engine = WorkflowEngine("test-workflow", workflow_data)
execution_order = engine.build_execution_graph()
assert "start-1" in execution_order
assert "llm-1" in execution_order
assert "end-1" in execution_order
assert execution_order.index("start-1") < execution_order.index("llm-1")
assert execution_order.index("llm-1") < execution_order.index("end-1")
def test_get_node_input(self):
"""测试获取节点输入"""
workflow_data = {
"nodes": [
{"id": "start-1", "type": "start"},
{"id": "llm-1", "type": "llm"}
],
"edges": [
{"id": "e1", "source": "start-1", "target": "llm-1"}
]
}
engine = WorkflowEngine("test-workflow", workflow_data)
engine.node_outputs = {
"start-1": {"input": "test data"}
}
input_data = engine.get_node_input("llm-1", engine.node_outputs)
assert "input" in input_data
assert input_data["input"] == "test data"
@pytest.mark.asyncio
async def test_execute_start_node(self):
"""测试执行开始节点"""
workflow_data = {
"nodes": [
{"id": "start-1", "type": "start", "data": {"label": "开始"}}
],
"edges": []
}
engine = WorkflowEngine("test-workflow", workflow_data)
node = workflow_data["nodes"][0]
input_data = {"test": "data"}
result = await engine.execute_node(node, input_data)
assert result["status"] == "success"
assert result["output"] == input_data
@pytest.mark.asyncio
async def test_execute_end_node(self):
"""测试执行结束节点"""
workflow_data = {
"nodes": [
{"id": "end-1", "type": "end", "data": {"label": "结束"}}
],
"edges": []
}
engine = WorkflowEngine("test-workflow", workflow_data)
node = workflow_data["nodes"][0]
input_data = {"result": "final output"}
result = await engine.execute_node(node, input_data)
assert result["status"] == "success"
assert result["output"] == input_data
@pytest.mark.asyncio
async def test_execute_condition_node(self):
"""测试执行条件节点"""
workflow_data = {
"nodes": [
{
"id": "condition-1",
"type": "condition",
"data": {
"label": "条件判断",
"expression": "{value} > 10"
}
}
],
"edges": []
}
engine = WorkflowEngine("test-workflow", workflow_data)
node = workflow_data["nodes"][0]
# 测试条件为真
input_data = {"value": 15}
result = await engine.execute_node(node, input_data)
assert result["status"] == "success"
assert result["branch"] == "true"
# 测试条件为假
input_data = {"value": 5}
result = await engine.execute_node(node, input_data)
assert result["status"] == "success"
assert result["branch"] == "false"
@pytest.mark.asyncio
async def test_execute_transform_node(self):
"""测试执行转换节点"""
workflow_data = {
"nodes": [
{
"id": "transform-1",
"type": "transform",
"data": {
"label": "数据转换",
"mode": "mapping",
"mapping": {
"new_field": "{old_field}"
}
}
}
],
"edges": []
}
engine = WorkflowEngine("test-workflow", workflow_data)
node = workflow_data["nodes"][0]
input_data = {"old_field": "test value"}
result = await engine.execute_node(node, input_data)
assert result["status"] == "success"
assert "new_field" in result["output"]
assert result["output"]["new_field"] == "test value"

View File

@@ -0,0 +1,115 @@
"""
工作流验证器测试
"""
import pytest
from app.services.workflow_validator import WorkflowValidator
@pytest.mark.unit
@pytest.mark.workflow
class TestWorkflowValidator:
"""工作流验证器测试"""
def test_validate_valid_workflow(self):
"""测试验证有效工作流"""
nodes = [
{"id": "start-1", "type": "start"},
{"id": "llm-1", "type": "llm"},
{"id": "end-1", "type": "end"}
]
edges = [
{"id": "e1", "source": "start-1", "target": "llm-1"},
{"id": "e2", "source": "llm-1", "target": "end-1"}
]
validator = WorkflowValidator(nodes, edges)
is_valid, errors, warnings = validator.validate()
assert is_valid is True
assert len(errors) == 0
def test_validate_no_start_node(self):
"""测试验证缺少开始节点"""
nodes = [
{"id": "llm-1", "type": "llm"},
{"id": "end-1", "type": "end"}
]
edges = [
{"id": "e1", "source": "llm-1", "target": "end-1"}
]
validator = WorkflowValidator(nodes, edges)
is_valid, errors, warnings = validator.validate()
assert is_valid is False
assert any("开始节点" in error for error in errors)
def test_validate_duplicate_node_id(self):
"""测试验证重复节点ID"""
nodes = [
{"id": "start-1", "type": "start"},
{"id": "start-1", "type": "llm"}, # 重复ID
{"id": "end-1", "type": "end"}
]
edges = []
validator = WorkflowValidator(nodes, edges)
is_valid, errors, warnings = validator.validate()
assert is_valid is False
assert any("重复" in error for error in errors)
def test_validate_cycle_detection(self):
"""测试循环检测"""
nodes = [
{"id": "start-1", "type": "start"},
{"id": "node-1", "type": "llm"},
{"id": "node-2", "type": "llm"}
]
edges = [
{"id": "e1", "source": "start-1", "target": "node-1"},
{"id": "e2", "source": "node-1", "target": "node-2"},
{"id": "e3", "source": "node-2", "target": "node-1"} # 形成循环
]
validator = WorkflowValidator(nodes, edges)
is_valid, errors, warnings = validator.validate()
assert is_valid is False
assert any("循环" in error for error in errors)
def test_validate_invalid_edge(self):
"""测试验证无效边"""
nodes = [
{"id": "start-1", "type": "start"},
{"id": "end-1", "type": "end"}
]
edges = [
{"id": "e1", "source": "nonexistent", "target": "end-1"} # 源节点不存在
]
validator = WorkflowValidator(nodes, edges)
is_valid, errors, warnings = validator.validate()
assert is_valid is False
assert any("不存在" in error for error in errors)
def test_validate_condition_branches(self):
"""测试验证条件节点分支"""
nodes = [
{"id": "start-1", "type": "start"},
{"id": "condition-1", "type": "condition"},
{"id": "end-1", "type": "end"}
]
edges = [
{"id": "e1", "source": "start-1", "target": "condition-1"},
{"id": "e2", "source": "condition-1", "target": "end-1", "sourceHandle": "true"}
# 缺少false分支
]
validator = WorkflowValidator(nodes, edges)
is_valid, errors, warnings = validator.validate()
# 缺少分支是警告,不是错误
assert is_valid is True
assert any("False分支" in warning for warning in warnings)

View File

@@ -0,0 +1,108 @@
"""
工作流API测试
"""
import pytest
from fastapi import status
@pytest.mark.unit
@pytest.mark.workflow
class TestWorkflows:
"""工作流相关测试"""
def test_create_workflow(self, authenticated_client, sample_workflow_data):
"""测试创建工作流"""
response = authenticated_client.post(
"/api/v1/workflows",
json=sample_workflow_data
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["name"] == sample_workflow_data["name"]
assert data["description"] == sample_workflow_data["description"]
assert "id" in data
assert "nodes" in data
assert "edges" in data
def test_get_workflow_list(self, authenticated_client, sample_workflow_data):
"""测试获取工作流列表"""
# 创建几个工作流
for i in range(3):
workflow_data = sample_workflow_data.copy()
workflow_data["name"] = f"工作流{i+1}"
authenticated_client.post("/api/v1/workflows", json=workflow_data)
# 获取列表
response = authenticated_client.get("/api/v1/workflows")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert isinstance(data, list)
assert len(data) >= 3
def test_get_workflow_detail(self, authenticated_client, sample_workflow_data):
"""测试获取工作流详情"""
# 创建工作流
create_response = authenticated_client.post(
"/api/v1/workflows",
json=sample_workflow_data
)
workflow_id = create_response.json()["id"]
# 获取详情
response = authenticated_client.get(f"/api/v1/workflows/{workflow_id}")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == workflow_id
assert data["name"] == sample_workflow_data["name"]
assert len(data["nodes"]) == len(sample_workflow_data["nodes"])
assert len(data["edges"]) == len(sample_workflow_data["edges"])
def test_update_workflow(self, authenticated_client, sample_workflow_data):
"""测试更新工作流"""
# 创建工作流
create_response = authenticated_client.post(
"/api/v1/workflows",
json=sample_workflow_data
)
workflow_id = create_response.json()["id"]
# 更新工作流
update_data = sample_workflow_data.copy()
update_data["name"] = "更新后的工作流"
update_data["description"] = "更新后的描述"
response = authenticated_client.put(
f"/api/v1/workflows/{workflow_id}",
json=update_data
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "更新后的工作流"
assert data["description"] == "更新后的描述"
def test_delete_workflow(self, authenticated_client, sample_workflow_data):
"""测试删除工作流"""
# 创建工作流
create_response = authenticated_client.post(
"/api/v1/workflows",
json=sample_workflow_data
)
workflow_id = create_response.json()["id"]
# 删除工作流
response = authenticated_client.delete(f"/api/v1/workflows/{workflow_id}")
assert response.status_code == status.HTTP_200_OK
# 验证已删除
get_response = authenticated_client.get(f"/api/v1/workflows/{workflow_id}")
assert get_response.status_code == status.HTTP_404_NOT_FOUND
def test_get_workflow_not_found(self, authenticated_client):
"""测试获取不存在的工作流"""
response = authenticated_client.get("/api/v1/workflows/nonexistent-id")
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_create_workflow_unauthorized(self, client, sample_workflow_data):
"""测试未授权创建工作流"""
response = client.post("/api/v1/workflows", json=sample_workflow_data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED