ForcePilot/backend/test/unit/external_systems/conftest.py

158 lines
4.9 KiB
Python
Raw Normal View History

"""external_systems 单元测试共享 fixture。
本文件仅放置跨多个测试文件复用的 fixture 与工厂函数单文件独有的 helper
应保留在对应测试文件内避免过度集中 testing-guidelines.md
"""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock
import pytest
from yuxi.external_systems.core.models import (
Environment,
ExternalSystem,
ExternalTool,
)
# ─── 工厂函数 ─────────────────────────────────────────────────────────────
@pytest.fixture
def make_external_tool() -> type[ExternalTool]:
"""返回 ExternalTool 工厂类型,调用时按需覆盖字段。
用法::
tool = make_external_tool(slug="my-tool", adapter_type="http")
"""
def _build(**overrides: Any) -> ExternalTool:
defaults: dict[str, Any] = {
"slug": "test-tool",
"name": "Test Tool",
"description": "A tool for testing",
"adapter_type": "http",
"auth_type": "none",
"timeout": 30,
"enabled": True,
}
defaults.update(overrides)
return ExternalTool(**defaults)
return _build
@pytest.fixture
def make_external_system() -> type[ExternalSystem]:
"""返回 ExternalSystem 工厂类型,调用时按需覆盖字段。"""
def _build(**overrides: Any) -> ExternalSystem:
defaults: dict[str, Any] = {
"slug": "test-system",
"name": "Test System",
"description": "A system for testing",
"adapter_type": "http",
"auth_type": "none",
"enabled": True,
}
defaults.update(overrides)
return ExternalSystem(**defaults)
return _build
@pytest.fixture
def make_environment() -> type[Environment]:
"""返回 Environment 工厂类型。"""
def _build(**overrides: Any) -> Environment:
defaults: dict[str, Any] = {
"system_id": 1,
"env_key": "default",
"name": "Default Env",
"is_default": False,
"enabled": True,
}
defaults.update(overrides)
return Environment(**defaults)
return _build
# ─── Stub 端口实现 ─────────────────────────────────────────────────────────
class StubCryptoService:
"""内存版 CryptoService 桩,用于 webhook_verifier / secret_rotation_scheduler 等测试。
- encrypt_text / decrypt_text 互为逆操作基于 base64
- mask 系列方法返回固定掩码占位
"""
def __init__(self) -> None:
self._store: dict[str, str] = {}
def encrypt_text(self, plaintext: str) -> str:
import base64
encoded = base64.b64encode(plaintext.encode()).decode()
self._store[encoded] = plaintext
return encoded
def decrypt_text(self, ciphertext: str) -> str:
import base64
if ciphertext in self._store:
return self._store[ciphertext]
return base64.b64decode(ciphertext.encode()).decode()
def encrypt_sensitive_fields(self, data: dict[str, Any], sensitive_keys: set[str]) -> dict[str, Any]:
result = dict(data)
for key in sensitive_keys:
if key in result and isinstance(result[key], str):
result[key] = self.encrypt_text(result[key])
return result
def decrypt_sensitive_fields(self, data: dict[str, Any], sensitive_keys: set[str]) -> dict[str, Any]:
result = dict(data)
for key in sensitive_keys:
if key in result and isinstance(result[key], str):
result[key] = self.decrypt_text(result[key])
return result
def mask_sensitive_fields(self, data: dict[str, Any], sensitive_keys: set[str]) -> dict[str, Any]:
result = dict(data)
for key in sensitive_keys:
if key in result:
result[key] = "***"
return result
def mask_all_string_values(self, data: dict[str, Any]) -> dict[str, Any]:
return {k: "***" if isinstance(v, str) else v for k, v in data.items()}
@staticmethod
def is_sensitive_key(key: str) -> bool:
return key.lower() in {"password", "secret", "token", "api_key"}
@staticmethod
def sensitive_keywords() -> list[str]:
return ["password", "secret", "token", "api_key"]
@pytest.fixture
def stub_crypto() -> StubCryptoService:
"""提供内存版 CryptoService 桩。"""
return StubCryptoService()
@pytest.fixture
def stub_secret_resolver() -> AsyncMock:
"""提供 SecretResolver 异步桩resolve 返回原始引用值。"""
resolver = AsyncMock()
resolver.resolve = AsyncMock(side_effect=lambda ref: ref)
return resolver