Spaces:
Paused
Paused
| """Tests for Codex auth — tokens stored in Hermes auth store (~/.hermes/auth.json).""" | |
| import json | |
| import time | |
| import base64 | |
| from pathlib import Path | |
| import pytest | |
| import yaml | |
| from hermes_cli.auth import ( | |
| AuthError, | |
| DEFAULT_CODEX_BASE_URL, | |
| PROVIDER_REGISTRY, | |
| _read_codex_tokens, | |
| _save_codex_tokens, | |
| _write_codex_cli_tokens, | |
| _import_codex_cli_tokens, | |
| get_codex_auth_status, | |
| get_provider_auth_state, | |
| resolve_codex_runtime_credentials, | |
| resolve_provider, | |
| ) | |
| def _setup_hermes_auth(hermes_home: Path, *, access_token: str = "access", refresh_token: str = "refresh"): | |
| """Write Codex tokens into the Hermes auth store.""" | |
| hermes_home.mkdir(parents=True, exist_ok=True) | |
| auth_store = { | |
| "version": 1, | |
| "active_provider": "openai-codex", | |
| "providers": { | |
| "openai-codex": { | |
| "tokens": { | |
| "access_token": access_token, | |
| "refresh_token": refresh_token, | |
| }, | |
| "last_refresh": "2026-02-26T00:00:00Z", | |
| "auth_mode": "chatgpt", | |
| }, | |
| }, | |
| } | |
| auth_file = hermes_home / "auth.json" | |
| auth_file.write_text(json.dumps(auth_store, indent=2)) | |
| return auth_file | |
| def _jwt_with_exp(exp_epoch: int) -> str: | |
| payload = {"exp": exp_epoch} | |
| encoded = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8")).rstrip(b"=").decode("utf-8") | |
| return f"h.{encoded}.s" | |
| def test_read_codex_tokens_success(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| _setup_hermes_auth(hermes_home) | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| data = _read_codex_tokens() | |
| assert data["tokens"]["access_token"] == "access" | |
| assert data["tokens"]["refresh_token"] == "refresh" | |
| def test_read_codex_tokens_missing(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| hermes_home.mkdir(parents=True, exist_ok=True) | |
| # Empty auth store | |
| (hermes_home / "auth.json").write_text(json.dumps({"version": 1, "providers": {}})) | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| with pytest.raises(AuthError) as exc: | |
| _read_codex_tokens() | |
| assert exc.value.code == "codex_auth_missing" | |
| def test_resolve_codex_runtime_credentials_missing_access_token(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| _setup_hermes_auth(hermes_home, access_token="") | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| with pytest.raises(AuthError) as exc: | |
| resolve_codex_runtime_credentials() | |
| assert exc.value.code == "codex_auth_missing_access_token" | |
| assert exc.value.relogin_required is True | |
| def test_resolve_codex_runtime_credentials_refreshes_expiring_token(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| expiring_token = _jwt_with_exp(int(time.time()) - 10) | |
| _setup_hermes_auth(hermes_home, access_token=expiring_token, refresh_token="refresh-old") | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| called = {"count": 0} | |
| def _fake_refresh(tokens, timeout_seconds): | |
| called["count"] += 1 | |
| return {"access_token": "access-new", "refresh_token": "refresh-new"} | |
| monkeypatch.setattr("hermes_cli.auth._refresh_codex_auth_tokens", _fake_refresh) | |
| resolved = resolve_codex_runtime_credentials() | |
| assert called["count"] == 1 | |
| assert resolved["api_key"] == "access-new" | |
| def test_resolve_codex_runtime_credentials_force_refresh(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| _setup_hermes_auth(hermes_home, access_token="access-current", refresh_token="refresh-old") | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| called = {"count": 0} | |
| def _fake_refresh(tokens, timeout_seconds): | |
| called["count"] += 1 | |
| return {"access_token": "access-forced", "refresh_token": "refresh-new"} | |
| monkeypatch.setattr("hermes_cli.auth._refresh_codex_auth_tokens", _fake_refresh) | |
| resolved = resolve_codex_runtime_credentials(force_refresh=True, refresh_if_expiring=False) | |
| assert called["count"] == 1 | |
| assert resolved["api_key"] == "access-forced" | |
| def test_resolve_provider_explicit_codex_does_not_fallback(monkeypatch): | |
| monkeypatch.delenv("OPENAI_API_KEY", raising=False) | |
| monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) | |
| assert resolve_provider("openai-codex") == "openai-codex" | |
| def test_save_codex_tokens_roundtrip(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| hermes_home.mkdir(parents=True, exist_ok=True) | |
| (hermes_home / "auth.json").write_text(json.dumps({"version": 1, "providers": {}})) | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| _save_codex_tokens({"access_token": "at123", "refresh_token": "rt456"}) | |
| data = _read_codex_tokens() | |
| assert data["tokens"]["access_token"] == "at123" | |
| assert data["tokens"]["refresh_token"] == "rt456" | |
| def test_import_codex_cli_tokens(tmp_path, monkeypatch): | |
| codex_home = tmp_path / "codex-cli" | |
| codex_home.mkdir(parents=True, exist_ok=True) | |
| (codex_home / "auth.json").write_text(json.dumps({ | |
| "tokens": {"access_token": "cli-at", "refresh_token": "cli-rt"}, | |
| })) | |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) | |
| tokens = _import_codex_cli_tokens() | |
| assert tokens is not None | |
| assert tokens["access_token"] == "cli-at" | |
| assert tokens["refresh_token"] == "cli-rt" | |
| def test_import_codex_cli_tokens_missing(tmp_path, monkeypatch): | |
| monkeypatch.setenv("CODEX_HOME", str(tmp_path / "nonexistent")) | |
| assert _import_codex_cli_tokens() is None | |
| def test_codex_tokens_not_written_to_shared_file(tmp_path, monkeypatch): | |
| """Verify _save_codex_tokens writes only to Hermes auth store, not ~/.codex/.""" | |
| hermes_home = tmp_path / "hermes" | |
| codex_home = tmp_path / "codex-cli" | |
| hermes_home.mkdir(parents=True, exist_ok=True) | |
| codex_home.mkdir(parents=True, exist_ok=True) | |
| (hermes_home / "auth.json").write_text(json.dumps({"version": 1, "providers": {}})) | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) | |
| _save_codex_tokens({"access_token": "hermes-at", "refresh_token": "hermes-rt"}) | |
| # ~/.codex/auth.json should NOT exist — _save_codex_tokens only touches Hermes store | |
| assert not (codex_home / "auth.json").exists() | |
| # Hermes auth store should have the tokens | |
| data = _read_codex_tokens() | |
| assert data["tokens"]["access_token"] == "hermes-at" | |
| def test_write_codex_cli_tokens_creates_file(tmp_path, monkeypatch): | |
| """_write_codex_cli_tokens creates ~/.codex/auth.json with refreshed tokens.""" | |
| codex_home = tmp_path / "codex-cli" | |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) | |
| _write_codex_cli_tokens("new-access", "new-refresh", last_refresh="2026-04-12T00:00:00Z") | |
| auth_path = codex_home / "auth.json" | |
| assert auth_path.exists() | |
| data = json.loads(auth_path.read_text()) | |
| assert data["tokens"]["access_token"] == "new-access" | |
| assert data["tokens"]["refresh_token"] == "new-refresh" | |
| assert data["last_refresh"] == "2026-04-12T00:00:00Z" | |
| # Verify file permissions are restricted | |
| assert (auth_path.stat().st_mode & 0o777) == 0o600 | |
| def test_write_codex_cli_tokens_preserves_existing(tmp_path, monkeypatch): | |
| """_write_codex_cli_tokens preserves extra fields in existing auth.json.""" | |
| codex_home = tmp_path / "codex-cli" | |
| codex_home.mkdir(parents=True, exist_ok=True) | |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) | |
| existing = { | |
| "tokens": { | |
| "access_token": "old-access", | |
| "refresh_token": "old-refresh", | |
| "extra_field": "preserved", | |
| }, | |
| "last_refresh": "2026-01-01T00:00:00Z", | |
| "custom_key": "keep_me", | |
| } | |
| (codex_home / "auth.json").write_text(json.dumps(existing)) | |
| _write_codex_cli_tokens("updated-access", "updated-refresh") | |
| data = json.loads((codex_home / "auth.json").read_text()) | |
| assert data["tokens"]["access_token"] == "updated-access" | |
| assert data["tokens"]["refresh_token"] == "updated-refresh" | |
| assert data["tokens"]["extra_field"] == "preserved" | |
| assert data["custom_key"] == "keep_me" | |
| # last_refresh not updated since we didn't pass it | |
| assert data["last_refresh"] == "2026-01-01T00:00:00Z" | |
| def test_write_codex_cli_tokens_handles_missing_dir(tmp_path, monkeypatch): | |
| """_write_codex_cli_tokens creates parent directories if missing.""" | |
| codex_home = tmp_path / "does" / "not" / "exist" | |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) | |
| _write_codex_cli_tokens("at", "rt") | |
| assert (codex_home / "auth.json").exists() | |
| data = json.loads((codex_home / "auth.json").read_text()) | |
| assert data["tokens"]["access_token"] == "at" | |
| def test_refresh_codex_auth_tokens_writes_back_to_cli(tmp_path, monkeypatch): | |
| """After refreshing, _refresh_codex_auth_tokens writes back to ~/.codex/auth.json.""" | |
| from hermes_cli.auth import _refresh_codex_auth_tokens | |
| hermes_home = tmp_path / "hermes" | |
| codex_home = tmp_path / "codex-cli" | |
| hermes_home.mkdir(parents=True, exist_ok=True) | |
| codex_home.mkdir(parents=True, exist_ok=True) | |
| (hermes_home / "auth.json").write_text(json.dumps({"version": 1, "providers": {}})) | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) | |
| # Write initial CLI tokens | |
| (codex_home / "auth.json").write_text(json.dumps({ | |
| "tokens": {"access_token": "old-at", "refresh_token": "old-rt"}, | |
| })) | |
| # Mock the pure refresh to return new tokens | |
| monkeypatch.setattr("hermes_cli.auth.refresh_codex_oauth_pure", lambda *a, **kw: { | |
| "access_token": "refreshed-at", | |
| "refresh_token": "refreshed-rt", | |
| "last_refresh": "2026-04-12T01:00:00Z", | |
| }) | |
| _refresh_codex_auth_tokens( | |
| {"access_token": "old-at", "refresh_token": "old-rt"}, | |
| timeout_seconds=10, | |
| ) | |
| # Verify CLI file was updated | |
| cli_data = json.loads((codex_home / "auth.json").read_text()) | |
| assert cli_data["tokens"]["access_token"] == "refreshed-at" | |
| assert cli_data["tokens"]["refresh_token"] == "refreshed-rt" | |
| def test_resolve_returns_hermes_auth_store_source(tmp_path, monkeypatch): | |
| hermes_home = tmp_path / "hermes" | |
| _setup_hermes_auth(hermes_home) | |
| monkeypatch.setenv("HERMES_HOME", str(hermes_home)) | |
| creds = resolve_codex_runtime_credentials() | |
| assert creds["source"] == "hermes-auth-store" | |
| assert creds["provider"] == "openai-codex" | |
| assert creds["base_url"] == DEFAULT_CODEX_BASE_URL | |