|
|
|
@ -1,28 +1,29 @@ |
|
|
|
import pytest |
|
|
|
import pytest |
|
|
|
|
|
|
|
from types import SimpleNamespace |
|
|
|
|
|
|
|
|
|
|
|
from voice_transcriptor.services.credentials import CredentialError, CredentialService |
|
|
|
from voice_transcriptor.services.credentials import CredentialError, CredentialService |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakeKeyring: |
|
|
|
class FakeKeyring: |
|
|
|
def __init__(self) -> None: |
|
|
|
def __init__(self) -> None: |
|
|
|
self.value: str | None = None |
|
|
|
self.credential = None |
|
|
|
self.calls: list[tuple[str, str, str | None]] = [] |
|
|
|
self.calls: list[tuple[str, str, str | None]] = [] |
|
|
|
|
|
|
|
|
|
|
|
def get_password(self, service: str, account: str) -> str | None: |
|
|
|
def get_credential(self, service: str, account: str | None): |
|
|
|
self.calls.append(("get", service, account)) |
|
|
|
self.calls.append(("get", service, account)) |
|
|
|
return self.value |
|
|
|
return self.credential |
|
|
|
|
|
|
|
|
|
|
|
def set_password(self, service: str, account: str, value: str) -> None: |
|
|
|
def set_password(self, service: str, account: str, value: str) -> None: |
|
|
|
self.calls.append(("set", service, account, value)) |
|
|
|
self.calls.append(("set", service, account, value)) |
|
|
|
self.value = value |
|
|
|
self.credential = SimpleNamespace(username=account, password=value) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_api_key_uses_stable_keyring_service_and_account() -> None: |
|
|
|
def test_get_api_key_uses_existing_windows_credential_target() -> None: |
|
|
|
backend = FakeKeyring() |
|
|
|
backend = FakeKeyring() |
|
|
|
backend.value = "secret" |
|
|
|
backend.credential = SimpleNamespace(username="stored-user", password="secret") |
|
|
|
|
|
|
|
|
|
|
|
assert CredentialService(backend).get_api_key() == "secret" |
|
|
|
assert CredentialService(backend).get_api_key() == "secret" |
|
|
|
assert backend.calls == [("get", "voice-transcriptor", "openai-api-key")] |
|
|
|
assert backend.calls == [("get", "OPENAI_API_KEY", None)] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_has_api_key_reflects_keyring_value() -> None: |
|
|
|
def test_has_api_key_reflects_keyring_value() -> None: |
|
|
|
@ -30,7 +31,7 @@ def test_has_api_key_reflects_keyring_value() -> None: |
|
|
|
service = CredentialService(backend) |
|
|
|
service = CredentialService(backend) |
|
|
|
|
|
|
|
|
|
|
|
assert service.has_api_key() is False |
|
|
|
assert service.has_api_key() is False |
|
|
|
backend.value = "secret" |
|
|
|
backend.credential = SimpleNamespace(username="stored-user", password="secret") |
|
|
|
assert service.has_api_key() is True |
|
|
|
assert service.has_api_key() is True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -45,16 +46,31 @@ def test_set_api_key_rejects_blank_values() -> None: |
|
|
|
|
|
|
|
|
|
|
|
def test_set_api_key_persists_nonblank_value() -> None: |
|
|
|
def test_set_api_key_persists_nonblank_value() -> None: |
|
|
|
backend = FakeKeyring() |
|
|
|
backend = FakeKeyring() |
|
|
|
|
|
|
|
backend.credential = SimpleNamespace(username="stored-user", password="old") |
|
|
|
|
|
|
|
|
|
|
|
CredentialService(backend).set_api_key("secret") |
|
|
|
CredentialService(backend).set_api_key("secret") |
|
|
|
|
|
|
|
|
|
|
|
assert backend.calls == [("set", "voice-transcriptor", "openai-api-key", "secret")] |
|
|
|
assert backend.calls == [ |
|
|
|
|
|
|
|
("get", "OPENAI_API_KEY", None), |
|
|
|
|
|
|
|
("set", "OPENAI_API_KEY", "stored-user", "secret"), |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("method", ["get_password", "set_password"]) |
|
|
|
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: |
|
|
|
def test_backend_failures_are_sanitized(method: str) -> None: |
|
|
|
class FailingKeyring: |
|
|
|
class FailingKeyring: |
|
|
|
def get_password(self, service: str, account: str) -> str | None: |
|
|
|
def get_credential(self, service: str, account: str | None): |
|
|
|
raise RuntimeError("backend leaked secret") |
|
|
|
raise RuntimeError("backend leaked secret") |
|
|
|
|
|
|
|
|
|
|
|
def set_password(self, service: str, account: str, value: str) -> None: |
|
|
|
def set_password(self, service: str, account: str, value: str) -> None: |
|
|
|
@ -63,7 +79,7 @@ def test_backend_failures_are_sanitized(method: str) -> None: |
|
|
|
service = CredentialService(FailingKeyring()) |
|
|
|
service = CredentialService(FailingKeyring()) |
|
|
|
|
|
|
|
|
|
|
|
with pytest.raises(CredentialError) as caught: |
|
|
|
with pytest.raises(CredentialError) as caught: |
|
|
|
if method == "get_password": |
|
|
|
if method == "get_credential": |
|
|
|
service.get_api_key() |
|
|
|
service.get_api_key() |
|
|
|
else: |
|
|
|
else: |
|
|
|
service.set_api_key("secret") |
|
|
|
service.set_api_key("secret") |
|
|
|
|