Rework AI-Typewriter en application d'arrière-plan multi-profils
- 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).
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user