mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* fix(scripts): validate Claude Keychain export credentials * fix(scripts): reuse validated Keychain access token
122 lines
4.3 KiB
Python
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
|