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