1217d8cef0
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 210h35m44s
CI/CD Pipeline / Frontend Lint (push) Failing after 210h36m11s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 210h36m17s
493 lines
17 KiB
Python
493 lines
17 KiB
Python
"""
|
||
标题库(Title Library)Use Case 回归测试
|
||
|
||
测试目标:
|
||
1. CreateTitleLibraryUseCase - 创建标题库条目
|
||
2. UpdateTitleLibraryUseCase - 更新标题库条目
|
||
3. 配额逻辑覆盖 - titles: free=50, basic=500, premium=500
|
||
4. 边界条件与异常场景
|
||
"""
|
||
|
||
from unittest.mock import Mock
|
||
|
||
import pytest
|
||
|
||
from packages.application.title_library.commands import (
|
||
CreateTitleLibraryCommand,
|
||
UpdateTitleLibraryCommand,
|
||
)
|
||
from packages.application.title_library.use_cases import (
|
||
CreateTitleLibraryUseCase,
|
||
DeleteTitleLibraryUseCase,
|
||
GetTitleLibraryUseCase,
|
||
ListTitleLibraryUseCase,
|
||
NotFoundError,
|
||
QuotaExceededError,
|
||
UpdateTitleLibraryUseCase,
|
||
)
|
||
from packages.domain.title_library import TitleLibraryItem
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_repo():
|
||
"""创建 Mock 仓储"""
|
||
repo = Mock()
|
||
repo.count_by_user = Mock(return_value=0)
|
||
repo.create = Mock(side_effect=lambda item: item)
|
||
repo.update = Mock(side_effect=lambda item: item)
|
||
repo.get = Mock(return_value=None)
|
||
repo.delete = Mock(return_value=True)
|
||
repo.list_by_user = Mock(return_value=[])
|
||
return repo
|
||
|
||
|
||
@pytest.fixture
|
||
def create_use_case(mock_repo):
|
||
return CreateTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
|
||
@pytest.fixture
|
||
def update_use_case(mock_repo):
|
||
return UpdateTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_create_command():
|
||
"""标准创建命令"""
|
||
return CreateTitleLibraryCommand(
|
||
user_id="user-001",
|
||
name="测试标题",
|
||
text="这是一个测试标题文本",
|
||
category="新闻",
|
||
description="用于测试的标题",
|
||
tags=["测试", "新闻"],
|
||
metadata_={"source": "unit_test"},
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def existing_title_item():
|
||
"""模拟已存在的标题条目"""
|
||
return TitleLibraryItem(
|
||
id="existing-title-001",
|
||
user_id="user-001",
|
||
name="旧标题",
|
||
text="旧文本",
|
||
category="旧分类",
|
||
description="旧描述",
|
||
tags=["旧"],
|
||
is_active=True,
|
||
metadata_={},
|
||
)
|
||
|
||
|
||
# ===========================================================================
|
||
# 1. CreateTitleLibraryUseCase 测试
|
||
# ===========================================================================
|
||
|
||
|
||
class TestCreateTitleLibraryUseCase:
|
||
"""标题库创建 UseCase 测试"""
|
||
|
||
def test_create_success_all_fields(self, create_use_case, mock_repo, sample_create_command):
|
||
"""测试创建成功 - 所有字段完整传入"""
|
||
result = create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
assert result is not None
|
||
assert result.user_id == "user-001"
|
||
assert result.name == "测试标题"
|
||
assert result.text == "这是一个测试标题文本"
|
||
assert result.category == "新闻"
|
||
assert result.description == "用于测试的标题"
|
||
assert result.tags == ["测试", "新闻"]
|
||
assert result.metadata_ == {"source": "unit_test"}
|
||
|
||
mock_repo.count_by_user.assert_called_once_with("user-001")
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_create_generates_uuid(self, create_use_case, mock_repo, sample_create_command):
|
||
"""测试创建时自动生成 UUID 作为 id"""
|
||
result = create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
assert result.id is not None
|
||
assert len(result.id) == 32 # uuid4().hex 长度为 32
|
||
assert result.id.isalnum()
|
||
|
||
def test_create_default_values(self, create_use_case, mock_repo):
|
||
"""测试默认值填充"""
|
||
command = CreateTitleLibraryCommand(
|
||
user_id="user-001",
|
||
name="最小化创建",
|
||
text="文本",
|
||
)
|
||
|
||
result = create_use_case.execute(command, plan_name="free")
|
||
|
||
assert result.category == "default"
|
||
assert result.description == ""
|
||
assert result.tags == []
|
||
assert result.metadata_ == {}
|
||
|
||
|
||
# ===========================================================================
|
||
# 2. 配额逻辑测试(titles: free=50, basic=500, premium=500)
|
||
# ===========================================================================
|
||
|
||
|
||
class TestCreateTitleLibraryQuota:
|
||
"""标题库创建配额检查测试"""
|
||
|
||
def test_quota_free_plan_under_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""free 套餐(上限50),当前 25 个,允许创建"""
|
||
mock_repo.count_by_user.return_value = 25
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
assert result is not None
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_quota_free_plan_at_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""free 套餐(上限50),当前 50 个,拒绝创建"""
|
||
mock_repo.count_by_user.return_value = 50
|
||
|
||
with pytest.raises(QuotaExceededError) as exc_info:
|
||
create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
assert exc_info.value.dimension == "max_titles"
|
||
assert exc_info.value.limit == 50
|
||
assert exc_info.value.used == 50
|
||
mock_repo.create.assert_not_called()
|
||
|
||
def test_quota_free_plan_just_under_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""free 套餐(上限50),当前 49 个,允许创建(边界)"""
|
||
mock_repo.count_by_user.return_value = 49
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
assert result is not None
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_quota_free_plan_over_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""free 套餐(上限50),当前 60 个,拒绝创建"""
|
||
mock_repo.count_by_user.return_value = 60
|
||
|
||
with pytest.raises(QuotaExceededError) as exc_info:
|
||
create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
assert exc_info.value.dimension == "max_titles"
|
||
assert exc_info.value.limit == 50
|
||
assert exc_info.value.used == 60
|
||
|
||
def test_quota_basic_plan_under_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""basic 套餐(上限500),当前 200 个,允许创建"""
|
||
mock_repo.count_by_user.return_value = 200
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name="basic")
|
||
|
||
assert result is not None
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_quota_basic_plan_at_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""basic 套餐(上限500),当前 500 个,拒绝创建"""
|
||
mock_repo.count_by_user.return_value = 500
|
||
|
||
with pytest.raises(QuotaExceededError) as exc_info:
|
||
create_use_case.execute(sample_create_command, plan_name="basic")
|
||
|
||
assert exc_info.value.dimension == "max_titles"
|
||
assert exc_info.value.limit == 500
|
||
assert exc_info.value.used == 500
|
||
|
||
def test_quota_basic_plan_just_under_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""basic 套餐(上限500),当前 499 个,允许创建(边界)"""
|
||
mock_repo.count_by_user.return_value = 499
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name="basic")
|
||
|
||
assert result is not None
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_quota_premium_plan_under_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""premium 套餐(上限500),当前 250 个,允许创建"""
|
||
mock_repo.count_by_user.return_value = 250
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name="premium")
|
||
|
||
assert result is not None
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_quota_premium_plan_at_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""premium 套餐(上限500),当前 500 个,拒绝创建"""
|
||
mock_repo.count_by_user.return_value = 500
|
||
|
||
with pytest.raises(QuotaExceededError) as exc_info:
|
||
create_use_case.execute(sample_create_command, plan_name="premium")
|
||
|
||
assert exc_info.value.dimension == "max_titles"
|
||
assert exc_info.value.limit == 500
|
||
assert exc_info.value.used == 500
|
||
|
||
def test_quota_premium_plan_just_under_limit(self, create_use_case, mock_repo, sample_create_command):
|
||
"""premium 套餐(上限500),当前 499 个,允许创建(边界)"""
|
||
mock_repo.count_by_user.return_value = 499
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name="premium")
|
||
|
||
assert result is not None
|
||
mock_repo.create.assert_called_once()
|
||
|
||
def test_quota_zero_usage_all_plans(self, create_use_case, mock_repo, sample_create_command):
|
||
"""新用户零使用量,所有套餐均可创建"""
|
||
mock_repo.count_by_user.return_value = 0
|
||
|
||
for plan in ["free", "basic", "premium"]:
|
||
mock_repo.create.reset_mock()
|
||
mock_repo.count_by_user.reset_mock()
|
||
mock_repo.count_by_user.return_value = 0
|
||
|
||
result = create_use_case.execute(sample_create_command, plan_name=plan)
|
||
assert result is not None, f"{plan} 套餐零使用量应允许创建"
|
||
|
||
def test_quota_unknown_plan_defaults_to_zero(self, create_use_case, mock_repo, sample_create_command):
|
||
"""未知套餐名默认配额为 0,无法创建"""
|
||
mock_repo.count_by_user.return_value = 0
|
||
|
||
with pytest.raises(QuotaExceededError):
|
||
create_use_case.execute(sample_create_command, plan_name="unknown_plan")
|
||
|
||
def test_quota_exceeded_error_attributes(self, create_use_case, mock_repo, sample_create_command):
|
||
"""QuotaExceededError 异常属性完整性"""
|
||
mock_repo.count_by_user.return_value = 50
|
||
|
||
with pytest.raises(QuotaExceededError) as exc_info:
|
||
create_use_case.execute(sample_create_command, plan_name="free")
|
||
|
||
err = exc_info.value
|
||
assert hasattr(err, "dimension")
|
||
assert hasattr(err, "limit")
|
||
assert hasattr(err, "used")
|
||
assert "max_titles" in str(err)
|
||
assert "50" in str(err)
|
||
|
||
|
||
# ===========================================================================
|
||
# 3. UpdateTitleLibraryUseCase 测试
|
||
# ===========================================================================
|
||
|
||
|
||
class TestUpdateTitleLibraryUseCase:
|
||
"""标题库更新 UseCase 测试"""
|
||
|
||
def test_update_success_all_fields(self, update_use_case, mock_repo, existing_title_item):
|
||
"""测试全字段更新成功"""
|
||
mock_repo.get.return_value = existing_title_item
|
||
|
||
command = UpdateTitleLibraryCommand(
|
||
title_id="existing-title-001",
|
||
user_id="user-001",
|
||
name="更新后标题",
|
||
text="更新后文本",
|
||
category="新分类",
|
||
description="新描述",
|
||
tags=["新标签"],
|
||
is_active=False,
|
||
metadata_={"updated": True},
|
||
)
|
||
|
||
result = update_use_case.execute(command)
|
||
|
||
assert result.name == "更新后标题"
|
||
assert result.text == "更新后文本"
|
||
assert result.category == "新分类"
|
||
assert result.description == "新描述"
|
||
assert result.tags == ["新标签"]
|
||
assert result.is_active is False
|
||
assert result.metadata_ == {"updated": True}
|
||
|
||
mock_repo.update.assert_called_once()
|
||
|
||
def test_update_partial_only_name(self, update_use_case, mock_repo, existing_title_item):
|
||
"""测试仅更新 name"""
|
||
mock_repo.get.return_value = existing_title_item
|
||
|
||
command = UpdateTitleLibraryCommand(
|
||
title_id="existing-title-001",
|
||
user_id="user-001",
|
||
name="仅改名",
|
||
)
|
||
|
||
result = update_use_case.execute(command)
|
||
|
||
assert result.name == "仅改名"
|
||
# 其他字段保持不变
|
||
assert result.text == "旧文本"
|
||
assert result.category == "旧分类"
|
||
assert result.description == "旧描述"
|
||
|
||
def test_update_partial_only_is_active(self, update_use_case, mock_repo, existing_title_item):
|
||
"""测试仅更新 is_active(软删除/恢复)"""
|
||
mock_repo.get.return_value = existing_title_item
|
||
|
||
command = UpdateTitleLibraryCommand(
|
||
title_id="existing-title-001",
|
||
user_id="user-001",
|
||
is_active=False,
|
||
)
|
||
|
||
result = update_use_case.execute(command)
|
||
|
||
assert result.is_active is False
|
||
assert result.name == "旧标题" # 其他字段不变
|
||
|
||
def test_update_not_found(self, update_use_case, mock_repo):
|
||
"""测试更新不存在的条目"""
|
||
mock_repo.get.return_value = None
|
||
|
||
command = UpdateTitleLibraryCommand(
|
||
title_id="nonexistent-id",
|
||
user_id="user-001",
|
||
name="不存在",
|
||
)
|
||
|
||
with pytest.raises(NotFoundError, match="nonexistent-id"):
|
||
update_use_case.execute(command)
|
||
|
||
mock_repo.update.assert_not_called()
|
||
|
||
def test_update_wrong_user(self, update_use_case, mock_repo):
|
||
"""测试用户隔离"""
|
||
mock_repo.get.return_value = None
|
||
|
||
command = UpdateTitleLibraryCommand(
|
||
title_id="existing-title-001",
|
||
user_id="other-user-999",
|
||
name="恶意修改",
|
||
)
|
||
|
||
with pytest.raises(NotFoundError):
|
||
update_use_case.execute(command)
|
||
|
||
def test_update_none_fields_not_changed(self, update_use_case, mock_repo, existing_title_item):
|
||
"""测试 None 字段不覆盖原有值"""
|
||
mock_repo.get.return_value = existing_title_item
|
||
|
||
command = UpdateTitleLibraryCommand(
|
||
title_id="existing-title-001",
|
||
user_id="user-001",
|
||
)
|
||
|
||
result = update_use_case.execute(command)
|
||
|
||
assert result.name == "旧标题"
|
||
assert result.text == "旧文本"
|
||
assert result.category == "旧分类"
|
||
assert result.is_active is True
|
||
|
||
|
||
# ===========================================================================
|
||
# 4. DeleteTitleLibraryUseCase 测试
|
||
# ===========================================================================
|
||
|
||
|
||
class TestDeleteTitleLibraryUseCase:
|
||
"""标题库删除 UseCase 测试"""
|
||
|
||
def test_delete_success(self, mock_repo):
|
||
"""测试删除成功"""
|
||
mock_repo.delete.return_value = True
|
||
use_case = DeleteTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
result = use_case.execute("title-001", "user-001")
|
||
|
||
assert result is True
|
||
mock_repo.delete.assert_called_once_with("title-001", "user-001")
|
||
|
||
def test_delete_not_found(self, mock_repo):
|
||
"""测试删除不存在的条目"""
|
||
mock_repo.delete.return_value = False
|
||
use_case = DeleteTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
result = use_case.execute("nonexistent", "user-001")
|
||
|
||
assert result is False
|
||
|
||
|
||
# ===========================================================================
|
||
# 5. GetTitleLibraryUseCase 测试
|
||
# ===========================================================================
|
||
|
||
|
||
class TestGetTitleLibraryUseCase:
|
||
"""标题库查询 UseCase 测试"""
|
||
|
||
def test_get_existing(self, mock_repo):
|
||
"""测试查询存在的条目"""
|
||
expected = TitleLibraryItem(
|
||
id="t-001",
|
||
user_id="user-001",
|
||
name="测试",
|
||
text="文本",
|
||
)
|
||
mock_repo.get.return_value = expected
|
||
use_case = GetTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
result = use_case.execute("t-001", "user-001")
|
||
|
||
assert result is not None
|
||
assert result.id == "t-001"
|
||
mock_repo.get.assert_called_once_with("t-001", "user-001")
|
||
|
||
def test_get_not_found(self, mock_repo):
|
||
"""测试查询不存在的条目"""
|
||
mock_repo.get.return_value = None
|
||
use_case = GetTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
result = use_case.execute("nonexistent", "user-001")
|
||
|
||
assert result is None
|
||
|
||
|
||
# ===========================================================================
|
||
# 6. ListTitleLibraryUseCase 测试
|
||
# ===========================================================================
|
||
|
||
|
||
class TestListTitleLibraryUseCase:
|
||
"""标题库列表 UseCase 测试"""
|
||
|
||
def test_list_default(self, mock_repo):
|
||
"""测试默认列表查询"""
|
||
items = [
|
||
TitleLibraryItem(id="t1", user_id="user-001", name="A", text="a"),
|
||
TitleLibraryItem(id="t2", user_id="user-001", name="B", text="b"),
|
||
]
|
||
mock_repo.list_by_user.return_value = items
|
||
use_case = ListTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
result = use_case.execute("user-001")
|
||
|
||
assert len(result) == 2
|
||
mock_repo.list_by_user.assert_called_once_with("user-001", category=None, skip=0, limit=50)
|
||
|
||
def test_list_with_category_filter(self, mock_repo):
|
||
"""测试按分类筛选"""
|
||
mock_repo.list_by_user.return_value = []
|
||
use_case = ListTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
use_case.execute("user-001", category="新闻", skip=5, limit=10)
|
||
|
||
mock_repo.list_by_user.assert_called_once_with("user-001", category="新闻", skip=5, limit=10)
|
||
|
||
def test_list_empty(self, mock_repo):
|
||
"""测试空列表"""
|
||
mock_repo.list_by_user.return_value = []
|
||
use_case = ListTitleLibraryUseCase(repository=mock_repo)
|
||
|
||
result = use_case.execute("user-001")
|
||
|
||
assert result == []
|