mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor(provider-keys): 拆分 keys 端点逻辑并补充配额刷新测试
将 admin keys 接口的创建、更新、查询、删除、导出与配额刷新逻辑迁移到 provider_keys 服务层 新增 auth_type 归一化、重复校验、响应构建与 quota_refresh(codex/kiro/antigravity)模块,并补充对应单元测试
This commit is contained in:
207
tests/services/test_provider_keys_key_command_service.py
Normal file
207
tests/services/test_provider_keys_key_command_service.py
Normal 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
|
||||
Reference in New Issue
Block a user