"""integrations/schemas.py 数据结构单元测试。 覆盖 IntegrationMetadata 与 GeneratedToolsDraft 两个 Pydantic BaseModel 的字段声明、默认值、约束校验与 extra="forbid" 行为。 """ from __future__ import annotations from typing import Any import pytest from pydantic import ValidationError from yuxi.external_systems.integrations.schemas import ( GeneratedToolsDraft, IntegrationMetadata, ) # ─── 辅助构造 ───────────────────────────────────────────────────────────── def _metadata_kwargs(**overrides: Any) -> dict[str, Any]: """构造 IntegrationMetadata 最小合法字段,按需覆盖。""" base: dict[str, Any] = { "key": "salesforce", "display_name": "Salesforce", "adapter_type": "http", "source_types": ["salesforce"], } base.update(overrides) return base # ─── IntegrationMetadata 字段声明 ───────────────────────────────────────── @pytest.mark.unit def test_integrationMetadata_fields_matchExpected(): # Arrange / Act meta = IntegrationMetadata( key="hubspot", display_name="HubSpot", description="CRM integration", adapter_type="http", source_types=["hubspot"], supported_auth_types=["oauth2"], tags=["crm", "marketing"], icon="hubspot", docs_url="https://docs.hubspot.com", connection_extra_schema={"type": "object"}, example_config={"host": "api.hubspot.com"}, dependency_note="需要 OAuth2 应用", ) # Assert assert meta.key == "hubspot" assert meta.display_name == "HubSpot" assert meta.description == "CRM integration" assert meta.adapter_type == "http" assert meta.source_types == ["hubspot"] assert meta.supported_auth_types == ["oauth2"] assert meta.tags == ["crm", "marketing"] assert meta.icon == "hubspot" assert meta.docs_url == "https://docs.hubspot.com" assert meta.connection_extra_schema == {"type": "object"} assert meta.example_config == {"host": "api.hubspot.com"} assert meta.dependency_note == "需要 OAuth2 应用" @pytest.mark.unit def test_integrationMetadata_defaults_appliedWhenOptionalOmitted(): # Arrange / Act - 仅传必填字段 meta = IntegrationMetadata(**_metadata_kwargs()) # Assert - 可选字段取默认值 assert meta.description == "" assert meta.supported_auth_types == [] assert meta.tags == [] assert meta.icon is None assert meta.docs_url is None assert meta.connection_extra_schema is None assert meta.example_config is None assert meta.dependency_note is None @pytest.mark.unit def test_integrationMetadata_extraField_forbidden_raises(): # Arrange / Act / Assert with pytest.raises(ValidationError) as exc_info: IntegrationMetadata(**_metadata_kwargs(unknown_field="x")) assert "extra" in str(exc_info.value).lower() # ─── IntegrationMetadata 约束校验 ────────────────────────────────────────── @pytest.mark.unit def test_integrationMetadata_key_empty_raises(): # Arrange / Act / Assert - min_length=1 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(key="")) @pytest.mark.unit def test_integrationMetadata_key_exceeds_maxLength_raises(): # Arrange / Act / Assert - max_length=64 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(key="a" * 65)) @pytest.mark.unit def test_integrationMetadata_key_maxLength_boundary_accepted(): # Arrange / Act - 边界值 64 字符应通过 meta = IntegrationMetadata(**_metadata_kwargs(key="a" * 64)) # Assert assert len(meta.key) == 64 @pytest.mark.unit def test_integrationMetadata_displayName_empty_raises(): # Arrange / Act / Assert - min_length=1 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(display_name="")) @pytest.mark.unit def test_integrationMetadata_displayName_exceeds_maxLength_raises(): # Arrange / Act / Assert - max_length=128 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(display_name="a" * 129)) @pytest.mark.unit def test_integrationMetadata_description_exceeds_maxLength_raises(): # Arrange / Act / Assert - max_length=512 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(description="a" * 513)) @pytest.mark.unit def test_integrationMetadata_adapterType_exceeds_maxLength_raises(): # Arrange / Act / Assert - max_length=32 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(adapter_type="a" * 33)) @pytest.mark.unit def test_integrationMetadata_sourceTypes_empty_raises(): # Arrange / Act / Assert - min_length=1 with pytest.raises(ValidationError): IntegrationMetadata(**_metadata_kwargs(source_types=[])) @pytest.mark.unit def test_integrationMetadata_missing_required_field_raises(): # Arrange / Act / Assert - 缺少 key with pytest.raises(ValidationError): IntegrationMetadata( display_name="x", adapter_type="http", source_types=["x"], ) # ─── GeneratedToolsDraft 字段声明 ───────────────────────────────────────── @pytest.mark.unit def test_generatedToolsDraft_fields_matchExpected(): # Arrange / Act draft = GeneratedToolsDraft( tool_configs=[{"slug": "tool-a"}, {"slug": "tool-b"}], override_existing=True, ) # Assert assert draft.tool_configs == [{"slug": "tool-a"}, {"slug": "tool-b"}] assert draft.override_existing is True @pytest.mark.unit def test_generatedToolsDraft_overrideExisting_defaultsFalse(): # Arrange / Act - 仅传必填字段 draft = GeneratedToolsDraft(tool_configs=[{"slug": "tool-a"}]) # Assert assert draft.override_existing is False @pytest.mark.unit def test_generatedToolsDraft_toolConfigs_required_raises(): # Arrange / Act / Assert - 缺少 tool_configs with pytest.raises(ValidationError): GeneratedToolsDraft() # type: ignore[call-arg] @pytest.mark.unit def test_generatedToolsDraft_extraField_forbidden_raises(): # Arrange / Act / Assert with pytest.raises(ValidationError) as exc_info: GeneratedToolsDraft(tool_configs=[], unknown="x") assert "extra" in str(exc_info.value).lower() @pytest.mark.unit def test_generatedToolsDraft_emptyToolConfigs_accepted(): # Arrange / Act - 空列表合法(无 min_length 约束) draft = GeneratedToolsDraft(tool_configs=[]) # Assert assert draft.tool_configs == []