style: normalize python formatting gates

This commit is contained in:
Xiaoxia AI
2026-06-21 06:52:19 +08:00
parent 0809a079c5
commit bfbaddbd9a
129 changed files with 3024 additions and 2485 deletions
+108 -72
View File
@@ -1,6 +1,7 @@
"""
API 集成测试
"""
import pytest
from fastapi.testclient import TestClient
@@ -11,130 +12,165 @@ client = TestClient(app)
class TestAuthAPI:
"""认证 API 集成测试"""
def test_register_success(self):
"""测试注册成功"""
response = client.post("/api/v1/auth/register", json={
"email": "test@example.com",
"password": "SecurePass123",
"username": "testuser",
"display_name": "Test User",
})
response = client.post(
"/api/v1/auth/register",
json={
"email": "test@example.com",
"password": "SecurePass123",
"username": "testuser",
"display_name": "Test User",
},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "test@example.com"
assert data["username"] == "testuser"
assert "user_id" in data
def test_register_duplicate_email(self):
"""测试重复邮箱注册"""
# 先注册一个用户
client.post("/api/v1/auth/register", json={
"email": "duplicate@example.com",
"password": "SecurePass123",
"username": "user1",
"display_name": "User 1",
})
client.post(
"/api/v1/auth/register",
json={
"email": "duplicate@example.com",
"password": "SecurePass123",
"username": "user1",
"display_name": "User 1",
},
)
# 尝试用相同邮箱再次注册
response = client.post("/api/v1/auth/register", json={
"email": "duplicate@example.com",
"password": "SecurePass123",
"username": "user2",
"display_name": "User 2",
})
response = client.post(
"/api/v1/auth/register",
json={
"email": "duplicate@example.com",
"password": "SecurePass123",
"username": "user2",
"display_name": "User 2",
},
)
assert response.status_code == 400
assert "already registered" in response.json()["detail"].lower()
def test_login_success(self):
"""测试登录成功"""
# 先注册
client.post("/api/v1/auth/register", json={
"email": "login@example.com",
"password": "SecurePass123",
"username": "loginuser",
"display_name": "Login User",
})
client.post(
"/api/v1/auth/register",
json={
"email": "login@example.com",
"password": "SecurePass123",
"username": "loginuser",
"display_name": "Login User",
},
)
# 登录
response = client.post("/api/v1/auth/login", json={
"email": "login@example.com",
"password": "SecurePass123",
})
response = client.post(
"/api/v1/auth/login",
json={
"email": "login@example.com",
"password": "SecurePass123",
},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
assert data["token_type"] == "bearer"
def test_login_wrong_password(self):
"""测试密码错误"""
response = client.post("/api/v1/auth/login", json={
"email": "login@example.com",
"password": "WrongPassword123",
})
response = client.post(
"/api/v1/auth/login",
json={
"email": "login@example.com",
"password": "WrongPassword123",
},
)
assert response.status_code == 401
class TestWorkspaceAPI:
"""工作空间 API 集成测试"""
def setup_method(self):
"""每个测试前的准备"""
# 注册并登录,获取 token
client.post("/api/v1/auth/register", json={
"email": "workspace@example.com",
"password": "SecurePass123",
"username": "workspaceuser",
"display_name": "Workspace User",
})
response = client.post("/api/v1/auth/login", json={
"email": "workspace@example.com",
"password": "SecurePass123",
})
client.post(
"/api/v1/auth/register",
json={
"email": "workspace@example.com",
"password": "SecurePass123",
"username": "workspaceuser",
"display_name": "Workspace User",
},
)
response = client.post(
"/api/v1/auth/login",
json={
"email": "workspace@example.com",
"password": "SecurePass123",
},
)
self.token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {self.token}"}
def test_create_workspace(self):
"""测试创建工作空间"""
response = client.post("/api/v1/workspaces", json={
"name": "My Workspace",
"subscription_plan": "free",
}, headers=self.headers)
response = client.post(
"/api/v1/workspaces",
json={
"name": "My Workspace",
"subscription_plan": "free",
},
headers=self.headers,
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "My Workspace"
assert data["subscription_plan"] == "free"
assert data["max_projects"] == 3
def test_list_workspaces(self):
"""测试获取工作空间列表"""
# 创建工作空间
client.post("/api/v1/workspaces", json={
"name": "Workspace 1",
}, headers=self.headers)
client.post(
"/api/v1/workspaces",
json={
"name": "Workspace 1",
},
headers=self.headers,
)
# 获取列表
response = client.get("/api/v1/workspaces", headers=self.headers)
assert response.status_code == 200
data = response.json()
assert len(data["workspaces"]) > 0
assert data["workspaces"][0]["name"] == "Workspace 1"
def test_create_workspace_unauthorized(self):
"""测试未登录创建工作空间"""
response = client.post("/api/v1/workspaces", json={
"name": "Unauthorized Workspace",
})
response = client.post(
"/api/v1/workspaces",
json={
"name": "Unauthorized Workspace",
},
)
assert response.status_code == 403 # FastAPI HTTPBearer 返回 403
+12 -12
View File
@@ -13,10 +13,10 @@ def test_add_tag_to_asset():
storage_key="uploads/abc/video.mp4",
mime_type="video/mp4",
)
asset.add_tag("风景")
asset.add_tag("自然")
assert len(asset.tags) == 2
assert "风景" in asset.tags
assert "自然" in asset.tags
@@ -32,10 +32,10 @@ def test_add_duplicate_tag_should_ignore():
storage_key="uploads/abc/video.mp4",
mime_type="video/mp4",
)
asset.add_tag("风景")
asset.add_tag("风景") # 重复
assert len(asset.tags) == 1
assert asset.tags.count("风景") == 1
@@ -50,10 +50,10 @@ def test_add_empty_tag_should_fail():
storage_key="uploads/abc/video.mp4",
mime_type="video/mp4",
)
with pytest.raises(ValueError, match="标签不能为空"):
asset.add_tag("")
with pytest.raises(ValueError, match="标签不能为空"):
asset.add_tag(" ") # 仅空格
@@ -68,12 +68,12 @@ def test_remove_tag_from_asset():
storage_key="uploads/abc/video.mp4",
mime_type="video/mp4",
)
asset.add_tag("风景")
asset.add_tag("自然")
asset.remove_tag("风景")
assert len(asset.tags) == 1
assert "风景" not in asset.tags
assert "自然" in asset.tags
@@ -89,11 +89,11 @@ def test_remove_nonexistent_tag_should_be_idempotent():
storage_key="uploads/abc/video.mp4",
mime_type="video/mp4",
)
asset.add_tag("风景")
# 删除不存在的标签,不应报错
asset.remove_tag("不存在的标签")
assert len(asset.tags) == 1
assert "风景" in asset.tags
@@ -1,6 +1,9 @@
from packages.application import SubmitClassificationJobCommand, SubmitClassificationJobUseCase
from packages.adapters.in_memory import InMemoryClassificationJobRepository
from packages.domain import ClassificationJobStatus, AssetClassification
from packages.application import (
SubmitClassificationJobCommand,
SubmitClassificationJobUseCase,
)
from packages.domain import AssetClassification, ClassificationJobStatus
def simulate_classify_asset(job_id: str, job_repo: InMemoryClassificationJobRepository) -> dict:
@@ -8,24 +11,24 @@ def simulate_classify_asset(job_id: str, job_repo: InMemoryClassificationJobRepo
job = job_repo.get(job_id)
if job is None:
return {"status": "failed", "error": "job not found"}
try:
# Update job status to PROCESSING
job.status = ClassificationJobStatus.PROCESSING
job_repo.update(job)
# Mock classification
asset_id_hash = sum(ord(c) for c in job.asset_id)
classifications = list(AssetClassification)
classification = classifications[asset_id_hash % len(classifications)]
confidence = 0.85
# Update job status to COMPLETED
job.status = ClassificationJobStatus.COMPLETED
job.classification = classification.value
job.confidence = confidence
job_repo.update(job)
return {
"status": "completed",
"job_id": job.id,
@@ -37,7 +40,7 @@ def simulate_classify_asset(job_id: str, job_repo: InMemoryClassificationJobRepo
job.status = ClassificationJobStatus.FAILED
job.error_message = str(e)
job_repo.update(job)
return {
"status": "failed",
"job_id": job.id,
@@ -48,7 +51,7 @@ def simulate_classify_asset(job_id: str, job_repo: InMemoryClassificationJobRepo
def test_classification_pipeline():
"""Test the full classification pipeline: submit job -> worker processes -> result."""
job_repo = InMemoryClassificationJobRepository()
# Submit classification job
use_case = SubmitClassificationJobUseCase(job_repo)
job = use_case.execute(
@@ -58,18 +61,18 @@ def test_classification_pipeline():
asset_id="asset-123",
)
)
assert job.status == ClassificationJobStatus.PENDING
assert job.classification == ""
assert job.confidence == 0.0
# Simulate worker task execution
result = simulate_classify_asset(job.id, job_repo)
assert result["status"] == "completed"
assert "classification" in result
assert "confidence" in result
# Verify job was updated
updated_job = job_repo.get(job.id)
assert updated_job is not None
+16 -3
View File
@@ -1,6 +1,10 @@
from datetime import datetime, timezone
from packages.application import CreateGenerationTaskCommand, CreateGenerationTaskUseCase, GetGeneratedVideoDownloadUrlUseCase
from packages.application import (
CreateGenerationTaskCommand,
CreateGenerationTaskUseCase,
GetGeneratedVideoDownloadUrlUseCase,
)
from packages.domain import GeneratedVideo, GenerationTaskStatus
@@ -41,7 +45,11 @@ class DummyGeneratedVideoRepository:
return [video for video in self.items.values() if video.generation_task_id == generation_task_id]
def simulate_generate_video(task_id: str, task_repo: DummyGenerationTaskRepository, video_repo: DummyGeneratedVideoRepository) -> dict:
def simulate_generate_video(
task_id: str,
task_repo: DummyGenerationTaskRepository,
video_repo: DummyGeneratedVideoRepository,
) -> dict:
task = task_repo.get(task_id)
if task is None:
return {"status": "failed", "error": "task not found"}
@@ -75,7 +83,12 @@ def simulate_generate_video(task_id: str, task_repo: DummyGenerationTaskReposito
task.completed_at = datetime.now(timezone.utc)
task_repo.update(task)
return {"status": "completed", "task_id": task.id, "video_id": video.id, "file_url": file_url}
return {
"status": "completed",
"task_id": task.id,
"video_id": video.id,
"file_url": file_url,
}
def test_create_generation_task_smoke():
+22 -15
View File
@@ -1,9 +1,16 @@
from packages.adapters.in_memory import (
InMemoryAssetRepository,
InMemoryIngestJobRepository,
)
from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
from packages.adapters.in_memory import InMemoryAssetRepository, InMemoryIngestJobRepository
from packages.domain import Asset, IngestJob, IngestJobStatus
def simulate_ingest_asset(job_id: str, job_repo: InMemoryIngestJobRepository, asset_repo: InMemoryAssetRepository) -> dict:
def simulate_ingest_asset(
job_id: str,
job_repo: InMemoryIngestJobRepository,
asset_repo: InMemoryAssetRepository,
) -> dict:
"""
Simulate ingest asset logic without Celery.
This is the core business logic that would run inside the worker task.
@@ -11,12 +18,12 @@ def simulate_ingest_asset(job_id: str, job_repo: InMemoryIngestJobRepository, as
job = job_repo.get(job_id)
if job is None:
return {"status": "failed", "error": "job not found"}
try:
# Update job status to PROCESSING
job.status = IngestJobStatus.PROCESSING
job_repo.update(job)
# Mock metadata extraction
mime_type = "video/mp4" if job.storage_key.endswith(".mp4") else "image/jpeg"
metadata = {
@@ -25,10 +32,10 @@ def simulate_ingest_asset(job_id: str, job_repo: InMemoryIngestJobRepository, as
"height": 1080,
"size_bytes": 1024000,
}
# Extract filename from storage_key
filename = job.storage_key.split("/")[-1]
# Create Asset
asset = Asset.create(
workspace_id=job.workspace_id,
@@ -40,12 +47,12 @@ def simulate_ingest_asset(job_id: str, job_repo: InMemoryIngestJobRepository, as
metadata=metadata,
)
asset_repo.create(asset)
# Update job status to COMPLETED
job.status = IngestJobStatus.COMPLETED
job.result_asset_id = asset.id
job_repo.update(job)
return {
"status": "completed",
"job_id": job.id,
@@ -56,7 +63,7 @@ def simulate_ingest_asset(job_id: str, job_repo: InMemoryIngestJobRepository, as
job.status = IngestJobStatus.FAILED
job.error_message = str(e)
job_repo.update(job)
return {
"status": "failed",
"job_id": job.id,
@@ -68,7 +75,7 @@ def test_ingest_asset_pipeline():
"""Test the full ingest pipeline: submit job -> worker processes -> asset created."""
job_repo = InMemoryIngestJobRepository()
asset_repo = InMemoryAssetRepository()
# Submit ingest job
use_case = SubmitIngestJobUseCase(job_repo)
job = use_case.execute(
@@ -79,22 +86,22 @@ def test_ingest_asset_pipeline():
storage_key="uploads/test-video.mp4",
)
)
assert job.status == IngestJobStatus.PENDING
assert job.result_asset_id == ""
# Simulate worker task execution
result = simulate_ingest_asset(job.id, job_repo, asset_repo)
assert result["status"] == "completed"
assert "asset_id" in result
# Verify job was updated
updated_job = job_repo.get(job.id)
assert updated_job is not None
assert updated_job.status == IngestJobStatus.COMPLETED
assert updated_job.result_asset_id != ""
# Verify asset was created
assets = asset_repo.list_by_library("lib-1")
assert len(assets) == 1
+37 -36
View File
@@ -1,4 +1,5 @@
"""项目管理功能集成测试"""
import pytest
from packages.adapters.in_memory.project_management_repositories import (
@@ -23,7 +24,7 @@ def test_create_task():
"""测试创建任务"""
repo = InMemoryTaskRepository()
use_case = CreateTaskUseCase(repo)
task = use_case.execute(
project_id="proj_1",
workspace_id="ws_1",
@@ -31,7 +32,7 @@ def test_create_task():
description="实现用户登录功能",
priority=TaskPriority.HIGH,
)
assert task.id is not None
assert task.name == "开发登录功能"
assert task.status == TaskStatus.PENDING
@@ -43,7 +44,7 @@ def test_list_tasks():
"""测试获取任务列表"""
repo = InMemoryTaskRepository()
create_use_case = CreateTaskUseCase(repo)
# 创建两个任务
create_use_case.execute(
project_id="proj_1",
@@ -55,11 +56,11 @@ def test_list_tasks():
workspace_id="ws_1",
name="任务2",
)
# 查询任务列表
list_use_case = ListProjectTasksUseCase(repo)
tasks = list_use_case.execute("proj_1")
assert len(tasks) == 2
assert tasks[0].name == "任务1"
assert tasks[1].name == "任务2"
@@ -70,17 +71,17 @@ def test_update_task_status():
repo = InMemoryTaskRepository()
create_use_case = CreateTaskUseCase(repo)
update_use_case = UpdateTaskStatusUseCase(repo)
# 创建任务
task = create_use_case.execute(
project_id="proj_1",
workspace_id="ws_1",
name="测试任务",
)
# 更新状态为进行中
updated_task = update_use_case.execute(task.id, TaskStatus.IN_PROGRESS)
assert updated_task.status == TaskStatus.IN_PROGRESS
assert updated_task.actual_start_date is not None
@@ -90,23 +91,23 @@ def test_update_task_progress():
repo = InMemoryTaskRepository()
create_use_case = CreateTaskUseCase(repo)
progress_use_case = UpdateTaskProgressUseCase(repo)
# 创建任务
task = create_use_case.execute(
project_id="proj_1",
workspace_id="ws_1",
name="测试任务",
)
# 更新进度到 50%
updated_task = progress_use_case.execute(task.id, 50.0)
assert updated_task.progress == 50.0
assert updated_task.status == TaskStatus.IN_PROGRESS
# 更新进度到 100%
completed_task = progress_use_case.execute(task.id, 100.0)
assert completed_task.progress == 100.0
assert completed_task.status == TaskStatus.COMPLETED
assert completed_task.actual_end_date is not None
@@ -116,14 +117,14 @@ def test_create_milestone():
"""测试创建里程碑"""
repo = InMemoryMilestoneRepository()
use_case = CreateMilestoneUseCase(repo)
milestone = use_case.execute(
project_id="proj_1",
workspace_id="ws_1",
name="V1.0 发布",
description="第一个正式版本",
)
assert milestone.id is not None
assert milestone.name == "V1.0 发布"
assert milestone.completed is False
@@ -135,7 +136,7 @@ def test_create_and_resolve_issue():
create_use_case = CreateTaskIssueUseCase(repo)
resolve_use_case = ResolveTaskIssueUseCase(repo)
list_use_case = ListTaskIssuesUseCase(repo)
# 创建问题
issue = create_use_case.execute(
task_id="task_1",
@@ -144,17 +145,17 @@ def test_create_and_resolve_issue():
title="接口报错",
description="调用登录接口返回 500",
)
assert issue.id is not None
assert issue.title == "接口报错"
assert issue.resolved is False
# 解决问题
resolved_issue = resolve_use_case.execute(issue.id)
assert resolved_issue.resolved is True
assert resolved_issue.resolved_at is not None
# 查询任务问题列表
issues = list_use_case.execute("task_1")
assert len(issues) == 1
@@ -165,14 +166,14 @@ def test_task_hierarchy():
"""测试任务层级关系"""
repo = InMemoryTaskRepository()
create_use_case = CreateTaskUseCase(repo)
# 创建父任务
parent_task = create_use_case.execute(
project_id="proj_1",
workspace_id="ws_1",
name="开发用户模块",
)
# 创建子任务
child_task_1 = create_use_case.execute(
project_id="proj_1",
@@ -180,17 +181,17 @@ def test_task_hierarchy():
name="登录功能",
parent_task_id=parent_task.id,
)
child_task_2 = create_use_case.execute(
project_id="proj_1",
workspace_id="ws_1",
name="注册功能",
parent_task_id=parent_task.id,
)
# 查询子任务
children = repo.list_by_parent(parent_task.id)
assert len(children) == 2
assert children[0].parent_task_id == parent_task.id
assert children[1].parent_task_id == parent_task.id
@@ -199,11 +200,11 @@ def test_task_hierarchy():
def test_get_task_detail():
"""测试获取任务详情"""
from packages.application.get_task_detail_use_case import GetTaskDetailUseCase
repo = InMemoryTaskRepository()
create_use_case = CreateTaskUseCase(repo)
get_use_case = GetTaskDetailUseCase(repo)
# 创建任务
task = create_use_case.execute(
project_id="proj_1",
@@ -211,14 +212,14 @@ def test_get_task_detail():
name="测试任务",
description="这是一个测试任务",
)
# 获取详情
retrieved_task = get_use_case.execute(task.id)
assert retrieved_task.id == task.id
assert retrieved_task.name == "测试任务"
assert retrieved_task.description == "这是一个测试任务"
# 测试不存在的任务
try:
get_use_case.execute("nonexistent_id")
@@ -230,11 +231,11 @@ def test_get_task_detail():
def test_update_task():
"""测试任务基本信息更新"""
from packages.application.update_task_use_case import UpdateTaskUseCase
repo = InMemoryTaskRepository()
create_use_case = CreateTaskUseCase(repo)
update_use_case = UpdateTaskUseCase(repo)
# 创建任务
task = create_use_case.execute(
project_id="proj_1",
@@ -243,7 +244,7 @@ def test_update_task():
description="原始描述",
priority="low",
)
# 更新任务
updated_task = update_use_case.execute(
task_id=task.id,
@@ -251,17 +252,17 @@ def test_update_task():
description="更新后的描述",
priority="high",
)
assert updated_task.name == "更新后的任务"
assert updated_task.description == "更新后的描述"
assert updated_task.priority == "high"
# 部分更新
partial_updated = update_use_case.execute(
task_id=task.id,
name="又更新了",
)
assert partial_updated.name == "又更新了"
assert partial_updated.description == "更新后的描述" # 保持不变
assert partial_updated.priority == "high" # 保持不变
+6 -6
View File
@@ -1,3 +1,9 @@
from packages.adapters.in_memory import (
InMemoryAssetLibraryRepository,
InMemoryAssetRepository,
InMemoryIngestJobRepository,
InMemoryProjectRepository,
)
from packages.application import (
CreateAssetCommand,
CreateAssetLibraryCommand,
@@ -11,12 +17,6 @@ from packages.application import (
SubmitIngestJobCommand,
SubmitIngestJobUseCase,
)
from packages.adapters.in_memory import (
InMemoryAssetLibraryRepository,
InMemoryAssetRepository,
InMemoryIngestJobRepository,
InMemoryProjectRepository,
)
from packages.domain import AssetLibraryKind, IngestJobStatus
@@ -2,7 +2,9 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from packages.adapters.sqlalchemy_impl.models import Base
from packages.adapters.sqlalchemy_impl.project_repository import SQLAlchemyProjectRepository
from packages.adapters.sqlalchemy_impl.project_repository import (
SQLAlchemyProjectRepository,
)
from packages.application import CreateProjectCommand, CreateProjectUseCase
@@ -13,12 +15,12 @@ def test_sqlalchemy_project_repository():
Base.metadata.create_all(engine)
SessionLocal = sessionmaker(bind=engine)
session: Session = SessionLocal()
try:
# Create repository and use case
repository = SQLAlchemyProjectRepository(session)
use_case = CreateProjectUseCase(repository)
# Create project
project = use_case.execute(
CreateProjectCommand(
@@ -27,10 +29,10 @@ def test_sqlalchemy_project_repository():
description="Test description",
)
)
assert project.name == "Test Project"
assert project.workspace_id == "ws-1"
# List projects
projects = repository.list_by_workspace("ws-1")
assert len(projects) == 1
+13 -9
View File
@@ -1,5 +1,8 @@
from packages.adapters.in_memory import (
InMemoryAssetRepository,
InMemoryIngestJobRepository,
)
from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
from packages.adapters.in_memory import InMemoryAssetRepository, InMemoryIngestJobRepository
from packages.domain import IngestJobStatus
@@ -13,11 +16,12 @@ def simulate_upload_and_ingest(
) -> dict:
"""Simulate full upload → ingest pipeline."""
from uuid import uuid4
from tests.integration.test_ingest_pipeline import simulate_ingest_asset
# Mock storage: generate storage_key
storage_key = f"uploads/{uuid4().hex[:8]}/{filename}"
# Submit ingest job
use_case = SubmitIngestJobUseCase(job_repo)
job = use_case.execute(
@@ -28,10 +32,10 @@ def simulate_upload_and_ingest(
storage_key=storage_key,
)
)
# Simulate worker task
result = simulate_ingest_asset(job.id, job_repo, asset_repo)
return {
"storage_key": storage_key,
"job_id": job.id,
@@ -43,7 +47,7 @@ def test_upload_to_asset_full_pipeline():
"""Test full pipeline: upload → storage → ingest job → worker → asset created."""
job_repo = InMemoryIngestJobRepository()
asset_repo = InMemoryAssetRepository()
# Simulate upload
result = simulate_upload_and_ingest(
workspace_id="ws-1",
@@ -53,17 +57,17 @@ def test_upload_to_asset_full_pipeline():
job_repo=job_repo,
asset_repo=asset_repo,
)
assert "storage_key" in result
assert result["storage_key"].endswith("demo-video.mp4")
assert result["worker_result"]["status"] == "completed"
# Verify job was completed
job = job_repo.get(result["job_id"])
assert job is not None
assert job.status == IngestJobStatus.COMPLETED
assert job.result_asset_id != ""
# Verify asset was created
assets = asset_repo.list_by_library("lib-1")
assert len(assets) == 1