from __future__ import annotations import os from dotenv import dotenv_values, set_key import keyring from keyring.errors import KeyringError, NoKeyringError, PasswordDeleteError import pytest from vibe.core.config import ProviderConfig from vibe.core.paths import GLOBAL_ENV_FILE from vibe.core.types import Backend from vibe.setup.auth.api_key_persistence import persist_api_key, remove_api_key def _provider(*, api_key_env_var: str = "CUSTOM_API_KEY") -> ProviderConfig: # Backend.GENERIC keeps onboarding telemetry out of these unit tests. return ProviderConfig( name="custom", api_base="https://custom.example/v1", api_key_env_var=api_key_env_var, backend=Backend.GENERIC, ) def test_persist_stores_in_keyring_and_clears_stale_env( monkeypatch: pytest.MonkeyPatch, ) -> None: stored: dict[str, str] = {} monkeypatch.delenv("CUSTOM_API_KEY", raising=False) monkeypatch.setattr( keyring, "set_password", lambda service, username, password: stored.__setitem__(username, password), ) # A stale plaintext copy that should be dropped after the keyring write. GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True) set_key(GLOBAL_ENV_FILE.path, "CUSTOM_API_KEY", "old-key") result = persist_api_key(_provider(), "new-key") assert result == "completed" assert stored == {"CUSTOM_API_KEY": "new-key"} assert os.environ["CUSTOM_API_KEY"] == "new-key" assert "CUSTOM_API_KEY" not in dotenv_values(GLOBAL_ENV_FILE.path) def test_persist_falls_back_to_env_when_keyring_unavailable( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("CUSTOM_API_KEY", raising=False) def _unavailable(service: str, username: str, password: str) -> None: raise KeyringError("no keyring") monkeypatch.setattr(keyring, "set_password", _unavailable) result = persist_api_key(_provider(), "new-key") assert result == "completed" assert os.environ["CUSTOM_API_KEY"] == "new-key" assert dotenv_values(GLOBAL_ENV_FILE.path)["CUSTOM_API_KEY"] == "new-key" def test_persist_returns_env_var_error_for_empty_env_var( monkeypatch: pytest.MonkeyPatch, ) -> None: def _fail(service: str, username: str, password: str) -> None: raise AssertionError("keyring should not be used for an empty env var") monkeypatch.setattr(keyring, "set_password", _fail) result = persist_api_key(_provider(api_key_env_var=""), "new-key") assert result == "env_var_error:" def test_remove_deletes_keyring_env_and_process_env( monkeypatch: pytest.MonkeyPatch, ) -> None: deleted: list[str] = [] monkeypatch.setenv("CUSTOM_API_KEY", "live-key") monkeypatch.setattr( keyring, "delete_password", lambda service, username: deleted.append(username) ) GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True) set_key(GLOBAL_ENV_FILE.path, "CUSTOM_API_KEY", "file-key") remove_api_key(_provider()) assert deleted == ["CUSTOM_API_KEY"] assert "CUSTOM_API_KEY" not in dotenv_values(GLOBAL_ENV_FILE.path) assert "CUSTOM_API_KEY" not in os.environ def test_remove_ignores_keyring_unavailable_and_still_clears_env( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CUSTOM_API_KEY", "live-key") def _unavailable(service: str, username: str) -> None: raise NoKeyringError("no keyring") monkeypatch.setattr(keyring, "delete_password", _unavailable) GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True) set_key(GLOBAL_ENV_FILE.path, "CUSTOM_API_KEY", "file-key") remove_api_key(_provider()) assert "CUSTOM_API_KEY" not in dotenv_values(GLOBAL_ENV_FILE.path) assert "CUSTOM_API_KEY" not in os.environ def test_remove_ignores_missing_keyring_entry_and_still_clears_env( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CUSTOM_API_KEY", "live-key") def _missing(service: str, username: str) -> None: raise PasswordDeleteError("not found") monkeypatch.setattr(keyring, "delete_password", _missing) GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True) set_key(GLOBAL_ENV_FILE.path, "CUSTOM_API_KEY", "file-key") remove_api_key(_provider()) assert "CUSTOM_API_KEY" not in dotenv_values(GLOBAL_ENV_FILE.path) assert "CUSTOM_API_KEY" not in os.environ def test_remove_surfaces_keyring_operation_error_but_still_clears_env( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("CUSTOM_API_KEY", "live-key") def _failed(service: str, username: str) -> None: raise KeyringError("delete failed") monkeypatch.setattr(keyring, "delete_password", _failed) GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True) set_key(GLOBAL_ENV_FILE.path, "CUSTOM_API_KEY", "file-key") with pytest.raises(KeyringError, match="delete failed"): remove_api_key(_provider()) assert "CUSTOM_API_KEY" not in dotenv_values(GLOBAL_ENV_FILE.path) assert "CUSTOM_API_KEY" not in os.environ