deer-flow/backend/tests/test_claude_keychain_export.py
Guo-Yixin 2970aa4323
fix(scripts): reject invalid Claude Keychain credential containers (#6281)
* fix(scripts): validate Claude Keychain export credentials

* fix(scripts): reuse validated Keychain access token
2026-10-05 15:39:32 +08:00

122 lines
4.3 KiB
Python

"""Exercise the manual OAuth exporter with synthetic Keychain responses."""
import importlib.util
import json
import shlex
from argparse import Namespace
from pathlib import Path
from types import SimpleNamespace
import pytest
@pytest.fixture
def exporter(monkeypatch):
script = Path(__file__).resolve().parents[2] / "scripts" / "export_claude_code_oauth.py"
spec = importlib.util.spec_from_file_location("claude_keychain_export", script)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
monkeypatch.setattr(module.platform, "system", lambda: "Darwin")
return module
def mock_keychain(exporter, monkeypatch, payload):
response = SimpleNamespace(returncode=0, stdout=json.dumps(payload), stderr="")
monkeypatch.setattr(exporter, "subprocess", SimpleNamespace(run=lambda *args, **kwargs: response))
INVALID_CONTAINERS = [
None,
[],
"synthetic-secret",
42,
{"claudeAiOauth": None},
{"claudeAiOauth": []},
{"claudeAiOauth": "synthetic-secret"},
{"claudeAiOauth": {"accessToken": None}},
{"claudeAiOauth": {"accessToken": True}},
{"claudeAiOauth": {"accessToken": 42}},
{"claudeAiOauth": {"accessToken": ["synthetic-secret"]}},
{"claudeAiOauth": {"accessToken": {"value": "synthetic-secret"}}},
{"claudeAiOauth": {"accessToken": ""}},
{"claudeAiOauth": {"accessToken": " \t\n"}},
]
@pytest.mark.parametrize("payload", INVALID_CONTAINERS)
@pytest.mark.parametrize("action", ["print_token", "print_export", "write_credentials"])
def test_invalid_keychain_container_fails_without_exporting(exporter, monkeypatch, tmp_path, capsys, payload, action):
mock_keychain(exporter, monkeypatch, payload)
destination = tmp_path / "credentials.json"
args = Namespace(
service="synthetic-service",
account="synthetic-account",
show_target=False,
print_token=False,
print_export=False,
write_credentials=None,
)
setattr(args, action, destination if action == "write_credentials" else True)
monkeypatch.setattr(exporter, "parse_args", lambda: args)
assert exporter.main() == 1
captured = capsys.readouterr()
assert captured.out == ""
assert captured.err == "Claude Code Keychain item did not contain claudeAiOauth.accessToken.\n"
assert "synthetic-secret" not in captured.err
assert not destination.exists()
@pytest.mark.parametrize("token", ["synthetic-token'with-shell-quote", " synthetic-token "])
def test_valid_container_preserves_all_export_modes(exporter, monkeypatch, tmp_path, capsys, token):
payload = {
"claudeAiOauth": {
"accessToken": token,
"refreshToken": "synthetic-refresh",
"expiresAt": 123,
},
"metadata": "preserved",
}
mock_keychain(exporter, monkeypatch, payload)
destination = tmp_path / "credentials.json"
args = Namespace(
service="synthetic-service",
account="synthetic-account",
show_target=False,
print_token=True,
print_export=True,
write_credentials=destination,
)
monkeypatch.setattr(exporter, "parse_args", lambda: args)
assert exporter.main() == 0
captured = capsys.readouterr()
assert captured.out == f"{token}\nexport CLAUDE_CODE_OAUTH_TOKEN={shlex.quote(token)}\n"
assert captured.err == f"Wrote Claude Code credentials to {destination}\n"
assert json.loads(destination.read_text(encoding="utf-8")) == payload
def test_export_uses_validated_token_without_reextracting_container(exporter, monkeypatch, tmp_path, capsys):
token = "synthetic-validated-token"
payload = {"metadata": "opaque-container"}
monkeypatch.setattr(exporter, "load_keychain_container", lambda **kwargs: (payload, token))
destination = tmp_path / "credentials.json"
args = Namespace(
service="synthetic-service",
account="synthetic-account",
show_target=False,
print_token=True,
print_export=True,
write_credentials=destination,
)
monkeypatch.setattr(exporter, "parse_args", lambda: args)
assert exporter.main() == 0
captured = capsys.readouterr()
assert captured.out == f"{token}\nexport CLAUDE_CODE_OAUTH_TOKEN={shlex.quote(token)}\n"
assert captured.err == f"Wrote Claude Code credentials to {destination}\n"
assert json.loads(destination.read_text(encoding="utf-8")) == payload