Files
xiaoxia-saas/tests/unit/test_title_library_use_cases.py
T
xiaoxia 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
style: apply isort formatting to pass CI validation (#129)
2026-06-30 18:17:44 +08:00

493 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
标题库(Title LibraryUse 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 == []