新增覆盖认证插件、运行时工具、数据库适配器、API路由、业务用例的完整单元与集成测试,包含: 1. 各认证插件的属性校验与异常场景测试 2. 运行时工具类的格式、唯一性与边界测试 3. gRPC/HTTP错误映射器测试 4. 通知解析、枚举类单元测试 5. 回收站、仪表盘API集成测试 6. 全局共享fixture与桩实现
158 lines
4.9 KiB
Python
158 lines
4.9 KiB
Python
"""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
|