refactor(provider-keys): 拆分 keys 端点逻辑并补充配额刷新测试

将 admin keys 接口的创建、更新、查询、删除、导出与配额刷新逻辑迁移到 provider_keys 服务层

新增 auth_type 归一化、重复校验、响应构建与 quota_refresh(codex/kiro/antigravity)模块,并补充对应单元测试
This commit is contained in:
AAEE86
2026-02-28 14:09:36 +08:00
parent 4b02078b60
commit 08b89b7ef8
18 changed files with 3465 additions and 1368 deletions

View File

@@ -0,0 +1,207 @@
from __future__ import annotations
import sys
import types
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any, cast
import pytest
from src.core.exceptions import InvalidRequestException
from src.models.endpoint_models import EndpointAPIKeyUpdate
async def _noop_invalidate_models_list_cache() -> None:
return None
_fake_models_service_module = types.ModuleType("src.api.base.models_service")
setattr(
_fake_models_service_module, "invalidate_models_list_cache", _noop_invalidate_models_list_cache
)
sys.modules.setdefault("src.api.base.models_service", _fake_models_service_module)
from src.services.provider_keys import key_command_service as command_module
from src.services.provider_keys import key_side_effects as side_effects_module
class _NoQueryDB:
def query(self, *args: Any, **kwargs: Any) -> Any: # pragma: no cover - 防御断言
_ = args, kwargs
raise AssertionError("unexpected query call")
class _FakeQuery:
def __init__(self, key: Any) -> None:
self._key = key
def filter(self, *args: Any, **kwargs: Any) -> _FakeQuery:
_ = args, kwargs
return self
def first(self) -> Any:
return self._key
class _FakeClearOAuthDB:
def __init__(self, key: Any) -> None:
self._key = key
self.commit_count = 0
def query(self, model: Any) -> _FakeQuery:
_ = model
return _FakeQuery(self._key)
def commit(self) -> None:
self.commit_count += 1
def _build_key(**overrides: Any) -> SimpleNamespace:
base: dict[str, Any] = {
"auto_fetch_models": False,
"allowed_models": None,
"model_include_patterns": None,
"model_exclude_patterns": None,
"provider_id": "provider-1",
"auth_type": "api_key",
}
base.update(overrides)
return SimpleNamespace(**base)
def test_prepare_update_payload_auth_type_null_ignored() -> None:
key = _build_key(auth_type="api_key")
key_data = EndpointAPIKeyUpdate.model_validate({"auth_type": None})
prepared = command_module._prepare_update_key_payload(
db=cast(Any, _NoQueryDB()),
key=cast(Any, key),
key_id="key-1",
key_data=key_data,
)
assert "auth_type" not in prepared.update_data
def test_prepare_update_payload_rejects_empty_api_key() -> None:
key = _build_key(auth_type="api_key")
key_data = EndpointAPIKeyUpdate.model_validate({"api_key": " "})
with pytest.raises(InvalidRequestException, match="api_key 不能为空"):
command_module._prepare_update_key_payload(
db=cast(Any, _NoQueryDB()),
key=cast(Any, key),
key_id="key-1",
key_data=key_data,
)
def test_prepare_update_payload_encrypts_empty_auth_config_dict(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = _build_key(auth_type="vertex_ai")
key_data = EndpointAPIKeyUpdate.model_validate({"auth_config": {}})
monkeypatch.setattr(command_module.crypto_service, "encrypt", lambda raw: f"ENC:{raw}")
prepared = command_module._prepare_update_key_payload(
db=cast(Any, _NoQueryDB()),
key=cast(Any, key),
key_id="key-1",
key_data=key_data,
)
assert prepared.update_data["auth_config"] == "ENC:{}"
def test_prepare_update_payload_vertex_to_oauth_clears_auth_config(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = _build_key(auth_type="vertex_ai")
key_data = EndpointAPIKeyUpdate.model_validate({"auth_type": "oauth"})
monkeypatch.setattr(command_module.crypto_service, "encrypt", lambda raw: f"ENC:{raw}")
prepared = command_module._prepare_update_key_payload(
db=cast(Any, _NoQueryDB()),
key=cast(Any, key),
key_id="key-1",
key_data=key_data,
)
assert prepared.update_data["auth_config"] is None
assert prepared.update_data["api_key"] == "ENC:__placeholder__"
def test_clear_oauth_invalid_response_invalidates_caches(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cache_calls: list[tuple[str, str | None]] = []
async def _fake_invalidate_key_cache(key_id: str) -> None:
cache_calls.append(("key", key_id))
async def _fake_invalidate_models_cache() -> None:
cache_calls.append(("models", None))
fake_provider_cache_module = types.ModuleType("src.services.cache.provider_cache")
class _FakeProviderCacheService:
@staticmethod
async def invalidate_provider_api_key_cache(key_id: str) -> None:
await _fake_invalidate_key_cache(key_id)
setattr(fake_provider_cache_module, "ProviderCacheService", _FakeProviderCacheService)
monkeypatch.setitem(
sys.modules, "src.services.cache.provider_cache", fake_provider_cache_module
)
fake_models_service_module = types.ModuleType("src.api.base.models_service")
setattr(
fake_models_service_module, "invalidate_models_list_cache", _fake_invalidate_models_cache
)
monkeypatch.setitem(sys.modules, "src.api.base.models_service", fake_models_service_module)
key = SimpleNamespace(
oauth_invalid_at=datetime.now(timezone.utc),
oauth_invalid_reason="forbidden",
is_active=False,
)
db = _FakeClearOAuthDB(key=key)
result = command_module.clear_oauth_invalid_response(cast(Any, db), key_id="key-1")
assert result["message"] == "已清除 OAuth 失效标记Key 已自动启用"
assert key.oauth_invalid_at is None
assert key.oauth_invalid_reason is None
assert key.is_active is True
assert db.commit_count == 1
assert cache_calls == [("key", "key-1"), ("models", None)]
@pytest.mark.asyncio
async def test_run_delete_key_side_effects_not_skip_disassociate(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
async def _fake_on_key_allowed_models_changed(**kwargs: Any) -> None:
captured.update(kwargs)
from src.services.model import global_model as global_model_module
monkeypatch.setattr(
global_model_module,
"on_key_allowed_models_changed",
_fake_on_key_allowed_models_changed,
)
await side_effects_module.run_delete_key_side_effects(
db=cast(Any, object()),
provider_id="provider-1",
deleted_key_allowed_models=None,
)
assert captured["provider_id"] == "provider-1"
assert "skip_disassociate" not in captured