Initial commit: Pixel AI comic/video creation platform

- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents
- Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow
- Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.)
- All HTTP clients migrated from sync requests to async httpx
- Admin-managed API keys via environment variables
- SSRF vulnerability fixed in ensure_url()
This commit is contained in:
张鹏
2026-04-29 01:20:12 +08:00
commit f9f4560459
808 changed files with 151724 additions and 0 deletions

View File

@@ -0,0 +1,775 @@
"""
Unit tests for data model mappers
Tests mapper conversion correctness between entities and schemas.
"""
import pytest
from datetime import datetime
import uuid
from src.models.entities import (
ProjectDB,
AssetDB,
EpisodeDB,
StoryboardDB,
TaskDB,
CanvasMetadataDB,
)
from src.models.schemas import (
CreateProjectRequest,
UpdateProjectRequest,
CreateCharacterAssetRequest,
CreateSceneAssetRequest,
CreatePropAssetRequest,
UpdateAssetRequest,
CreateEpisodeRequest,
UpdateEpisodeRequest,
CreateStoryboardRequest,
UpdateStoryboardRequest,
)
from src.mappers import (
ProjectMapper,
AssetMapper,
EpisodeMapper,
StoryboardMapper,
TaskMapper,
CanvasMetadataMapper,
)
class TestProjectMapper:
"""Test ProjectMapper conversion correctness"""
def test_to_entity_from_create_request(self):
"""Test converting CreateProjectRequest to ProjectDB entity"""
request = CreateProjectRequest(
name="Test Project",
description="Test Description",
type="video",
chapters=[{"title": "Chapter 1"}],
assets=[{"name": "Asset 1"}]
)
entity = ProjectMapper.to_entity(request)
assert entity.name == "Test Project"
assert entity.description == "Test Description"
assert entity.type == "video"
assert entity.status == "active"
assert entity.id is not None
assert entity.created_at is not None
assert entity.updated_at is not None
assert entity.chapters == [{"title": "Chapter 1"}]
def test_to_entity_with_custom_id(self):
"""Test converting with custom ID"""
request = CreateProjectRequest(name="Test Project")
custom_id = str(uuid.uuid4())
entity = ProjectMapper.to_entity(request, project_id=custom_id)
assert entity.id == custom_id
def test_to_schema_from_entity(self):
"""Test converting ProjectDB entity to schema"""
now = datetime.now().timestamp()
entity = ProjectDB(
id="proj_123",
name="Test Project",
description="Test Description",
type="video",
status="active",
created_at=now,
updated_at=now,
resolution="1920x1080",
ratio="16:9"
)
schema = ProjectMapper.to_schema(entity)
assert schema.id == "proj_123"
assert schema.name == "Test Project"
assert schema.description == "Test Description"
assert schema.type == "video"
assert schema.status == "active"
assert schema.resolution == "1920x1080"
assert schema.ratio == "16:9"
def test_update_entity(self):
"""Test updating ProjectDB entity from UpdateProjectRequest"""
entity = ProjectDB(
name="Original Name",
description="Original Description",
resolution="1280x720"
)
update_request = UpdateProjectRequest(
name="Updated Name",
resolution="1920x1080"
)
updated = ProjectMapper.update_entity(entity, update_request)
assert updated.name == "Updated Name"
assert updated.resolution == "1920x1080"
assert updated.description == "Original Description" # unchanged
assert updated.updated_at > entity.created_at
def test_update_entity_partial(self):
"""Test partial update of ProjectDB entity"""
entity = ProjectDB(
name="Original Name",
description="Original Description",
resolution="1280x720"
)
update_request = UpdateProjectRequest(name="Updated Name")
updated = ProjectMapper.update_entity(entity, update_request)
assert updated.name == "Updated Name"
assert updated.description == "Original Description"
assert updated.resolution == "1280x720"
def test_roundtrip_conversion(self):
"""Test roundtrip conversion: Request -> Entity -> Schema"""
request = CreateProjectRequest(
name="Test Project",
description="Test Description",
type="video"
)
entity = ProjectMapper.to_entity(request)
schema = ProjectMapper.to_schema(entity)
assert schema.name == request.name
assert schema.description == request.description
assert schema.type == request.type
class TestAssetMapper:
"""Test AssetMapper conversion correctness"""
def test_to_entity_character_asset(self):
"""Test converting CreateCharacterAssetRequest to AssetDB"""
request = CreateCharacterAssetRequest(
type="character",
name="Hero",
desc="Main character",
tags=["protagonist"],
age="25",
gender="male",
role="hero",
appearance="Tall and strong"
)
entity = AssetMapper.to_entity(request, project_id="proj_123")
assert entity.project_id == "proj_123"
assert entity.type == "character"
assert entity.name == "Hero"
assert entity.desc == "Main character"
assert entity.tags == ["protagonist"]
assert entity.extra_data["age"] == "25"
assert entity.extra_data["gender"] == "male"
assert entity.extra_data["role"] == "hero"
assert entity.extra_data["appearance"] == "Tall and strong"
def test_to_entity_scene_asset(self):
"""Test converting CreateSceneAssetRequest to AssetDB"""
request = CreateSceneAssetRequest(
type="scene",
name="Forest",
desc="Dark forest",
location="Northern Woods",
time_of_day="night",
atmosphere="mysterious"
)
entity = AssetMapper.to_entity(request, project_id="proj_123")
assert entity.type == "scene"
assert entity.name == "Forest"
assert entity.extra_data["location"] == "Northern Woods"
assert entity.extra_data["time_of_day"] == "night"
assert entity.extra_data["atmosphere"] == "mysterious"
def test_to_entity_prop_asset(self):
"""Test converting CreatePropAssetRequest to AssetDB"""
request = CreatePropAssetRequest(
type="prop",
name="Magic Sword",
desc="Ancient sword",
usage="weapon"
)
entity = AssetMapper.to_entity(request, project_id="proj_123")
assert entity.type == "prop"
assert entity.name == "Magic Sword"
assert entity.extra_data["usage"] == "weapon"
def test_to_schema_character_asset(self):
"""Test converting AssetDB to CharacterAsset schema"""
entity = AssetDB(
id="asset_123",
project_id="proj_123",
type="character",
name="Hero",
desc="Main character",
tags=["protagonist"],
extra_data={"age": "25", "gender": "male", "role": "hero"}
)
schema = AssetMapper.to_schema(entity)
assert schema.id == "asset_123"
assert schema.type == "character"
assert schema.name == "Hero"
assert schema.age == "25"
assert schema.gender == "male"
assert schema.role == "hero"
def test_to_schema_scene_asset(self):
"""Test converting AssetDB to SceneAsset schema"""
entity = AssetDB(
id="asset_123",
project_id="proj_123",
type="scene",
name="Forest",
desc="Dark forest",
extra_data={"location": "Northern Woods", "time_of_day": "night"}
)
schema = AssetMapper.to_schema(entity)
assert schema.type == "scene"
assert schema.name == "Forest"
assert schema.location == "Northern Woods"
assert schema.time_of_day == "night"
def test_update_entity(self):
"""Test updating AssetDB entity"""
entity = AssetDB(
project_id="proj_123",
type="character",
name="Original Name",
desc="Original Description",
extra_data={"age": "25"}
)
update_request = UpdateAssetRequest(
name="Updated Name",
age="26"
)
updated = AssetMapper.update_entity(entity, update_request)
assert updated.name == "Updated Name"
assert updated.extra_data["age"] == "26"
assert updated.desc == "Original Description"
def test_roundtrip_conversion_character(self):
"""Test roundtrip conversion for character asset"""
request = CreateCharacterAssetRequest(
type="character",
name="Hero",
desc="Main character",
age="25",
gender="male"
)
entity = AssetMapper.to_entity(request, project_id="proj_123")
schema = AssetMapper.to_schema(entity)
assert schema.name == request.name
assert schema.desc == request.desc
assert schema.age == request.age
assert schema.gender == request.gender
class TestEpisodeMapper:
"""Test EpisodeMapper conversion correctness"""
def test_to_entity(self):
"""Test converting CreateEpisodeRequest to EpisodeDB"""
request = CreateEpisodeRequest(
title="Episode 1",
order=1,
desc="First episode",
status="draft"
)
entity = EpisodeMapper.to_entity(request, project_id="proj_123")
assert entity.project_id == "proj_123"
assert entity.title == "Episode 1"
assert entity.order_index == 1
assert entity.desc == "First episode"
assert entity.status == "draft"
assert entity.id is not None
def test_to_schema(self):
"""Test converting EpisodeDB to Episode schema"""
entity = EpisodeDB(
id="ep_123",
project_id="proj_123",
order_index=1,
title="Episode 1",
desc="First episode",
content="Episode content",
status="draft"
)
schema = EpisodeMapper.to_schema(entity)
assert schema.id == "ep_123"
assert schema.title == "Episode 1"
assert schema.order == 1
assert schema.desc == "First episode"
assert schema.content == "Episode content"
assert schema.status == "draft"
def test_update_entity(self):
"""Test updating EpisodeDB entity"""
entity = EpisodeDB(
project_id="proj_123",
order_index=1,
title="Original Title",
status="draft"
)
update_request = UpdateEpisodeRequest(
title="Updated Title",
status="production"
)
updated = EpisodeMapper.update_entity(entity, update_request)
assert updated.title == "Updated Title"
assert updated.status == "production"
assert updated.order_index == 1 # unchanged
def test_roundtrip_conversion(self):
"""Test roundtrip conversion for episode"""
request = CreateEpisodeRequest(
title="Episode 1",
order=1,
desc="First episode"
)
entity = EpisodeMapper.to_entity(request, project_id="proj_123")
schema = EpisodeMapper.to_schema(entity)
assert schema.title == request.title
assert schema.order == request.order
assert schema.desc == request.desc
class TestStoryboardMapper:
"""Test StoryboardMapper conversion correctness"""
def test_to_entity(self):
"""Test converting CreateStoryboardRequest to StoryboardDB"""
request = CreateStoryboardRequest(
episode_id="ep_123",
order=1,
shot="Shot 1",
desc="Opening scene",
duration="5s",
type="image",
scene_id="scene_123",
character_ids=["char_1", "char_2"],
prop_ids=["prop_1"],
camera_angle="wide",
lens="50mm",
location="forest",
time="morning"
)
entity = StoryboardMapper.to_entity(request, project_id="proj_123")
assert entity.project_id == "proj_123"
assert entity.episode_id == "ep_123"
assert entity.order_index == 1
assert entity.shot == "Shot 1"
assert entity.desc == "Opening scene"
assert entity.duration == "5s"
assert entity.type == "image"
assert entity.scene_id == "scene_123"
assert entity.character_ids == ["char_1", "char_2"]
assert entity.prop_ids == ["prop_1"]
assert entity.camera_angle == "wide"
assert entity.lens == "50mm"
assert entity.location == "forest"
assert entity.time == "morning"
def test_to_schema(self):
"""Test converting StoryboardDB to Storyboard schema"""
entity = StoryboardDB(
id="sb_123",
project_id="proj_123",
episode_id="ep_123",
order_index=1,
shot="Shot 1",
desc="Opening scene",
duration="5s",
type="image",
scene_id="scene_123",
character_ids=["char_1"],
camera_angle="wide",
location="forest"
)
schema = StoryboardMapper.to_schema(entity)
assert schema.id == "sb_123"
assert schema.episode_id == "ep_123"
assert schema.order == 1
assert schema.shot == "Shot 1"
assert schema.scene_id == "scene_123"
assert schema.character_ids == ["char_1"]
assert schema.camera_angle == "wide"
assert schema.location == "forest"
def test_update_entity(self):
"""Test updating StoryboardDB entity"""
entity = StoryboardDB(
project_id="proj_123",
episode_id="ep_123",
order_index=1,
shot="Original Shot",
desc="Original Description",
duration="5s",
type="image"
)
update_request = UpdateStoryboardRequest(
shot="Updated Shot",
duration="10s",
camera_angle="close-up"
)
updated = StoryboardMapper.update_entity(entity, update_request)
assert updated.shot == "Updated Shot"
assert updated.duration == "10s"
assert updated.camera_angle == "close-up"
assert updated.desc == "Original Description" # unchanged
def test_roundtrip_conversion(self):
"""Test roundtrip conversion for storyboard"""
request = CreateStoryboardRequest(
episode_id="ep_123",
order=1,
shot="Shot 1",
desc="Opening scene",
duration="5s",
type="image",
camera_angle="wide"
)
entity = StoryboardMapper.to_entity(request, project_id="proj_123")
schema = StoryboardMapper.to_schema(entity)
assert schema.episode_id == request.episode_id
assert schema.order == request.order
assert schema.shot == request.shot
assert schema.camera_angle == request.camera_angle
class TestTaskMapper:
"""Test TaskMapper conversion correctness"""
def test_to_entity(self):
"""Test creating TaskDB entity from parameters"""
entity = TaskMapper.to_entity(
task_type="image",
model="flux-dev",
params={"prompt": "test"},
status="pending",
user_id="user_123",
project_id="proj_123",
max_retries=5
)
assert entity.type == "image"
assert entity.model == "flux-dev"
assert entity.params == {"prompt": "test"}
assert entity.status == "pending"
assert entity.user_id == "user_123"
assert entity.project_id == "proj_123"
assert entity.max_retries == 5
assert entity.retry_count == 0
assert entity.id is not None
def test_to_entity_with_custom_id(self):
"""Test creating TaskDB with custom ID"""
custom_id = str(uuid.uuid4())
entity = TaskMapper.to_entity(
task_type="video",
model="kling-v1",
params={"prompt": "test"},
task_id=custom_id
)
assert entity.id == custom_id
def test_to_schema(self):
"""Test converting TaskDB to Task schema"""
now = datetime.now().timestamp()
entity = TaskDB(
id="task_123",
type="image",
status="success",
created_at=now,
updated_at=now,
model="flux-dev",
params={"prompt": "test"},
provider_task_id="provider_123",
result={"url": "https://example.com/image.png"},
retry_count=1,
max_retries=3,
started_at=now,
completed_at=now + 10,
user_id="user_123",
project_id="proj_123"
)
schema = TaskMapper.to_schema(entity)
assert schema.id == "task_123"
assert schema.type == "image"
assert schema.status == "success"
assert schema.model == "flux-dev"
assert schema.provider_task_id == "provider_123"
assert schema.result["url"] == "https://example.com/image.png"
assert schema.retry_count == 1
assert schema.user_id == "user_123"
def test_update_status_to_processing(self):
"""Test updating task status to processing"""
entity = TaskDB(
type="image",
status="pending",
model="flux-dev",
params={"prompt": "test"}
)
updated = TaskMapper.update_status(
entity,
status="processing",
provider_task_id="provider_123"
)
assert updated.status == "processing"
assert updated.provider_task_id == "provider_123"
assert updated.started_at is not None
assert updated.completed_at is None
def test_update_status_to_success(self):
"""Test updating task status to success"""
entity = TaskDB(
type="image",
status="processing",
model="flux-dev",
params={"prompt": "test"}
)
result = {"url": "https://example.com/image.png"}
updated = TaskMapper.update_status(
entity,
status="success",
result=result
)
assert updated.status == "success"
assert updated.result == result
assert updated.completed_at is not None
def test_update_status_to_failed(self):
"""Test updating task status to failed"""
entity = TaskDB(
type="image",
status="processing",
model="flux-dev",
params={"prompt": "test"}
)
updated = TaskMapper.update_status(
entity,
status="failed",
error="Generation failed"
)
assert updated.status == "failed"
assert updated.error == "Generation failed"
assert updated.completed_at is not None
def test_increment_retry(self):
"""Test incrementing retry count"""
entity = TaskDB(
type="image",
status="failed",
model="flux-dev",
params={"prompt": "test"},
retry_count=0
)
updated = TaskMapper.increment_retry(entity)
assert updated.retry_count == 1
assert updated.updated_at > entity.created_at
def test_multiple_retry_increments(self):
"""Test multiple retry increments"""
entity = TaskDB(
type="image",
status="failed",
model="flux-dev",
params={"prompt": "test"},
retry_count=0,
max_retries=3
)
# Increment 3 times
for i in range(3):
entity = TaskMapper.increment_retry(entity)
assert entity.retry_count == i + 1
assert entity.retry_count == 3
assert entity.retry_count == entity.max_retries
class TestCanvasMetadataMapper:
"""Test CanvasMetadataMapper conversion correctness"""
def test_to_entity_general_canvas(self):
"""Test creating general canvas metadata entity"""
from src.models.schemas import CreateGeneralCanvasRequest
request = CreateGeneralCanvasRequest(
name="Main Canvas",
description="Main project canvas"
)
entity = CanvasMetadataMapper.to_entity(
schema=request,
project_id="proj_123"
)
assert entity.project_id == "proj_123"
assert entity.canvas_type == "general"
assert entity.name == "Main Canvas"
assert entity.description == "Main project canvas"
assert entity.order_index == 0
assert entity.is_pinned is False
assert entity.related_entity_type is None
assert entity.related_entity_id is None
def test_create_asset_canvas(self):
"""Test creating asset canvas metadata entity"""
entity = CanvasMetadataMapper.create_asset_canvas(
project_id="proj_123",
asset_id="asset_123",
asset_name="Hero"
)
assert entity.project_id == "proj_123"
assert entity.canvas_type == "asset"
assert entity.related_entity_type == "asset"
assert entity.related_entity_id == "asset_123"
assert entity.name == "Hero Canvas"
def test_create_storyboard_canvas(self):
"""Test creating storyboard canvas metadata entity"""
entity = CanvasMetadataMapper.create_storyboard_canvas(
project_id="proj_123",
storyboard_id="sb_123",
storyboard_shot="Shot 1"
)
assert entity.project_id == "proj_123"
assert entity.canvas_type == "storyboard"
assert entity.related_entity_type == "storyboard"
assert entity.related_entity_id == "sb_123"
assert entity.name == "Shot 1 Canvas"
def test_to_schema(self):
"""Test converting CanvasMetadataDB to schema"""
now = datetime.now().timestamp()
entity = CanvasMetadataDB(
id="canvas_123",
project_id="proj_123",
canvas_type="general",
name="Main Canvas",
description="Main project canvas",
order_index=0,
is_pinned=True,
tags=["main", "primary"],
node_count=5,
last_accessed_at=now,
access_count=10,
created_at=now,
updated_at=now
)
schema = CanvasMetadataMapper.to_schema(entity)
assert schema.id == "canvas_123"
assert schema.project_id == "proj_123"
assert schema.canvas_type == "general"
assert schema.name == "Main Canvas"
assert schema.is_pinned is True
assert len(schema.tags) == 2
assert schema.node_count == 5
assert schema.access_count == 10
def test_update_entity(self):
"""Test updating canvas metadata"""
from src.models.schemas import UpdateCanvasMetadataRequest
entity = CanvasMetadataDB(
project_id="proj_123",
canvas_type="general",
name="Original Name",
description="Original Description",
order_index=0,
is_pinned=False,
tags=["old"]
)
update_request = UpdateCanvasMetadataRequest(
name="Updated Name",
isPinned=True,
tags=["new", "updated"]
)
updated = CanvasMetadataMapper.update_entity(entity, update_request)
assert updated.name == "Updated Name"
assert updated.is_pinned is True
assert updated.tags == ["new", "updated"]
assert updated.description == "Original Description" # unchanged
def test_update_access(self):
"""Test updating canvas access tracking"""
entity = CanvasMetadataDB(
project_id="proj_123",
canvas_type="general",
name="Main Canvas",
order_index=0,
is_pinned=False,
access_count=5
)
initial_access_count = entity.access_count
updated = CanvasMetadataMapper.update_access(entity)
assert updated.access_count == initial_access_count + 1
assert updated.last_accessed_at is not None
if __name__ == "__main__":
pytest.main([__file__, "-v"])