Files
iAOP/core/llm-gateway/tests/test_cloudapi.py
T

132 lines
5.0 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""云端 API 推理后端 + 安全网关测试(issue #45)。
覆盖:
1. 参数化与配置资产(provider/endpoint/model/api_key_env);
2. API Key 从环境变量读取(不硬编码、health 不泄露密钥);
3. dry-run(未配置 endpoint)占位输出;
4. OpenAI 兼容调用:Bearer 认证头 + mock 响应提取;
5. 安全网关:safety_checker 拒绝 → 拦截占位、不调用上游;
6. 坏响应 → RuntimeError。
"""
import os
import sys
import unittest
from unittest import mock
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import _bootstrap # noqa: F401
from llm_gateway.backends import CloudApiBackend # noqa: E402
CONFIG = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"config", "cloudapi.template.yaml",
)
class TestParameterization(unittest.TestCase):
"""参数化与配置资产。"""
def test_config_asset_parses(self):
import yaml
with open(CONFIG, "r", encoding="utf-8") as fh:
cfg = yaml.safe_load(fh)["cloudapi"]
b = CloudApiBackend(**{k: v for k, v in cfg.items() if k != "provider"})
self.assertEqual(b.endpoint, "https://api.deepseek.com/v1")
self.assertEqual(b.model, "deepseek-chat")
self.assertEqual(b.api_key_env, "DEEPSEEK_API_KEY")
def test_api_key_from_env(self):
with mock.patch.dict(os.environ, {"MY_KEY": "sk-secret-123"}):
b = CloudApiBackend(endpoint="http://x/v1", api_key_env="MY_KEY")
self.assertEqual(b._api_key, "sk-secret-123")
def test_health_does_not_leak_key(self):
with mock.patch.dict(os.environ, {"K": "sk-secret-123"}):
b = CloudApiBackend(api_key_env="K")
health = b.health()
self.assertEqual(health["api_key_configured"], True)
self.assertNotIn("sk-secret", str(health))
def test_name(self):
self.assertEqual(CloudApiBackend().name, "cloud-api")
class TestDryRun(unittest.TestCase):
"""未配置 endpoint:占位输出。"""
def setUp(self):
self.b = CloudApiBackend()
def test_dry_run_placeholder(self):
out = self.b.generate("海绵钛是什么", ["公开资料"])
self.assertIn("[云端API占位]", out)
self.assertIn("[来源: 公开资料]", out)
class TestOpenAICompat(unittest.TestCase):
"""OpenAI 兼容调用(Bearer 认证)。"""
def setUp(self):
with mock.patch.dict(os.environ, {"DK": "sk-abc"}):
self.b = CloudApiBackend(
endpoint="https://api.deepseek.com/v1", api_key_env="DK")
def test_generate_with_bearer_auth(self):
fake = {"choices": [{"message": {"content": "海绵钛是钛的一种形态"}}]}
with mock.patch.object(self.b, "_post_json", return_value=fake) as post:
out = self.b.generate("海绵钛是什么", ["公开资料"])
path, payload = post.call_args[0]
self.assertEqual(path, "/v1/chat/completions")
self.assertEqual(payload["model"], "deepseek-chat")
self.assertEqual(out, "海绵钛是钛的一种形态")
def test_bearer_header_sent(self):
# 验证 _post_json 实际携带 Authorization: Bearer
opened = []
def fake_urlopen(req, timeout=None):
opened.append(req)
resp = mock.MagicMock()
resp.read.return_value = b'{"choices":[]}'
cm = mock.MagicMock()
cm.__enter__.return_value = resp
return cm
with mock.patch("urllib.request.urlopen", side_effect=fake_urlopen):
self.b._post_json("/v1/chat/completions", {"model": "x"})
self.assertEqual(opened[0].get_header("Authorization"), "Bearer sk-abc")
def test_bad_response_raises(self):
with mock.patch.object(self.b, "_post_json", return_value={}):
with self.assertRaises(RuntimeError):
self.b.generate("x", [])
class TestSafetyGateway(unittest.TestCase):
"""安全网关:safety_checker 复查(fail-closed)。"""
def test_checker_reject_blocks(self):
b = CloudApiBackend(endpoint="http://x/v1",
safety_checker=lambda p: "脱敏" in p)
with mock.patch.object(b, "_post_json") as post:
out = b.generate("含敏感内容", [])
post.assert_not_called() # 不调用上游
self.assertIn("拦截", out)
def test_checker_allow_passes(self):
b = CloudApiBackend(endpoint="http://x/v1",
safety_checker=lambda p: "脱敏" in p)
fake = {"choices": [{"message": {"content": "ok"}}]}
with mock.patch.object(b, "_post_json", return_value=fake) as post:
out = b.generate("脱敏后的公开问题", [])
post.assert_called_once()
self.assertEqual(out, "ok")
def test_health_reports_safety(self):
b = CloudApiBackend(safety_checker=lambda p: True)
self.assertTrue(b.health()["safety_checker"])
if __name__ == "__main__":
unittest.main()