Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

191 lines
5.9 KiB
Python

from __future__ import annotations
import httpx
import pytest
from anthropic import AuthenticationError as AnthropicAuthError
from openai import AuthenticationError as OpenAIAuthError
from surfaces.cli.wizard.config import PROVIDER_BY_VALUE
from surfaces.cli.wizard.validation import _get_provider_base_url, validate_provider_credentials
@pytest.fixture(autouse=True)
def _preload_sdk_error_classes(monkeypatch) -> None:
"""Pre-populate the module-level ``*AuthError`` globals in validation.py.
``_load_anthropic_client``/``_load_openai_client`` lazily import the SDKs
and override the module-level ``Anthropic``/``OpenAI`` names when their
matching ``*AuthError`` is ``None``. If a test monkeypatches only the
client class (``Anthropic`` / ``OpenAI``), the first call to the loader
re-imports and silently replaces that monkeypatch with the real SDK —
which then hits the real network using any ``ANTHROPIC_API_KEY`` /
``OPENAI_API_KEY`` leaked in from the test environment.
By setting the ``*AuthError`` globals here to the real classes (already
imported at the top of this file), we short-circuit the loader's import
branch and make monkeypatches of ``Anthropic`` / ``OpenAI`` reliable.
"""
monkeypatch.setattr("surfaces.cli.wizard.validation.AnthropicAuthError", AnthropicAuthError)
monkeypatch.setattr("surfaces.cli.wizard.validation.OpenAIAuthError", OpenAIAuthError)
class _FakeAnthropicTextBlock:
type = "text"
def __init__(self, text: str) -> None:
self.text = text
class _FakeAnthropicResponse:
def __init__(self, text: str) -> None:
self.content = [_FakeAnthropicTextBlock(text)]
class _FakeAnthropicMessages:
def __init__(self, result: object) -> None:
self._result = result
def create(self, **_kwargs):
if isinstance(self._result, Exception):
raise self._result
return self._result
class _FakeAnthropicClient:
def __init__(self, result: object) -> None:
self.messages = _FakeAnthropicMessages(result)
class _FakeOpenAIMessage:
def __init__(self, content: str) -> None:
self.content = content
class _FakeOpenAIChoice:
def __init__(self, content: str) -> None:
self.message = _FakeOpenAIMessage(content)
class _FakeOpenAIResponse:
def __init__(self, content: str) -> None:
self.choices = [_FakeOpenAIChoice(content)]
class _FakeOpenAICompletions:
def __init__(self, result: object) -> None:
self._result = result
def create(self, **_kwargs):
if isinstance(self._result, Exception):
raise self._result
return self._result
class _FakeOpenAIChat:
def __init__(self, result: object) -> None:
self.completions = _FakeOpenAICompletions(result)
class _FakeOpenAIClient:
def __init__(self, result: object) -> None:
self.chat = _FakeOpenAIChat(result)
def _request(url: str) -> httpx.Request:
return httpx.Request("POST", url)
def test_validate_provider_credentials_returns_failure_for_bad_anthropic_key(monkeypatch) -> None:
auth_error = AnthropicAuthError(
"unauthorized",
response=httpx.Response(401, request=_request("https://api.anthropic.com/v1/messages")),
body=None,
)
monkeypatch.setattr(
"surfaces.cli.wizard.validation.Anthropic",
lambda **_kwargs: _FakeAnthropicClient(auth_error),
)
result = validate_provider_credentials(
provider=PROVIDER_BY_VALUE["anthropic"],
api_key="bad-key",
model="claude-opus-4-5",
)
assert result.ok is False
assert result.detail == "Anthropic rejected the API key."
def test_validate_provider_credentials_returns_success_for_valid_anthropic_key(monkeypatch) -> None:
monkeypatch.setattr(
"surfaces.cli.wizard.validation.Anthropic",
lambda **_kwargs: _FakeAnthropicClient(_FakeAnthropicResponse("OpenSRE ready")),
)
result = validate_provider_credentials(
provider=PROVIDER_BY_VALUE["anthropic"],
api_key="good-key",
model="claude-opus-4-5",
)
assert result.ok is True
assert result.detail == "Anthropic API key validated."
assert result.sample_response == "OpenSRE ready"
def test_validate_provider_credentials_returns_failure_for_bad_openai_key(monkeypatch) -> None:
auth_error = OpenAIAuthError(
"unauthorized",
response=httpx.Response(
401, request=_request("https://api.openai.com/v1/chat/completions")
),
body=None,
)
monkeypatch.setattr(
"surfaces.cli.wizard.validation.OpenAI",
lambda **_kwargs: _FakeOpenAIClient(auth_error),
)
result = validate_provider_credentials(
provider=PROVIDER_BY_VALUE["openai"],
api_key="bad-key",
model="gpt-5-mini",
)
assert result.ok is False
assert result.detail == "OpenAI rejected the API key."
def test_validate_provider_credentials_returns_success_for_valid_openai_key(monkeypatch) -> None:
monkeypatch.setattr(
"surfaces.cli.wizard.validation.OpenAI",
lambda **_kwargs: _FakeOpenAIClient(_FakeOpenAIResponse("OpenSRE ready")),
)
result = validate_provider_credentials(
provider=PROVIDER_BY_VALUE["openai"],
api_key="good-key",
model="gpt-5-mini",
)
assert result.ok is True
assert result.detail == "OpenAI API key validated."
assert result.sample_response == "OpenSRE ready"
def test_get_provider_base_url_deepseek() -> None:
from config.config import DEEPSEEK_BASE_URL
assert _get_provider_base_url("deepseek") == DEEPSEEK_BASE_URL
def test_get_provider_base_url_groq() -> None:
from config.config import GROQ_BASE_URL
assert _get_provider_base_url("groq") == GROQ_BASE_URL
def test_get_provider_base_url_openai_returns_none() -> None:
"""Native OpenAI should return None (uses default base_url)."""
assert _get_provider_base_url("openai") is None