erplibre/test/test_dms_access_repair.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

182 lines
6.6 KiB
Python

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Ce qui doit tenir avant de toucher à une base de production.
Deux propriétés portent tout le reste. La première : sans `--apply`,
RIEN ne s'écrit — un outil de diagnostic qui répare tout seul est un
piège. La seconde : le compte de visibilité doit être fait avec un vrai
utilisateur, jamais en sudo, car la règle globale d'OCA DMS ne s'applique
pas au super-utilisateur et un compte en sudo dirait « tout va bien »
alors que personne ne voit rien.
"""
import ast
import io
import os
import sys
import unittest
from contextlib import redirect_stdout
sys.path.append(
os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
)
from script.odoo.migration import dms_access_repair as repair # noqa: E402
from script.todo import todo_i18n # noqa: E402
class TestTheGeneratedScript(unittest.TestCase):
def test_the_dry_run_never_creates_anything(self):
# Le garde vit dans le script POUSSÉ : c'est lui qui décide, pas
# l'appelant. Un `DRY = True` qui n'entoure pas le `create` ne
# protège de rien.
code = repair.build_script(dry_run=True)
self.assertIn("DRY = True", code)
arbre = ast.parse(code)
creations = [
n
for n in ast.walk(arbre)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Attribute)
and n.func.attr == "create"
]
self.assertTrue(creations, "aucune création dans le script ?")
for noeud in creations:
self.assertTrue(
self._under_dry_guard(arbre, noeud),
"une création hors du garde `not DRY`",
)
@staticmethod
def _under_dry_guard(arbre, cible):
"""La création est-elle sous un `if not DRY ...` ?"""
for noeud in ast.walk(arbre):
if not isinstance(noeud, ast.If):
continue
texte = ast.dump(noeud.test)
if "DRY" not in texte:
continue
for enfant in ast.walk(noeud):
if enfant is cible:
return True
return False
def test_apply_lifts_the_guard(self):
self.assertIn("DRY = False", repair.build_script(dry_run=False))
def test_the_script_is_valid_python(self):
for dry in (True, False):
ast.parse(repair.build_script(dry_run=dry))
def test_visibility_is_measured_with_a_real_user(self):
# `with_user` est le point tout entier : en sudo, la règle globale
# ne s'applique pas et le rapport serait rassurant à tort.
code = repair.build_script(dry_run=True)
self.assertIn("with_user(temoin)", code)
self.assertIn("u.id != 1", code)
def test_it_reuses_the_shared_sentinels(self):
from script.odoo.migration import database_cleanup
self.assertEqual(repair.DEBUT, database_cleanup.START)
self.assertEqual(repair.FIN, database_cleanup.END)
class TestTheReport(unittest.TestCase):
PLEIN = {
"files": 69,
"directories": 16,
"access_groups_before": 0,
"witness": "marie@example.org",
"before": {"directories": 0, "files": 0},
"roots": ["CNESST", "Contrat"],
"already_repaired": False,
}
def test_it_shows_what_exists_and_what_is_seen(self):
texte = "\n".join(repair.render(self.PLEIN, dry_run=True))
self.assertIn("69", texte)
self.assertIn("marie@example.org", texte)
def test_a_dry_run_says_nothing_was_written(self):
texte = "\n".join(repair.render(self.PLEIN, dry_run=True))
self.assertIn(
todo_i18n.t("Nothing written. Re-run with --apply."), texte
)
def test_an_applied_run_reports_the_new_visibility(self):
rapport = dict(self.PLEIN, after={"directories": 16, "files": 69})
texte = "\n".join(repair.render(rapport, dry_run=False))
self.assertIn(todo_i18n.t("Now visible to"), texte)
def test_an_already_repaired_database_says_so_and_stops(self):
rapport = dict(self.PLEIN, already_repaired=True)
texte = "\n".join(repair.render(rapport, dry_run=True))
self.assertIn(
todo_i18n.t("Already repaired: the group exists."), texte
)
self.assertNotIn(
todo_i18n.t("Would create one access group over"), texte
)
def test_no_witness_is_flagged_not_silently_fine(self):
# Sans utilisateur témoin on ne SAIT pas : le taire ferait passer
# une base muette pour une base saine.
rapport = dict(self.PLEIN, before=None, witness=None)
texte = "\n".join(repair.render(rapport, dry_run=True))
self.assertIn(
todo_i18n.t("No DMS user to test visibility with."), texte
)
def test_every_translation_key_exists(self):
with io.open(
repair.__file__.replace(".pyc", ".py"), encoding="utf-8"
) as handle:
src = handle.read()
for node in ast.walk(ast.parse(src)):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "t"
and node.args
and isinstance(node.args[0], ast.Constant)
):
cle = node.args[0].value
self.assertTrue(
cle in todo_i18n.TRANSLATIONS,
f"clé sans traduction : {cle!r}",
)
class TestTheVersionGuard(unittest.TestCase):
def setUp(self):
from script.odoo.migration import database_cleanup
self.cleanup = database_cleanup
self.vraie = database_cleanup.require_matching_version
self.vrai_shell = database_cleanup.run_shell
self.appels = []
database_cleanup.run_shell = lambda *a, **k: (
self.appels.append(a) or {}
)
def tearDown(self):
self.cleanup.require_matching_version = self.vraie
self.cleanup.run_shell = self.vrai_shell
def test_a_mismatch_stops_before_opening_the_database(self):
# Un Odoo d'une autre version ÉCRIT avant d'échouer : le refus
# doit précéder toute ouverture, pas la suivre.
self.cleanup.require_matching_version = lambda base: "18.0 vs 12.0"
tampon = io.StringIO()
with redirect_stdout(tampon):
code = repair.main(["-d", "vieille_base"])
self.assertEqual(code, 2)
self.assertEqual(self.appels, [])
self.assertIn("18.0 vs 12.0", tampon.getvalue())
if __name__ == "__main__":
unittest.main()