Files
wehub-resource-sync 0418dc5cf9
CI / Python tests (3.10) (push) Has been cancelled
CI / Python tests (3.11) (push) Has been cancelled
CI / Python quality (push) Has been cancelled
CI / Frontend typecheck (push) Has been cancelled
Autopilot Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:45:00 +08:00

654 lines
22 KiB
Python

from __future__ import annotations
import base64
import json
import urllib.error
from pathlib import Path
import pytest
from typer.testing import CliRunner
from openharness.auth.external import (
CLAUDE_PROVIDER,
CODEX_PROVIDER,
ExternalAuthState,
describe_external_binding,
default_binding_for_provider,
get_claude_code_version,
load_external_credential,
refresh_claude_oauth_credential,
)
from openharness.auth.storage import ExternalAuthBinding, load_external_binding, store_external_binding
from openharness.cli import app
from openharness.config.settings import Settings, load_settings
def _b64url(data: dict[str, object]) -> str:
raw = json.dumps(data, separators=(",", ":")).encode("utf-8")
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def _fake_jwt(payload: dict[str, object]) -> str:
return f"{_b64url({'alg': 'none', 'typ': 'JWT'})}.{_b64url(payload)}.sig"
def test_load_codex_external_credential(monkeypatch, tmp_path: Path):
codex_home = tmp_path / "codex-home"
codex_home.mkdir()
token = _fake_jwt(
{
"exp": 4_102_444_800,
"https://api.openai.com/profile": {"email": "dev@example.com"},
}
)
(codex_home / "auth.json").write_text(
json.dumps(
{
"auth_mode": "chatgpt",
"tokens": {
"access_token": token,
"refresh_token": "refresh-token",
},
}
),
encoding="utf-8",
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
binding = default_binding_for_provider(CODEX_PROVIDER)
credential = load_external_credential(binding)
assert credential.provider == CODEX_PROVIDER
assert credential.auth_kind == "api_key"
assert credential.value == token
assert credential.refresh_token == "refresh-token"
assert credential.profile_label == "dev@example.com"
assert credential.expires_at_ms == 4_102_444_800_000
def test_load_claude_external_credential(monkeypatch, tmp_path: Path):
claude_home = tmp_path / "claude-home"
claude_home.mkdir()
(claude_home / ".credentials.json").write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "claude-access-token",
"refreshToken": "claude-refresh-token",
"expiresAt": 4_102_444_800_000,
}
}
),
encoding="utf-8",
)
monkeypatch.setenv("CLAUDE_HOME", str(claude_home))
monkeypatch.setattr("openharness.auth.external.platform.system", lambda: "Linux")
binding = default_binding_for_provider(CLAUDE_PROVIDER)
credential = load_external_credential(binding)
assert credential.provider == CLAUDE_PROVIDER
assert credential.auth_kind == "auth_token"
assert credential.value == "claude-access-token"
assert credential.refresh_token == "claude-refresh-token"
assert credential.expires_at_ms == 4_102_444_800_000
def test_default_claude_binding_uses_keychain_on_macos(monkeypatch, tmp_path: Path):
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
monkeypatch.delenv("CLAUDE_HOME", raising=False)
monkeypatch.setattr("openharness.auth.external.platform.system", lambda: "Darwin")
monkeypatch.setattr(Path, "home", lambda: tmp_path)
binding = default_binding_for_provider(CLAUDE_PROVIDER)
assert binding.source_kind == "claude_credentials_keychain"
assert binding.source_path == "keychain:Claude Code-credentials"
def test_default_claude_binding_prefers_config_dir_on_macos(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "claude-config"
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(config_dir))
monkeypatch.setattr("openharness.auth.external.platform.system", lambda: "Darwin")
binding = default_binding_for_provider(CLAUDE_PROVIDER)
assert binding.source_kind == "claude_credentials_json"
assert Path(binding.source_path) == config_dir / ".credentials.json"
def test_load_claude_external_credential_from_keychain(monkeypatch, tmp_path: Path):
login_keychain = tmp_path / "login.keychain-db"
def _fake_check_output(args, text=True):
if args == ["security", "find-generic-password", "-w", "-s", "Claude Code-credentials"]:
return json.dumps(
{
"claudeAiOauth": {
"accessToken": "claude-access-token",
"refreshToken": "claude-refresh-token",
"expiresAt": 4_102_444_800_000,
}
}
)
if args == ["security", "find-generic-password", "-s", "Claude Code-credentials"]:
return (
f'keychain: "{login_keychain}"\n'
'attributes:\n'
' "acct"<blob>="yanchundong"\n'
' "svce"<blob>="Claude Code-credentials"\n'
)
raise AssertionError(args)
monkeypatch.setattr("openharness.auth.external.subprocess.check_output", _fake_check_output)
credential = load_external_credential(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path="keychain:Claude Code-credentials",
source_kind="claude_credentials_keychain",
managed_by="claude-cli",
profile_label="Claude CLI",
)
)
assert credential.provider == CLAUDE_PROVIDER
assert credential.auth_kind == "auth_token"
assert credential.value == "claude-access-token"
assert credential.refresh_token == "claude-refresh-token"
assert credential.expires_at_ms == 4_102_444_800_000
assert credential.source_path == login_keychain
assert credential.profile_label == "yanchundong"
def test_settings_resolve_auth_uses_external_binding(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "config"
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
source = tmp_path / "claude-credentials.json"
source.write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "bound-claude-token",
"refreshToken": "bound-claude-refresh",
"expiresAt": 4_102_444_800_000,
}
}
),
encoding="utf-8",
)
store_external_binding(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path=str(source),
source_kind="claude_credentials_json",
managed_by="claude-cli",
profile_label="Claude CLI",
)
)
resolved = Settings(active_profile="claude-subscription").resolve_auth()
assert resolved.auth_kind == "auth_token"
assert resolved.value == "bound-claude-token"
assert str(source) in resolved.source
def test_settings_resolve_auth_refreshes_expired_external_binding(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "config"
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
source = tmp_path / "claude-credentials.json"
source.write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "expired-token",
"refreshToken": "refresh-token",
"expiresAt": 1,
}
}
),
encoding="utf-8",
)
monkeypatch.setattr(
"openharness.auth.external.refresh_claude_oauth_credential",
lambda refresh_token: {
"access_token": "fresh-token",
"refresh_token": refresh_token,
"expires_at_ms": 4_102_444_800_000,
},
)
store_external_binding(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path=str(source),
source_kind="claude_credentials_json",
managed_by="claude-cli",
profile_label="Claude CLI",
)
)
resolved = Settings(active_profile="claude-subscription").resolve_auth()
assert resolved.value == "fresh-token"
persisted = json.loads(source.read_text(encoding="utf-8"))
assert persisted["claudeAiOauth"]["accessToken"] == "fresh-token"
assert persisted["claudeAiOauth"]["refreshToken"] == "refresh-token"
def test_cli_codex_login_binds_without_switching(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "config"
codex_home = tmp_path / "codex-home"
config_dir.mkdir()
codex_home.mkdir()
token = _fake_jwt({"exp": 4_102_444_800})
(codex_home / "auth.json").write_text(
json.dumps(
{
"auth_mode": "chatgpt",
"tokens": {
"access_token": token,
"refresh_token": "refresh-token",
},
}
),
encoding="utf-8",
)
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
monkeypatch.setenv("CODEX_HOME", str(codex_home))
# Prevent env var leakage from overriding the configured api_key
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
(config_dir / "settings.json").write_text(
json.dumps(
{
"api_format": "openai",
"provider": "openai",
"model": "kimi-k2.5",
"base_url": "https://api.moonshot.cn/anthropic",
"api_key": "stale-key",
}
),
encoding="utf-8",
)
runner = CliRunner()
result = runner.invoke(app, ["auth", "codex-login"])
assert result.exit_code == 0
settings = load_settings()
assert settings.active_profile != "codex"
assert settings.provider == "openai"
assert settings.base_url == "https://api.moonshot.cn/anthropic"
assert settings.api_key == "stale-key"
assert "Use `oh provider use codex` to activate it." in result.stdout
binding = load_external_binding(CODEX_PROVIDER)
assert binding is not None
assert Path(binding.source_path) == codex_home / "auth.json"
def test_cli_claude_login_binds_without_switching(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "config"
claude_home = tmp_path / "claude-home"
claude_home.mkdir()
(claude_home / ".credentials.json").write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "claude-access-token",
"refreshToken": "claude-refresh-token",
"expiresAt": 4_102_444_800_000,
}
}
),
encoding="utf-8",
)
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
monkeypatch.setenv("CLAUDE_HOME", str(claude_home))
monkeypatch.setattr("openharness.auth.external.platform.system", lambda: "Linux")
runner = CliRunner()
result = runner.invoke(app, ["auth", "claude-login"])
assert result.exit_code == 0
settings = load_settings()
assert settings.provider == "anthropic"
assert settings.api_format == "anthropic"
assert settings.active_profile == "claude-api"
assert "Use `oh provider use claude-subscription` to activate it." in result.stdout
binding = load_external_binding(CLAUDE_PROVIDER)
assert binding is not None
assert Path(binding.source_path) == claude_home / ".credentials.json"
def test_cli_claude_login_refreshes_expired_credentials(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "config"
claude_home = tmp_path / "claude-home"
claude_home.mkdir()
source = claude_home / ".credentials.json"
source.write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "expired-token",
"refreshToken": "claude-refresh-token",
"expiresAt": 1,
"scopes": ["user:inference"],
}
}
),
encoding="utf-8",
)
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
monkeypatch.setenv("CLAUDE_HOME", str(claude_home))
monkeypatch.setattr("openharness.auth.external.platform.system", lambda: "Linux")
monkeypatch.setattr(
"openharness.auth.external.refresh_claude_oauth_credential",
lambda refresh_token: {
"access_token": "fresh-token",
"refresh_token": refresh_token,
"expires_at_ms": 4_102_444_800_000,
},
)
runner = CliRunner()
result = runner.invoke(app, ["auth", "claude-login"])
assert result.exit_code == 0
persisted = json.loads(source.read_text(encoding="utf-8"))
assert persisted["claudeAiOauth"]["accessToken"] == "fresh-token"
assert persisted["claudeAiOauth"]["scopes"] == ["user:inference"]
def test_load_claude_external_credential_refreshes_expired_keychain(monkeypatch, tmp_path: Path):
login_keychain = tmp_path / "login.keychain-db"
writes: list[list[str]] = []
def _fake_check_output(args, text=True):
if args == ["security", "find-generic-password", "-w", "-s", "Claude Code-credentials"]:
return json.dumps(
{
"claudeAiOauth": {
"accessToken": "expired-token",
"refreshToken": "refresh-token",
"expiresAt": 1,
"scopes": ["user:inference"],
}
}
)
if args == ["security", "find-generic-password", "-s", "Claude Code-credentials"]:
return (
f'keychain: "{login_keychain}"\n'
'attributes:\n'
' "acct"<blob>="yanchundong"\n'
' "svce"<blob>="Claude Code-credentials"\n'
)
raise AssertionError(args)
def _fake_run(args, check=True, capture_output=True, text=True):
writes.append(args)
return None
monkeypatch.setattr("openharness.auth.external.subprocess.check_output", _fake_check_output)
monkeypatch.setattr("openharness.auth.external.subprocess.run", _fake_run)
monkeypatch.setattr(
"openharness.auth.external.refresh_claude_oauth_credential",
lambda refresh_token: {
"access_token": "fresh-token",
"refresh_token": refresh_token,
"expires_at_ms": 4_102_444_800_000,
},
)
credential = load_external_credential(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path="keychain:Claude Code-credentials",
source_kind="claude_credentials_keychain",
managed_by="claude-cli",
profile_label="Claude CLI",
),
refresh_if_needed=True,
)
assert credential.value == "fresh-token"
assert writes
assert writes[0][:6] == [
"security",
"add-generic-password",
"-U",
"-s",
"Claude Code-credentials",
"-a",
]
assert "yanchundong" in writes[0]
def test_cli_provider_use_activates_codex_profile(monkeypatch, tmp_path: Path):
config_dir = tmp_path / "config"
codex_home = tmp_path / "codex-home"
config_dir.mkdir()
codex_home.mkdir()
token = _fake_jwt({"exp": 4_102_444_800})
(codex_home / "auth.json").write_text(
json.dumps(
{
"auth_mode": "chatgpt",
"tokens": {
"access_token": token,
"refresh_token": "refresh-token",
},
}
),
encoding="utf-8",
)
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
monkeypatch.setenv("CODEX_HOME", str(codex_home))
runner = CliRunner()
assert runner.invoke(app, ["auth", "codex-login"]).exit_code == 0
result = runner.invoke(app, ["provider", "use", "codex"])
assert result.exit_code == 0
settings = load_settings()
assert settings.active_profile == "codex"
assert settings.provider == CODEX_PROVIDER
assert settings.api_format == "openai"
assert settings.base_url is None
assert settings.model == "gpt-5.4"
def test_settings_resolve_auth_rejects_third_party_base_url_for_claude_subscription(
monkeypatch,
tmp_path: Path,
):
config_dir = tmp_path / "config"
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(config_dir))
source = tmp_path / "claude-credentials.json"
source.write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "valid-token",
"refreshToken": "refresh-token",
"expiresAt": 4_102_444_800_000,
}
}
),
encoding="utf-8",
)
store_external_binding(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path=str(source),
source_kind="claude_credentials_json",
managed_by="claude-cli",
profile_label="Claude CLI",
)
)
settings = Settings(active_profile="claude-subscription").model_copy(
update={"base_url": "https://api.moonshot.cn/anthropic"}
).sync_active_profile_from_flat_fields()
with pytest.raises(ValueError, match="third-party"):
settings.resolve_auth()
def test_describe_external_binding_reports_refreshable_claude_token(tmp_path: Path):
source = tmp_path / "claude-credentials.json"
source.write_text(
json.dumps(
{
"claudeAiOauth": {
"accessToken": "expired-token",
"refreshToken": "refresh-token",
"expiresAt": 1,
}
}
),
encoding="utf-8",
)
state = describe_external_binding(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path=str(source),
source_kind="claude_credentials_json",
managed_by="claude-cli",
profile_label="Claude CLI",
)
)
assert state == ExternalAuthState(
configured=True,
state="refreshable",
source="external",
detail=f"expired token can be refreshed from {source}",
)
def test_describe_external_binding_reports_configured_claude_keychain(
monkeypatch, tmp_path: Path
):
login_keychain = tmp_path / "login.keychain-db"
def _fake_check_output(args, text=True):
if args == ["security", "find-generic-password", "-w", "-s", "Claude Code-credentials"]:
return json.dumps(
{
"claudeAiOauth": {
"accessToken": "claude-access-token",
"refreshToken": "claude-refresh-token",
"expiresAt": 4_102_444_800_000,
}
}
)
if args == ["security", "find-generic-password", "-s", "Claude Code-credentials"]:
return (
f'keychain: "{login_keychain}"\n'
'attributes:\n'
' "acct"<blob>="yanchundong"\n'
' "svce"<blob>="Claude Code-credentials"\n'
)
raise AssertionError(args)
monkeypatch.setattr("openharness.auth.external.subprocess.check_output", _fake_check_output)
state = describe_external_binding(
ExternalAuthBinding(
provider=CLAUDE_PROVIDER,
source_path="keychain:Claude Code-credentials",
source_kind="claude_credentials_keychain",
managed_by="claude-cli",
profile_label="Claude CLI",
)
)
assert state == ExternalAuthState(
configured=True,
state="configured",
source="external",
detail=str(login_keychain),
)
def test_refresh_claude_oauth_credential(monkeypatch):
seen: dict[str, object] = {}
class _FakeResponse:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
return json.dumps(
{
"access_token": "fresh-token",
"refresh_token": "fresh-refresh",
"expires_in": 7200,
}
).encode("utf-8")
def _fake_urlopen(request, timeout=10):
seen["timeout"] = timeout
seen["headers"] = dict(request.header_items())
seen["body"] = json.loads(request.data.decode("utf-8"))
return _FakeResponse()
monkeypatch.setattr("openharness.auth.external.urllib.request.urlopen", _fake_urlopen)
monkeypatch.setattr("openharness.auth.external.time.time", lambda: 1000)
refreshed = refresh_claude_oauth_credential("refresh-token")
assert refreshed["access_token"] == "fresh-token"
assert refreshed["refresh_token"] == "fresh-refresh"
assert refreshed["expires_at_ms"] == (1000 * 1000) + (7200 * 1000)
assert seen["timeout"] == 10
assert seen["headers"]["Content-type"] == "application/json"
assert seen["body"]["grant_type"] == "refresh_token"
assert seen["body"]["refresh_token"] == "refresh-token"
assert "user:inference" in seen["body"]["scope"]
def test_refresh_claude_oauth_credential_reports_invalid_grant(monkeypatch):
class _FakeResponse:
def read(self):
return b'{"error":"invalid_grant","error_description":"Refresh token not found or invalid"}'
def close(self):
return None
error = urllib.error.HTTPError(
"https://platform.claude.com/v1/oauth/token",
400,
"Bad Request",
hdrs=None,
fp=_FakeResponse(),
)
monkeypatch.setattr(
"openharness.auth.external.urllib.request.urlopen",
lambda request, timeout=10: (_ for _ in ()).throw(error),
)
with pytest.raises(ValueError, match="claude auth login"):
refresh_claude_oauth_credential("refresh-token")
def test_get_claude_code_version_uses_fallback(monkeypatch):
class _Result:
returncode = 1
stdout = ""
monkeypatch.setattr(
"openharness.auth.external.subprocess.run",
lambda *args, **kwargs: _Result(),
)
monkeypatch.setattr("openharness.auth.external._claude_code_version_cache", None)
assert get_claude_code_version() == "2.1.92"