test: P3-1 第32波单元测试(bgm_utils/exceptions/asset_libraries) #807
Executable
+157
@@ -0,0 +1,157 @@
|
||||
"""素材库 UseCase 单元测试."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.application.asset_libraries import (
|
||||
CreateAssetLibraryCommand,
|
||||
CreateAssetLibraryUseCase,
|
||||
ListAssetLibrariesUseCase,
|
||||
)
|
||||
from packages.domain import AssetLibrary, AssetLibraryKind
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_repo():
|
||||
repo = MagicMock()
|
||||
return repo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_library():
|
||||
lib = AssetLibrary.create(
|
||||
project_id="proj_456",
|
||||
name="测试视频库",
|
||||
kind=AssetLibraryKind.VIDEO,
|
||||
)
|
||||
lib.id = "lib_123"
|
||||
return lib
|
||||
|
||||
|
||||
class TestListAssetLibrariesUseCase:
|
||||
"""ListAssetLibrariesUseCase 测试"""
|
||||
|
||||
def test_list_returns_repo_results(self, mock_repo, sample_library):
|
||||
"""正常返回 repository 的查询结果"""
|
||||
mock_repo.find_by_project.return_value = [sample_library]
|
||||
use_case = ListAssetLibrariesUseCase(mock_repo)
|
||||
|
||||
result = use_case.execute("proj_456")
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].id == "lib_123"
|
||||
mock_repo.find_by_project.assert_called_once_with("proj_456")
|
||||
|
||||
def test_empty_project_raises_value_error(self, mock_repo):
|
||||
"""空 project_id 抛出 ValueError"""
|
||||
use_case = ListAssetLibrariesUseCase(mock_repo)
|
||||
|
||||
with pytest.raises(ValueError, match="project_id 不能为空"):
|
||||
use_case.execute("")
|
||||
|
||||
mock_repo.find_by_project.assert_not_called()
|
||||
|
||||
def test_whitespace_project_raises_value_error(self, mock_repo):
|
||||
"""纯空格 project_id 也抛出 ValueError"""
|
||||
use_case = ListAssetLibrariesUseCase(mock_repo)
|
||||
|
||||
with pytest.raises(ValueError, match="project_id 不能为空"):
|
||||
use_case.execute(" ")
|
||||
|
||||
mock_repo.find_by_project.assert_not_called()
|
||||
|
||||
def test_project_id_stripped_before_query(self, mock_repo, sample_library):
|
||||
"""project_id 会被 strip 后再查询"""
|
||||
mock_repo.find_by_project.return_value = [sample_library]
|
||||
use_case = ListAssetLibrariesUseCase(mock_repo)
|
||||
|
||||
use_case.execute(" proj_456 ")
|
||||
|
||||
mock_repo.find_by_project.assert_called_once_with("proj_456")
|
||||
|
||||
def test_empty_list(self, mock_repo):
|
||||
"""项目没有素材库时返回空列表"""
|
||||
mock_repo.find_by_project.return_value = []
|
||||
use_case = ListAssetLibrariesUseCase(mock_repo)
|
||||
|
||||
result = use_case.execute("proj_456")
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestCreateAssetLibraryUseCase:
|
||||
"""CreateAssetLibraryUseCase 测试"""
|
||||
|
||||
def test_create_success(self, mock_repo, sample_library):
|
||||
"""创建成功返回 AssetLibrary"""
|
||||
mock_repo.create.return_value = sample_library
|
||||
use_case = CreateAssetLibraryUseCase(mock_repo)
|
||||
|
||||
command = CreateAssetLibraryCommand(
|
||||
project_id="proj_456",
|
||||
name="新素材库",
|
||||
kind=AssetLibraryKind.IMAGE,
|
||||
)
|
||||
result = use_case.execute(command)
|
||||
|
||||
assert result.id == "lib_123"
|
||||
mock_repo.create.assert_called_once()
|
||||
# 验证传入 repository 的是一个 AssetLibrary 对象
|
||||
created = mock_repo.create.call_args[0][0]
|
||||
assert isinstance(created, AssetLibrary)
|
||||
assert created.project_id == "proj_456"
|
||||
assert created.name == "新素材库"
|
||||
assert created.kind == AssetLibraryKind.IMAGE
|
||||
|
||||
def test_create_with_video_kind(self, mock_repo):
|
||||
"""创建视频类型素材库"""
|
||||
mock_repo.create.side_effect = lambda x: x
|
||||
use_case = CreateAssetLibraryUseCase(mock_repo)
|
||||
|
||||
command = CreateAssetLibraryCommand(
|
||||
project_id="proj_1",
|
||||
name="视频库",
|
||||
kind=AssetLibraryKind.VIDEO,
|
||||
)
|
||||
result = use_case.execute(command)
|
||||
|
||||
assert result.kind == AssetLibraryKind.VIDEO
|
||||
assert result.name == "视频库"
|
||||
|
||||
def test_create_with_voice_kind(self, mock_repo):
|
||||
"""创建音色类型素材库"""
|
||||
mock_repo.create.side_effect = lambda x: x
|
||||
use_case = CreateAssetLibraryUseCase(mock_repo)
|
||||
|
||||
command = CreateAssetLibraryCommand(
|
||||
project_id="proj_1",
|
||||
name="音色库",
|
||||
kind=AssetLibraryKind.VOICE,
|
||||
)
|
||||
result = use_case.execute(command)
|
||||
|
||||
assert result.kind == AssetLibraryKind.VOICE
|
||||
|
||||
|
||||
class TestCreateAssetLibraryCommand:
|
||||
"""CreateAssetLibraryCommand 数据类测试"""
|
||||
|
||||
def test_command_fields(self):
|
||||
"""命令对象字段正确"""
|
||||
cmd = CreateAssetLibraryCommand(
|
||||
project_id="proj_1",
|
||||
name="test",
|
||||
kind=AssetLibraryKind.VIDEO,
|
||||
)
|
||||
assert cmd.project_id == "proj_1"
|
||||
assert cmd.name == "test"
|
||||
assert cmd.kind == AssetLibraryKind.VIDEO
|
||||
|
||||
def test_command_is_dataclass(self):
|
||||
"""命令是 dataclass"""
|
||||
from dataclasses import is_dataclass
|
||||
|
||||
assert is_dataclass(CreateAssetLibraryCommand)
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
"""BGM 配置工具函数单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.bgm_utils import merge_bgm_config
|
||||
|
||||
|
||||
class TestMergeBgmConfig:
|
||||
"""merge_bgm_config 测试"""
|
||||
|
||||
def test_user_bgm_empty_returns_template_copy(self):
|
||||
"""用户配置为空时,返回模板配置的拷贝"""
|
||||
template = {"enabled": True, "volume": 0.5, "asset_id": "tpl_123"}
|
||||
result = merge_bgm_config(template, {})
|
||||
assert result == template
|
||||
assert result is not template
|
||||
|
||||
def test_user_bgm_none_returns_template_copy(self):
|
||||
"""用户配置为 None 时,返回模板配置的拷贝"""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
result = merge_bgm_config(template, None) # type: ignore
|
||||
assert result == template
|
||||
|
||||
def test_template_bgm_empty_returns_user_copy(self):
|
||||
"""模板配置为空时,返回用户配置的拷贝"""
|
||||
user = {"enabled": False, "volume": 0.8, "asset_id": "user_456"}
|
||||
result = merge_bgm_config({}, user)
|
||||
assert result == user
|
||||
assert result is not user
|
||||
|
||||
def test_template_bgm_none_returns_user_copy(self):
|
||||
"""模板配置为 None 时,返回用户配置的拷贝"""
|
||||
user = {"enabled": False, "volume": 0.8}
|
||||
result = merge_bgm_config(None, user) # type: ignore
|
||||
assert result == user
|
||||
|
||||
def test_user_fields_override_template(self):
|
||||
"""用户显式指定的字段覆盖模板对应字段"""
|
||||
template = {
|
||||
"enabled": True,
|
||||
"volume": 0.5,
|
||||
"asset_id": "tpl_123",
|
||||
"fade_in": 1.0,
|
||||
}
|
||||
user = {
|
||||
"volume": 0.8,
|
||||
"asset_id": "user_456",
|
||||
}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["volume"] == 0.8
|
||||
assert result["asset_id"] == "user_456"
|
||||
assert result["fade_in"] == 1.0 # 模板值保留
|
||||
|
||||
def test_enabled_not_in_user_preserves_template_enabled(self):
|
||||
"""enabled 特殊处理:用户没传 enabled 时保留模板的 enabled 值"""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"volume": 0.8} # 没传 enabled
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is True # 保留模板的
|
||||
assert result["volume"] == 0.8 # 用户指定的覆盖
|
||||
|
||||
def test_enabled_in_user_overrides_template(self):
|
||||
"""用户传了 enabled 时覆盖模板的 enabled"""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"enabled": False, "volume": 0.8}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is False
|
||||
assert result["volume"] == 0.8
|
||||
|
||||
def test_user_adds_new_fields(self):
|
||||
"""用户配置中的新字段会被添加到结果中"""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
user = {"sidechain_enabled": True, "sidechain_ratio": 0.6}
|
||||
result = merge_bgm_config(template, user)
|
||||
assert result["enabled"] is True
|
||||
assert result["volume"] == 0.5
|
||||
assert result["sidechain_enabled"] is True
|
||||
assert result["sidechain_ratio"] == 0.6
|
||||
|
||||
def test_both_empty_returns_empty_dict(self):
|
||||
"""两者都为空时返回空字典"""
|
||||
result = merge_bgm_config({}, {})
|
||||
assert result == {}
|
||||
|
||||
def test_nested_dict_shallow_merge(self):
|
||||
"""嵌套字典是浅合并(当前设计)"""
|
||||
template = {"enabled": True, "config": {"eq": True, "compression": False}}
|
||||
user = {"config": {"compression": True, "reverb": 0.5}}
|
||||
result = merge_bgm_config(template, user)
|
||||
# 浅合并:整个 config 被用户值覆盖
|
||||
assert result["config"] == {"compression": True, "reverb": 0.5}
|
||||
|
||||
def test_does_not_mutate_template(self):
|
||||
"""不修改原始模板配置"""
|
||||
template = {"enabled": True, "volume": 0.5}
|
||||
original = dict(template)
|
||||
merge_bgm_config(template, {"volume": 0.8})
|
||||
assert template == original
|
||||
|
||||
def test_does_not_mutate_user(self):
|
||||
"""不修改原始用户配置"""
|
||||
user = {"volume": 0.8}
|
||||
original = dict(user)
|
||||
merge_bgm_config({"enabled": True}, user)
|
||||
assert user == original
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
"""领域层通用异常单元测试."""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.exceptions import (
|
||||
DomainError,
|
||||
NotFoundError,
|
||||
QuotaExceededError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
class TestDomainError:
|
||||
"""DomainError 基类测试"""
|
||||
|
||||
def test_is_exception(self):
|
||||
"""DomainError 是 Exception 的子类"""
|
||||
assert issubclass(DomainError, Exception)
|
||||
|
||||
def test_can_raise_and_catch(self):
|
||||
"""可以抛出和捕获"""
|
||||
with pytest.raises(DomainError):
|
||||
raise DomainError("something went wrong")
|
||||
|
||||
def test_message(self):
|
||||
"""异常消息正确"""
|
||||
err = DomainError("test message")
|
||||
assert str(err) == "test message"
|
||||
|
||||
|
||||
class TestNotFoundError:
|
||||
"""NotFoundError 测试"""
|
||||
|
||||
def test_is_domain_error(self):
|
||||
"""NotFoundError 继承自 DomainError"""
|
||||
assert issubclass(NotFoundError, DomainError)
|
||||
|
||||
def test_can_raise_as_domain_error(self):
|
||||
"""可以作为 DomainError 捕获"""
|
||||
with pytest.raises(DomainError):
|
||||
raise NotFoundError("resource not found")
|
||||
|
||||
def test_default_message(self):
|
||||
"""无参构造"""
|
||||
err = NotFoundError()
|
||||
assert isinstance(err, NotFoundError)
|
||||
|
||||
def test_custom_message(self):
|
||||
"""自定义消息"""
|
||||
err = NotFoundError("user 123 not found")
|
||||
assert str(err) == "user 123 not found"
|
||||
|
||||
|
||||
class TestValidationError:
|
||||
"""ValidationError 测试"""
|
||||
|
||||
def test_is_domain_error(self):
|
||||
"""ValidationError 继承自 DomainError"""
|
||||
assert issubclass(ValidationError, DomainError)
|
||||
|
||||
def test_can_raise_as_domain_error(self):
|
||||
"""可以作为 DomainError 捕获"""
|
||||
with pytest.raises(DomainError):
|
||||
raise ValidationError("invalid input")
|
||||
|
||||
def test_custom_message(self):
|
||||
"""自定义消息"""
|
||||
err = ValidationError("duration must be positive")
|
||||
assert str(err) == "duration must be positive"
|
||||
|
||||
|
||||
class TestQuotaExceededError:
|
||||
"""QuotaExceededError 测试"""
|
||||
|
||||
def test_is_domain_error(self):
|
||||
"""QuotaExceededError 继承自 DomainError"""
|
||||
assert issubclass(QuotaExceededError, DomainError)
|
||||
|
||||
def test_can_raise_as_domain_error(self):
|
||||
"""可以作为 DomainError 捕获"""
|
||||
with pytest.raises(DomainError):
|
||||
raise QuotaExceededError("storage", 1024.0, 2048.0)
|
||||
|
||||
def test_stores_dimension_limit_used(self):
|
||||
"""保存 dimension、limit、used 属性"""
|
||||
err = QuotaExceededError("storage_mb", 1024.0, 1500.0)
|
||||
assert err.dimension == "storage_mb"
|
||||
assert err.limit == 1024.0
|
||||
assert err.used == 1500.0
|
||||
|
||||
def test_error_message_format(self):
|
||||
"""异常消息格式正确"""
|
||||
err = QuotaExceededError("storage_mb", 1024.0, 1500.0)
|
||||
msg = str(err)
|
||||
assert "storage_mb" in msg
|
||||
assert "1500.0" in msg
|
||||
assert "1024.0" in msg
|
||||
assert "Quota exceeded" in msg
|
||||
|
||||
def test_integer_values(self):
|
||||
"""整数值也能正常工作"""
|
||||
err = QuotaExceededError("projects", 10, 15)
|
||||
assert err.dimension == "projects"
|
||||
assert err.limit == 10
|
||||
assert err.used == 15
|
||||
|
||||
def test_zero_limit(self):
|
||||
"""限制为 0 时也能正常工作"""
|
||||
err = QuotaExceededError("custom_templates", 0, 1)
|
||||
assert err.limit == 0
|
||||
assert err.used == 1
|
||||
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
"""异常继承关系测试"""
|
||||
|
||||
def test_all_are_domain_errors(self):
|
||||
"""所有异常都可以作为 DomainError 捕获"""
|
||||
errors = [
|
||||
NotFoundError(),
|
||||
ValidationError("bad"),
|
||||
QuotaExceededError("x", 10.0, 20.0),
|
||||
]
|
||||
for err in errors:
|
||||
assert isinstance(err, DomainError)
|
||||
|
||||
def test_distinct_types(self):
|
||||
"""不同异常类型可以区分"""
|
||||
assert not issubclass(NotFoundError, ValidationError)
|
||||
assert not issubclass(ValidationError, QuotaExceededError)
|
||||
assert not issubclass(NotFoundError, QuotaExceededError)
|
||||
Reference in New Issue
Block a user