You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

88 lines
2.8 KiB

import pytest
from types import SimpleNamespace
from voice_transcriptor.services.credentials import CredentialError, CredentialService
class FakeKeyring:
def __init__(self) -> None:
self.credential = None
self.calls: list[tuple[str, str, str | None]] = []
def get_credential(self, service: str, account: str | None):
self.calls.append(("get", service, account))
return self.credential
def set_password(self, service: str, account: str, value: str) -> None:
self.calls.append(("set", service, account, value))
self.credential = SimpleNamespace(username=account, password=value)
def test_get_api_key_uses_existing_windows_credential_target() -> None:
backend = FakeKeyring()
backend.credential = SimpleNamespace(username="stored-user", password="secret")
assert CredentialService(backend).get_api_key() == "secret"
assert backend.calls == [("get", "OPENAI_API_KEY", None)]
def test_has_api_key_reflects_keyring_value() -> None:
backend = FakeKeyring()
service = CredentialService(backend)
assert service.has_api_key() is False
backend.credential = SimpleNamespace(username="stored-user", password="secret")
assert service.has_api_key() is True
def test_set_api_key_rejects_blank_values() -> None:
backend = FakeKeyring()
with pytest.raises(CredentialError, match="API key"):
CredentialService(backend).set_api_key(" \t")
assert backend.calls == []
def test_set_api_key_persists_nonblank_value() -> None:
backend = FakeKeyring()
backend.credential = SimpleNamespace(username="stored-user", password="old")
CredentialService(backend).set_api_key("secret")
assert backend.calls == [
("get", "OPENAI_API_KEY", None),
("set", "OPENAI_API_KEY", "stored-user", "secret"),
]
def test_set_api_key_creates_target_with_stable_username_when_missing() -> None:
backend = FakeKeyring()
CredentialService(backend).set_api_key("secret")
assert backend.calls == [
("get", "OPENAI_API_KEY", None),
("set", "OPENAI_API_KEY", "OPENAI_API_KEY", "secret"),
]
@pytest.mark.parametrize("method", ["get_credential", "set_password"])
def test_backend_failures_are_sanitized(method: str) -> None:
class FailingKeyring:
def get_credential(self, service: str, account: str | None):
raise RuntimeError("backend leaked secret")
def set_password(self, service: str, account: str, value: str) -> None:
raise RuntimeError("backend leaked secret")
service = CredentialService(FailingKeyring())
with pytest.raises(CredentialError) as caught:
if method == "get_credential":
service.get_api_key()
else:
service.set_api_key("secret")
assert "secret" not in str(caught.value).lower()
assert caught.value.__cause__ is None