ForcePilot/backend/test/unit/external_systems/integrations/test_registry.py
Kris 08617091dc refactor: 整理项目包结构与导入路径
- 新增多个业务域的__init__.py模块文件,规范包导出结构
- 调整多个DTO文件的导入路径,统一模块组织方式
- 移除测试文件中多余的空行与导入语句
- 优化部分业务模块的包层级划分
2026-07-18 02:04:03 +08:00

405 lines
14 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.

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