- 新增多个业务域的__init__.py模块文件,规范包导出结构 - 调整多个DTO文件的导入路径,统一模块组织方式 - 移除测试文件中多余的空行与导入语句 - 优化部分业务模块的包层级划分
405 lines
14 KiB
Python
405 lines
14 KiB
Python
"""integrations/registry.py 厂商集成目录注册表单元测试。
|
||
|
||
覆盖 register / filter_integrations / list_integrations / count_integrations /
|
||
get_integration / get_integration_or_raise / list_adapter_types / clear。
|
||
全局状态(_integrations)通过 _snapshot_registry fixture 在测试后恢复。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from yuxi.external_systems.exceptions import (
|
||
DomainValidationError,
|
||
EntityNotFoundError,
|
||
)
|
||
from yuxi.external_systems.integrations.registry import IntegrationRegistry
|
||
from yuxi.external_systems.integrations.schemas import IntegrationMetadata
|
||
|
||
# ─── 全局状态快照 fixture ─────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.fixture
|
||
def _snapshot_registry():
|
||
"""保存并恢复 _integrations,避免污染其他测试。"""
|
||
saved = dict(IntegrationRegistry._integrations)
|
||
IntegrationRegistry._integrations.clear()
|
||
try:
|
||
yield
|
||
finally:
|
||
IntegrationRegistry._integrations.clear()
|
||
IntegrationRegistry._integrations.update(saved)
|
||
|
||
|
||
# ─── 辅助构造 ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
def _make_metadata(**overrides: Any) -> IntegrationMetadata:
|
||
"""构造测试用 IntegrationMetadata。"""
|
||
base: dict[str, Any] = {
|
||
"key": "test",
|
||
"display_name": "Test Integration",
|
||
"description": "A test integration",
|
||
"adapter_type": "http",
|
||
"source_types": ["test"],
|
||
}
|
||
base.update(overrides)
|
||
return IntegrationMetadata(**base)
|
||
|
||
|
||
# ─── register ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_register_adds_metadata_to_registry(_snapshot_registry):
|
||
# Arrange
|
||
meta = _make_metadata(key="salesforce")
|
||
|
||
# Act
|
||
IntegrationRegistry.register(meta)
|
||
|
||
# Assert
|
||
assert IntegrationRegistry.get_integration("salesforce") is meta
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_register_idempotent_with_same_instance(_snapshot_registry):
|
||
# Arrange
|
||
meta = _make_metadata(key="salesforce")
|
||
IntegrationRegistry.register(meta)
|
||
|
||
# Act - 同一实例重复注册不应抛异常
|
||
IntegrationRegistry.register(meta)
|
||
|
||
# Assert
|
||
assert IntegrationRegistry.get_integration("salesforce") is meta
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_register_conflict_with_different_instance_raises(_snapshot_registry):
|
||
# Arrange
|
||
first = _make_metadata(key="salesforce", display_name="First")
|
||
IntegrationRegistry.register(first)
|
||
second = _make_metadata(key="salesforce", display_name="Second")
|
||
|
||
# Act / Assert - 不同实例重复注册应抛 DomainValidationError
|
||
with pytest.raises(DomainValidationError) as exc_info:
|
||
IntegrationRegistry.register(second)
|
||
assert "salesforce" in str(exc_info.value)
|
||
assert "First" in str(exc_info.value)
|
||
assert "Second" in str(exc_info.value)
|
||
# 原注册项保留
|
||
assert IntegrationRegistry.get_integration("salesforce") is first
|
||
|
||
|
||
# ─── filter_integrations ──────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_returns_all_sorted_by_key(_snapshot_registry):
|
||
# Arrange - 故意乱序注册
|
||
IntegrationRegistry.register(_make_metadata(key="zoho"))
|
||
IntegrationRegistry.register(_make_metadata(key="apple"))
|
||
IntegrationRegistry.register(_make_metadata(key="microsoft"))
|
||
|
||
# Act
|
||
result = IntegrationRegistry.filter_integrations()
|
||
|
||
# Assert - 按 key 升序稳定排序
|
||
assert [it.key for it in result] == ["apple", "microsoft", "zoho"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_by_adapter_type(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf", adapter_type="http"))
|
||
IntegrationRegistry.register(_make_metadata(key="grpc_pkg", adapter_type="grpc"))
|
||
IntegrationRegistry.register(_make_metadata(key="hub", adapter_type="http"))
|
||
|
||
# Act
|
||
result = IntegrationRegistry.filter_integrations(adapter_type="http")
|
||
|
||
# Assert - 精确匹配 adapter_type
|
||
assert [it.key for it in result] == ["hub", "sf"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_by_tags_or_relation(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf", tags=["crm", "sales"]))
|
||
IntegrationRegistry.register(_make_metadata(key="slack", tags=["messaging"]))
|
||
IntegrationRegistry.register(_make_metadata(key="hub", tags=["crm", "marketing"]))
|
||
|
||
# Act - 命中任一标签即返回(OR 关系)
|
||
result = IntegrationRegistry.filter_integrations(tags=["sales", "messaging"])
|
||
|
||
# Assert
|
||
assert [it.key for it in result] == ["sf", "slack"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_by_keyword_case_insensitive(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf", display_name="Salesforce"))
|
||
IntegrationRegistry.register(_make_metadata(key="hub", display_name="HubSpot"))
|
||
|
||
# Act - 大小写不敏感匹配 display_name
|
||
result = IntegrationRegistry.filter_integrations(keyword="SALES")
|
||
|
||
# Assert
|
||
assert [it.key for it in result] == ["sf"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_by_keyword_matches_description(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf", display_name="SF", description="CRM platform"))
|
||
IntegrationRegistry.register(_make_metadata(key="slack", display_name="Slack", description="Chat tool"))
|
||
|
||
# Act - 关键词匹配 description
|
||
result = IntegrationRegistry.filter_integrations(keyword="crm")
|
||
|
||
# Assert
|
||
assert [it.key for it in result] == ["sf"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_combined_filters(_snapshot_registry):
|
||
# Arrange - sf/hub 同时满足三条件,slack 仅满足 adapter_type+keyword 不满足 tags
|
||
IntegrationRegistry.register(
|
||
_make_metadata(key="sf", adapter_type="http", tags=["crm"], display_name="Salesforce CRM")
|
||
)
|
||
IntegrationRegistry.register(
|
||
_make_metadata(key="hub", adapter_type="http", tags=["crm"], display_name="HubSpot CRM")
|
||
)
|
||
IntegrationRegistry.register(
|
||
_make_metadata(key="slack", adapter_type="http", tags=["messaging"], display_name="Slack CRM")
|
||
)
|
||
|
||
# Act - adapter_type + tags + keyword 组合(AND 关系)
|
||
result = IntegrationRegistry.filter_integrations(adapter_type="http", tags=["crm"], keyword="crm")
|
||
|
||
# Assert - 同时满足所有条件:sf/hub 命中,slack 因 tags 不匹配被排除
|
||
assert [it.key for it in result] == ["hub", "sf"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_empty_when_no_match(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
|
||
# Act
|
||
result = IntegrationRegistry.filter_integrations(keyword="nonexistent")
|
||
|
||
# Assert
|
||
assert result == []
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_filterIntegrations_tolerates_none_fields(_snapshot_registry):
|
||
# Arrange - 模拟注册包异常导致字段为空,确保过滤不抛 500
|
||
meta = _make_metadata(key="sf", display_name="Salesforce")
|
||
IntegrationRegistry.register(meta)
|
||
meta.description = None
|
||
meta.tags = None
|
||
|
||
# Act / Assert - keyword/tags 过滤均不应崩溃
|
||
result_by_keyword = IntegrationRegistry.filter_integrations(keyword="sales")
|
||
result_by_tags = IntegrationRegistry.filter_integrations(tags=["crm"])
|
||
|
||
assert [it.key for it in result_by_keyword] == ["sf"]
|
||
assert result_by_tags == []
|
||
|
||
|
||
# ─── list_integrations ────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listIntegrations_paginates_results(_snapshot_registry):
|
||
# Arrange
|
||
for k in ("a", "b", "c", "d", "e"):
|
||
IntegrationRegistry.register(_make_metadata(key=k))
|
||
|
||
# Act - 第 2 页,每页 2 条
|
||
result = IntegrationRegistry.list_integrations(limit=2, offset=2)
|
||
|
||
# Assert
|
||
assert [it.key for it in result] == ["c", "d"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listIntegrations_offset_negative_clamped_to_zero(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
|
||
# Act - offset 为负数应归零
|
||
result = IntegrationRegistry.list_integrations(limit=10, offset=-5)
|
||
|
||
# Assert
|
||
assert [it.key for it in result] == ["sf"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listIntegrations_limit_negative_clamped_to_zero(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
|
||
# Act - limit 为负数应归零,返回空列表
|
||
result = IntegrationRegistry.list_integrations(limit=-1, offset=0)
|
||
|
||
# Assert
|
||
assert result == []
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listIntegrations_offset_beyond_length_returns_empty(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
|
||
# Act - offset 超过总数
|
||
result = IntegrationRegistry.list_integrations(limit=10, offset=100)
|
||
|
||
# Assert
|
||
assert result == []
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listIntegrations_defaults_limit50_offset0(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
|
||
# Act - 不传分页参数
|
||
result = IntegrationRegistry.list_integrations()
|
||
|
||
# Assert
|
||
assert [it.key for it in result] == ["sf"]
|
||
|
||
|
||
# ─── count_integrations ───────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_countIntegrations_returns_total(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
IntegrationRegistry.register(_make_metadata(key="hub"))
|
||
|
||
# Act
|
||
total = IntegrationRegistry.count_integrations()
|
||
|
||
# Assert
|
||
assert total == 2
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_countIntegrations_respects_filters(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf", adapter_type="http"))
|
||
IntegrationRegistry.register(_make_metadata(key="grpc_pkg", adapter_type="grpc"))
|
||
|
||
# Act
|
||
total = IntegrationRegistry.count_integrations(adapter_type="http")
|
||
|
||
# Assert
|
||
assert total == 1
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_countIntegrations_zero_when_no_match(_snapshot_registry):
|
||
# Arrange / Act / Assert
|
||
assert IntegrationRegistry.count_integrations(keyword="nonexistent") == 0
|
||
|
||
|
||
# ─── get_integration ─────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_getIntegration_returns_metadata(_snapshot_registry):
|
||
# Arrange
|
||
meta = _make_metadata(key="salesforce")
|
||
IntegrationRegistry.register(meta)
|
||
|
||
# Act
|
||
result = IntegrationRegistry.get_integration("salesforce")
|
||
|
||
# Assert
|
||
assert result is meta
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_getIntegration_unknown_returns_none(_snapshot_registry):
|
||
# Arrange / Act / Assert
|
||
assert IntegrationRegistry.get_integration("nonexistent") is None
|
||
|
||
|
||
# ─── get_integration_or_raise ──────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_getIntegrationOrRaise_returns_metadata(_snapshot_registry):
|
||
# Arrange
|
||
meta = _make_metadata(key="salesforce")
|
||
IntegrationRegistry.register(meta)
|
||
|
||
# Act
|
||
result = IntegrationRegistry.get_integration_or_raise("salesforce")
|
||
|
||
# Assert
|
||
assert result is meta
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_getIntegrationOrRaise_unknown_raises_entityNotFoundError(_snapshot_registry):
|
||
# Arrange / Act / Assert
|
||
with pytest.raises(EntityNotFoundError) as exc_info:
|
||
IntegrationRegistry.get_integration_or_raise("nonexistent")
|
||
assert "nonexistent" in str(exc_info.value)
|
||
|
||
|
||
# ─── list_adapter_types ───────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listAdapterTypes_returns_dedup_types(_snapshot_registry):
|
||
# Arrange - 多个集成共用 adapter_type
|
||
IntegrationRegistry.register(_make_metadata(key="sf", adapter_type="http"))
|
||
IntegrationRegistry.register(_make_metadata(key="hub", adapter_type="http"))
|
||
IntegrationRegistry.register(_make_metadata(key="grpc_pkg", adapter_type="grpc"))
|
||
|
||
# Act
|
||
types = IntegrationRegistry.list_adapter_types()
|
||
|
||
# Assert - 去重后按字母升序排序返回
|
||
assert types == ["grpc", "http"]
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_listAdapterTypes_empty_when_nothing_registered(_snapshot_registry):
|
||
# Arrange / Act / Assert
|
||
assert IntegrationRegistry.list_adapter_types() == []
|
||
|
||
|
||
# ─── clear ────────────────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_clear_empties_integrations(_snapshot_registry):
|
||
# Arrange
|
||
IntegrationRegistry.register(_make_metadata(key="sf"))
|
||
|
||
# Act
|
||
IntegrationRegistry.clear()
|
||
|
||
# Assert
|
||
assert IntegrationRegistry.list_adapter_types() == []
|
||
assert IntegrationRegistry.get_integration("sf") is None
|
||
assert IntegrationRegistry.count_integrations() == 0
|
||
|
||
|
||
@pytest.mark.unit
|
||
def test_clear_safe_when_already_empty(_snapshot_registry):
|
||
# Arrange / Act / Assert - 空注册表清空不抛异常
|
||
IntegrationRegistry.clear()
|
||
assert IntegrationRegistry.count_integrations() == 0
|