erplibre/test/test_kdbx_manager.py
Mathieu Benoit cfbdd9406e [REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.

--- EN ---

This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.

Assisted-by: Claude Opus 5
2026-09-24 14:40:38 -04:00

133 lines
5.1 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Ouverture du coffre KeePass depuis le CLI.
Signalé à l'usage : une mauvaise saisie affichait une trace `construct` de
quarante lignes, puis `pykeepass.exceptions.CredentialsError`, et tuait le
CLI — `make: *** Error 1`. L'invite disait par ailleurs `enter_password`,
la clé i18n brute, sans nommer ce qu'elle demandait.
Un mot de passe refusé est le cas NORMAL de cette fonction : elle doit le
dire, laisser recommencer, et laisser partir.
"""
import os
import tempfile
import unittest
from unittest.mock import MagicMock, patch
from pykeepass import create_database
from script.todo.kdbx_manager import KdbxManager
class KdbxCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.chemin = os.path.join(self.tmp.name, "coffre.kdbx")
create_database(self.chemin, password="bon")
def tearDown(self):
self.tmp.cleanup()
def _manager(self, mot_de_passe_configure=None):
config = MagicMock()
config.get_config_value.side_effect = lambda keys: (
self.chemin if keys == ["kdbx", "path"] else mot_de_passe_configure
)
return KdbxManager(config)
def _saisies(self, *reponses):
"""Renvoie (résultat, texte affiché) pour une suite de saisies."""
vues = []
with (
patch("getpass.getpass", side_effect=list(reponses)),
patch(
"builtins.print",
# `**k` : un bouchon de `print` doit accepter la signature de
# `print`. Sans lui, ajouter un `flush=True` dans le code
# testé faisait échouer six tests sur une différence qui n'a
# rien à voir avec ce qu'ils vérifient.
side_effect=lambda *a, **k: vues.append(" ".join(map(str, a))),
),
):
resultat = self._manager().get_kdbx()
return resultat, "\n".join(vues)
class TestWrongPasswordIsToldNotRaised(KdbxCase):
def test_three_refusals_give_up_without_raising(self):
resultat, vu = self._saisies("faux1", "faux2", "faux3")
self.assertIsNone(resultat)
self.assertEqual(vu.count("Mot de passe incorrect"), 3)
def test_the_message_names_keepass_not_the_library_error(self):
"""« Invalid credentials » ne dit pas DE QUOI on parle : l'erreur
d'authentification du serveur de courriel a exactement le même
libellé. Le mot « KeePass » est ce qui les distingue."""
_, vu = self._saisies("faux", "")
self.assertIn("KeePass", vu)
self.assertNotIn("CredentialsError", vu)
self.assertNotIn("Traceback", vu)
def test_the_prompt_says_which_vault_it_wants_to_open(self):
"""L'invite affichait `enter_password`, une clé i18n absente de la
table. Elle doit nommer le fichier : plusieurs coffres peuvent
exister, et rien ne disait lequel était demandé."""
_, vu = self._saisies("faux", "")
self.assertIn(self.chemin, vu)
self.assertNotIn("enter_password", vu)
class TestGivingUpIsPossible(KdbxCase):
def test_an_empty_entry_gives_up_immediately(self):
"""Sans porte de sortie, la seule façon de quitter était de tuer le
programme — ce que la trace faisait, mais par accident."""
resultat, vu = self._saisies("")
self.assertIsNone(resultat)
self.assertIn("abandonne", vu)
def test_giving_up_asks_only_once(self):
appels = []
with (
patch(
"getpass.getpass",
side_effect=lambda **k: appels.append(1) or "",
),
patch("builtins.print"),
):
self._manager().get_kdbx()
self.assertEqual(len(appels), 1)
class TestRecoveryAndSuccess(KdbxCase):
def test_a_wrong_try_does_not_prevent_a_later_good_one(self):
"""Le contrôle POSITIF : sans lui, une fonction qui refuserait
TOUJOURS passerait les tests ci-dessus."""
resultat, _ = self._saisies("faux", "bon")
self.assertIsNotNone(resultat)
def test_a_good_password_opens_the_vault_at_once(self):
resultat, _ = self._saisies("bon")
self.assertIsNotNone(resultat)
def test_a_wrong_password_from_the_configuration_is_reported(self):
"""Chemin sans saisie : personne à qui redemander, mais la trace ne
doit pas remonter pour autant."""
vues = []
with patch(
"builtins.print",
# `**k` : un bouchon de `print` doit accepter la signature de
# `print`. Sans lui, ajouter un `flush=True` dans le code
# testé faisait échouer six tests sur une différence qui n'a
# rien à voir avec ce qu'ils vérifient.
side_effect=lambda *a, **k: vues.append(" ".join(map(str, a))),
):
resultat = self._manager("mauvais").get_kdbx()
self.assertIsNone(resultat)
self.assertIn("KeePass", "\n".join(vues))
if __name__ == "__main__":
unittest.main()