ForcePilot/backend/test/unit/external_systems/integrations/test_registry.py

407 lines
14 KiB
Python
Raw Normal View History

"""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