erplibre/test/test_script_permissions.py

125 lines
4.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Un script avec shebang doit être exécutable — et pas inscriptible en groupe.
Le bit d'exécution n'accorde rien : qui peut lire le fichier peut déjà faire
« python3 fichier ». Ce qui compte est la PAIRE : un fichier à la fois
exécuté par d'autres et modifiable par le groupe laisse un membre du groupe
changer ce que les autres lancent. L'umask 0002 du poste crée exactement
cette paire dès qu'on ajoute +x sans y penser (664 -> 775).
Git ne stocke que le bit d'exécution, jamais 664 contre 644 : le mode complet
ne se vérifie donc que sur le disque, ici.
"""
import glob
import os
import stat
import unittest
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PATTERNS = (
"script/analyse/*.py",
"script/analyse/*/*.py",
"script/odoo/migration/*.py",
)
def scripts():
"""(chemin, a un shebang) pour chaque script des dossiers visés."""
found = []
for pattern in PATTERNS:
for path in sorted(glob.glob(os.path.join(REPO, pattern))):
with open(path, "rb") as handle:
found.append((path, handle.read(2) == b"#!"))
return found
class TestExecutableBit(unittest.TestCase):
def test_the_inventory_is_not_empty(self):
# Un test qui ne trouve rien passe toujours.
self.assertGreater(len(scripts()), 10)
def test_every_shebang_script_is_executable(self):
for path, has_shebang in scripts():
if not has_shebang:
continue
with self.subTest(script=os.path.relpath(path, REPO)):
self.assertTrue(os.access(path, os.X_OK))
def test_none_is_writable_by_group_or_others(self):
# LE point qui compte : exécuté par d'autres ET modifiable par eux.
for path, _ in scripts():
mode = stat.S_IMODE(os.stat(path).st_mode)
with self.subTest(script=os.path.relpath(path, REPO)):
self.assertFalse(
mode & (stat.S_IWGRP | stat.S_IWOTH), oct(mode)
)
def test_none_is_setuid_or_setgid(self):
for path, _ in scripts():
mode = os.stat(path).st_mode
with self.subTest(script=os.path.relpath(path, REPO)):
self.assertFalse(mode & (stat.S_ISUID | stat.S_ISGID))
class TestTheTuiSaysWhyItRefuses(unittest.TestCase):
"""Rendre un script lançable ouvre un chemin sans le venv.
Le shebang est « #!/usr/bin/env python3 » : lancé directement depuis un
shell où le venv n'est pas actif, c'est le python du système qui répond,
et Textual n'y est pas. La TUI retombait alors sur le rapport texte sans
rien dire — le même défaut muet que le tube de sortie.
"""
def run_tui(self):
import sys
sys.path.insert(0, os.path.join(REPO, "script", "odoo", "migration"))
self.addCleanup(sys.path.remove, sys.path[0])
from cow_drift_tui import run_tui
return run_tui
def finding(self):
return {
"id": 1,
"key": "k",
"website_id": 1,
"reason": "r",
"module_id": 2,
"module_arch": "a",
"copy_arch": "b",
"decl_current": None,
"decl_target": None,
"current_version": "odoo12.0",
"target_version": "odoo13.0",
}
def test_a_pipe_is_explained_not_swallowed(self):
import io
from contextlib import redirect_stdout
out = io.StringIO()
with redirect_stdout(out):
result = self.run_tui()([self.finding()])
self.assertFalse(result)
self.assertTrue(out.getvalue().strip(), "refus muet")
def test_nothing_to_show_stays_silent(self):
# Une liste vide n'est pas un refus : il n'y a rien à annoncer.
import io
from contextlib import redirect_stdout
out = io.StringIO()
with redirect_stdout(out):
self.assertFalse(self.run_tui()([]))
self.assertEqual(out.getvalue(), "")
if __name__ == "__main__":
unittest.main()