Files
openswarm/openswarm-runner/tests/test_router_credentials.py
T

112 lines
4.2 KiB
Python

"""The runner must be structurally unable to rotate a user's OAuth grant.
Every test here exists to make one class of bug unwritable: a refresh token reaching
9Router's db.json. Delete either wall in runner/seed/router_credentials.py and these go red.
"""
import json
import os
import stat
from datetime import datetime, timedelta, timezone
import pytest
from runner.run_spec import InvalidRunSpec, ProviderCredential, RunSpec, load_run_spec
from runner.seed.router_credentials import (
RefreshTokenLeak,
assert_no_refresh_token,
router_db_payload,
write_router_db,
)
NOW = datetime(2026, 7, 31, 12, 0, 0, tzinfo=timezone.utc)
ACCESS_TOKEN = "at-test-value-not-a-real-token"
REFRESH_TOKEN = "rt-test-value-not-a-real-token"
def spec_json(credential: dict) -> str:
return json.dumps({
"run_id": "run-1",
"workflow": {"id": "wf-1", "title": "Test", "steps": [{"text": "say hi"}]},
"credentials": [credential],
})
def oauth_credential() -> ProviderCredential:
return ProviderCredential(
provider="claude",
auth_type="oauth",
access_token=ACCESS_TOKEN,
expires_at=NOW + timedelta(hours=8),
)
@pytest.mark.parametrize("key", ["refreshToken", "refresh_token", "Refresh-Token", "oauthRefreshToken"])
def test_a_spec_carrying_a_refresh_token_never_parses(key: str, tmp_path, monkeypatch) -> None:
payload = {
"provider": "claude",
"auth_type": "oauth",
"access_token": ACCESS_TOKEN,
"expires_at": (NOW + timedelta(hours=8)).isoformat(),
key: REFRESH_TOKEN,
}
monkeypatch.setenv("OPENSWARM_RUN_SPEC", spec_json(payload))
with pytest.raises(InvalidRunSpec) as caught:
load_run_spec()
assert key in str(caught.value)
assert not list(tmp_path.iterdir()), "a rejected spec must not leave anything on disk"
@pytest.mark.parametrize("key", ["refreshToken", "refresh_token", "Refresh-Token", "oauthRefreshToken"])
def test_the_writer_guard_rejects_a_poisoned_payload(key: str) -> None:
payload = {"providerConnections": [{"provider": "claude", key: REFRESH_TOKEN}]}
with pytest.raises(RefreshTokenLeak):
assert_no_refresh_token(payload)
def test_the_guard_passes_a_clean_payload() -> None:
assert_no_refresh_token(router_db_payload([oauth_credential()], NOW))
def test_written_db_carries_the_access_token_and_no_refresh_token(tmp_path) -> None:
path = write_router_db(str(tmp_path / "9router"), [oauth_credential()], NOW)
raw = open(path, "r", encoding="utf-8").read()
# Without this the "no refresh token" assertion below would also pass on an empty file.
assert ACCESS_TOKEN in raw
connection = json.loads(raw)["providerConnections"][0]
assert connection["provider"] == "claude"
assert connection["isActive"] is True
assert connection["expiresAt"] == "2026-07-31T20:00:00.000Z"
assert "refresh" not in raw.lower()
assert not any("refresh" in key.lower() for key in connection)
def test_the_db_and_its_directory_are_owner_only(tmp_path) -> None:
path = write_router_db(str(tmp_path / "9router"), [oauth_credential()], NOW)
assert stat.S_IMODE(os.stat(path).st_mode) == 0o600
assert stat.S_IMODE(os.stat(os.path.dirname(path)).st_mode) == 0o700
def test_an_api_key_credential_never_reaches_the_router_db(tmp_path) -> None:
credential = ProviderCredential(provider="anthropic", auth_type="api_key", api_key="sk-test-not-real")
path = write_router_db(str(tmp_path / "9router"), [credential], NOW)
assert json.loads(open(path, encoding="utf-8").read())["providerConnections"] == []
def test_an_oauth_credential_without_an_access_token_is_rejected() -> None:
with pytest.raises(ValueError, match="no access_token"):
ProviderCredential(provider="claude", auth_type="oauth", expires_at=NOW)
def test_an_expired_access_token_is_fatal_not_refreshable() -> None:
spec = RunSpec.model_validate_json(spec_json({
"provider": "claude",
"auth_type": "oauth",
"access_token": ACCESS_TOKEN,
"expires_at": (NOW + timedelta(seconds=30)).isoformat(),
}))
assert [credential.provider for credential in spec.expired_credentials(NOW)] == ["claude"]
assert spec.expired_credentials(NOW - timedelta(hours=1)) == []