ForcePilot/backend/test/unit/external_systems/integrations/test_registry.py
Kris 15c1cd95ba test: 新增并完善外部系统单元测试覆盖
- 新增多个集成服务(Slack、Notion、Twilio、Microsoft365等)的元数据、常量、凭证、错误处理相关单元测试
- 新增持久化适配器、工作单元、健康检查仓储的单元测试
- 修复认证插件测试中缺失的凭证类型注册
- 修正密钥轮换调度器的异常类型匹配
- 完善审计日志测试用例,新增创建者/更新者字段校验
- 新增告警触发时持久化通知渠道的测试
- 精简核心模型测试代码,移除冗余的超时测试用例
2026-07-11 05:42:04 +08:00

397 lines
13 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 == []
# ─── 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 == ["http", "grpc"]
@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