- Rollback gestion d'équations : écriture littérale caractère par caractère (suppression de math_format.py et de la normalisation Unicode). - Nouveau profil par défaut 'Mathématiques (LaTeX)' : le modèle émet du LaTeX encadré [EQ]...[/EQ], intercepté par KeyStepper qui déclenche Alt+= en début d'équation et -> en fin. - Config multi-profils dans %APPDATA%/ai-typewriter/config.json (ConfigStore + profils par défaut auto-créés). - Icône de zone de notification (pystray) : Ouvrir les logs (temps réel), Ajouter/Modifier un profil, Gérer l'authentification, Quitter. - Sélecteur de modèles : modèles locaux Ollama + recherche/téléchargement depuis la bibliothèque publique ; fournisseurs tiers (OpenAI, OpenRouter, Gemini, custom). - Clés d'API stockées de façon sécurisée dans le Gestionnaire d'identifiants Windows via keyring (jamais en clair dans config.json). - Tests : 44 tests verts (stepper, profils, clients IA, credentials, catalogue de modèles, moteur).
97 lines
2.7 KiB
Python
97 lines
2.7 KiB
Python
"""Tests du stockage sécurisé (keyring mocké pour éviter tout accès au
|
|
gestionnaire d'identifiants réel de la machine)."""
|
|
|
|
import pytest
|
|
|
|
from ai_typewriter.credentials import CredentialError, SecureStore, get_cred
|
|
|
|
|
|
class FakeKeyring:
|
|
"""Mini stub de keyring en mémoire, exposant la même interface."""
|
|
|
|
_data = {}
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
cls._data = {}
|
|
|
|
@classmethod
|
|
def set_password(cls, service, username, password):
|
|
cls._data[(service, username)] = password
|
|
|
|
@classmethod
|
|
def get_password(cls, service, username):
|
|
return cls._data.get((service, username))
|
|
|
|
@classmethod
|
|
def delete_password(cls, service, username):
|
|
cls._data.pop((service, username), None)
|
|
|
|
|
|
class FakeKeyringErrors:
|
|
class PasswordDeleteError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def test_store_and_get(monkeypatch):
|
|
import ai_typewriter.credentials as cred
|
|
|
|
monkeypatch.setattr(cred, "keyring", FakeKeyring)
|
|
monkeypatch.setattr(cred.keyring, "errors", FakeKeyringErrors, raising=False)
|
|
FakeKeyring.reset()
|
|
|
|
store = SecureStore("test-service")
|
|
store.store("openai", "sk-secret")
|
|
assert store.get("openai") == "sk-secret"
|
|
assert store.has("openai") is True
|
|
|
|
|
|
def test_empty_credential_returns_empty(monkeypatch):
|
|
import ai_typewriter.credentials as cred
|
|
|
|
monkeypatch.setattr(cred, "keyring", FakeKeyring)
|
|
FakeKeyring.reset()
|
|
|
|
store = SecureStore()
|
|
assert store.get("") == ""
|
|
assert store.has("") is False
|
|
|
|
|
|
def test_get_cred_helper_creates_store(monkeypatch):
|
|
import ai_typewriter.credentials as cred
|
|
|
|
monkeypatch.setattr(cred, "keyring", FakeKeyring)
|
|
monkeypatch.setattr(cred.keyring, "errors", FakeKeyringErrors, raising=False)
|
|
FakeKeyring.reset()
|
|
|
|
# via un store passé explicitement
|
|
store = SecureStore("t")
|
|
store.store("k", "v")
|
|
assert get_cred(store, "k") == "v"
|
|
# via None (crée un store par défaut, mais retombe sur service réel) —
|
|
# on vérifie que quelques accesseurs ne plantent pas.
|
|
assert get_cred(None, "") == ""
|
|
|
|
|
|
def test_store_invalid_credential_name(monkeypatch):
|
|
import ai_typewriter.credentials as cred
|
|
|
|
monkeypatch.setattr(cred, "keyring", FakeKeyring)
|
|
FakeKeyring.reset()
|
|
|
|
store = SecureStore()
|
|
with pytest.raises(CredentialError):
|
|
store.store("", "secret")
|
|
|
|
|
|
def test_delete(monkeypatch):
|
|
import ai_typewriter.credentials as cred
|
|
|
|
monkeypatch.setattr(cred, "keyring", FakeKeyring)
|
|
monkeypatch.setattr(cred.keyring, "errors", FakeKeyringErrors, raising=False)
|
|
FakeKeyring.reset()
|
|
|
|
store = SecureStore("t")
|
|
store.store("gemini", "cle")
|
|
store.delete("gemini")
|
|
assert store.has("gemini") is False |