test: P3-1 第32波单元测试(bgm_utils/exceptions/asset_libraries) #807

Merged
xiaoxia merged 1 commits from test/unit-test-wave32 into develop 2026-07-24 10:24:53 +08:00
3 changed files with 393 additions and 0 deletions
+157
View File
@@ -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)
+105
View File
@@ -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
+131
View File
@@ -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)