[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
"""Dupliquer : ce qui se refuse AVANT de créer quoi que ce soit.
|
|
|
|
|
|
|
|
|
|
Une duplication rate de deux façons. Elle peut échouer — c'est le cas
|
|
|
|
|
bénin, on recommence. Ou elle peut RÉUSSIR sur la mauvaise base : écraser
|
|
|
|
|
une copie qui contenait autre chose, ou rendre une copie qui n'est pas
|
|
|
|
|
neutralisée alors qu'on l'a demandé. Ces deux-là ne se rattrapent pas, et
|
|
|
|
|
c'est ce que ce fichier épingle.
|
|
|
|
|
|
|
|
|
|
La neutralisation n'existe qu'à partir d'Odoo 16 : `neutralize.py` est
|
|
|
|
|
absent avant, et `exp_duplicate_database` n'y prend que deux arguments.
|
|
|
|
|
Demander la neutralisation sous Odoo 15 doit être REFUSÉ, jamais ignoré
|
|
|
|
|
en silence — une copie qu'on croit neutralisée est pire qu'une copie
|
|
|
|
|
qu'on sait brute.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
from script.database import db_duplicate as dup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWhatANameMayBe(unittest.TestCase):
|
|
|
|
|
"""Le nom entre dans du SQL par `database_identifier` : il se filtre."""
|
|
|
|
|
|
|
|
|
|
def test_ordinary_names_pass(self):
|
[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.
Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.
L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.
--- EN ---
Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.
The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.
The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.
Assisted-by: Claude Opus 5
2026-08-30 02:02:51 -04:00
|
|
|
for nom in ("sireine", "el_essai", "a", "base-2024", "_interne"):
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
self.assertTrue(dup.nom_valide(nom), nom)
|
|
|
|
|
|
|
|
|
|
def test_a_name_that_could_carry_sql_is_refused(self):
|
|
|
|
|
for nom in ('a"; DROP DATABASE x; --', "a b", "a'b", "a;b", "a(b)"):
|
|
|
|
|
self.assertFalse(dup.nom_valide(nom), nom)
|
|
|
|
|
|
|
|
|
|
def test_an_empty_or_oversized_name_is_refused(self):
|
|
|
|
|
self.assertFalse(dup.nom_valide(""))
|
|
|
|
|
self.assertFalse(dup.nom_valide(None))
|
|
|
|
|
self.assertFalse(dup.nom_valide("x" * 64))
|
|
|
|
|
self.assertTrue(dup.nom_valide("x" * 63))
|
|
|
|
|
|
|
|
|
|
def test_a_name_starting_with_a_digit_is_refused(self):
|
|
|
|
|
self.assertFalse(dup.nom_valide("2024_base"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWhatIsRefusedBeforeAnythingIsCreated(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
# On ne veut pas d'un PostgreSQL réel : le sujet est la décision,
|
|
|
|
|
# pas la connexion. `None` = « je n'ai pas pu demander », ce qui
|
|
|
|
|
# doit laisser passer plutôt que d'inventer un refus.
|
|
|
|
|
self._vrai = dup.bases_existantes
|
|
|
|
|
dup.bases_existantes = lambda: {"source", "deja_la"}
|
|
|
|
|
self.addCleanup(setattr, dup, "bases_existantes", self._vrai)
|
|
|
|
|
|
|
|
|
|
def test_the_same_name_twice_is_refused(self):
|
|
|
|
|
refus = dup.verifier("source", "source", False)
|
|
|
|
|
self.assertTrue(any("nom" in r or "name" in r for r in refus))
|
|
|
|
|
|
|
|
|
|
def test_an_unknown_source_is_refused(self):
|
|
|
|
|
self.assertTrue(dup.verifier("pas_la", "neuve", False))
|
|
|
|
|
|
|
|
|
|
def test_overwriting_an_existing_database_is_refused(self):
|
|
|
|
|
"""Le contenu d'avant n'existerait plus nulle part."""
|
|
|
|
|
refus = dup.verifier("source", "deja_la", False)
|
|
|
|
|
self.assertTrue(refus)
|
|
|
|
|
self.assertTrue(any("deja_la" in r for r in refus))
|
|
|
|
|
|
|
|
|
|
def test_a_plain_duplication_passes(self):
|
|
|
|
|
self.assertEqual(dup.verifier("source", "neuve", False), [])
|
|
|
|
|
|
|
|
|
|
def test_when_postgres_cannot_be_asked_we_do_not_invent_a_refusal(self):
|
|
|
|
|
dup.bases_existantes = lambda: None
|
|
|
|
|
self.assertEqual(dup.verifier("source", "neuve", False), [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestNeutralisationIsRefusedBeforeOdoo16(unittest.TestCase):
|
|
|
|
|
def test_the_boundary_is_sixteen(self):
|
|
|
|
|
self.assertFalse(dup.supporte_neutralisation("15.0"))
|
|
|
|
|
self.assertTrue(dup.supporte_neutralisation("16.0"))
|
|
|
|
|
self.assertTrue(dup.supporte_neutralisation("18.0"))
|
|
|
|
|
|
|
|
|
|
def test_an_unreadable_version_is_treated_as_unsupported(self):
|
|
|
|
|
"""Dans le doute, refuser : le silence serait un faux calme."""
|
|
|
|
|
for version in (None, "", "quatorze"):
|
|
|
|
|
self.assertFalse(dup.supporte_neutralisation(version))
|
|
|
|
|
|
[FIX] database: de 12 à 15, neutraliser par le script plutôt que refuser
De 12 à 15 la neutralisation d'Odoo n'existe pas et la demande était refusée ;
le dépôt a pourtant sa technique de longue date, update_prod_to_dev.sh, et une
copie imparfaitement neutralisée vaut mieux qu'une copie brute. Le chemin suivi
est ANNONCÉ à l'exécution, car les deux ne posent pas les mêmes gestes : le
script ne pose pas is_neutralized, ne désactive pas les crons et laisse les
clés de paiement, là où il supprime les serveurs de courriel et pose un compte
de développement. Un seul cas reste refusé, le script introuvable, et son échec
fait échouer la copie : elle sortirait brute en s'annonçant neutralisée.
--- EN ---
From 12 to 15 Odoo's neutralisation does not exist and the request was refused;
yet the repository has long had its own technique, update_prod_to_dev.sh, and
an imperfectly neutralised copy beats a raw one. The route taken is ANNOUNCED
at run time, because the two do not make the same gestures: the script does not
set is_neutralized, does not disable crons and leaves the payment keys, where
it deletes the mail servers and sets up a development account. One case is
still refused, a missing script, and its failure fails the copy: it would come
out raw while announcing itself neutralised.
Assisted-by: Claude Opus 5
(cherry picked from commit bd7b648305a09c37adfe4b280e7479f1f178f20e)
2026-08-25 04:12:04 -04:00
|
|
|
def test_an_old_checkout_falls_back_instead_of_refusing(self):
|
|
|
|
|
"""De 12 à 15, le dépôt a sa technique : on l'emploie."""
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
self._vrai = dup.bases_existantes
|
|
|
|
|
dup.bases_existantes = lambda: {"source"}
|
|
|
|
|
vraie_version = dup.lire_version
|
|
|
|
|
dup.lire_version = lambda nom: "15.0"
|
|
|
|
|
try:
|
|
|
|
|
refus = dup.verifier("source", "neuve", True)
|
[FIX] database: de 12 à 15, neutraliser par le script plutôt que refuser
De 12 à 15 la neutralisation d'Odoo n'existe pas et la demande était refusée ;
le dépôt a pourtant sa technique de longue date, update_prod_to_dev.sh, et une
copie imparfaitement neutralisée vaut mieux qu'une copie brute. Le chemin suivi
est ANNONCÉ à l'exécution, car les deux ne posent pas les mêmes gestes : le
script ne pose pas is_neutralized, ne désactive pas les crons et laisse les
clés de paiement, là où il supprime les serveurs de courriel et pose un compte
de développement. Un seul cas reste refusé, le script introuvable, et son échec
fait échouer la copie : elle sortirait brute en s'annonçant neutralisée.
--- EN ---
From 12 to 15 Odoo's neutralisation does not exist and the request was refused;
yet the repository has long had its own technique, update_prod_to_dev.sh, and
an imperfectly neutralised copy beats a raw one. The route taken is ANNOUNCED
at run time, because the two do not make the same gestures: the script does not
set is_neutralized, does not disable crons and leaves the payment keys, where
it deletes the mail servers and sets up a development account. One case is
still refused, a missing script, and its failure fails the copy: it would come
out raw while announcing itself neutralised.
Assisted-by: Claude Opus 5
(cherry picked from commit bd7b648305a09c37adfe4b280e7479f1f178f20e)
2026-08-25 04:12:04 -04:00
|
|
|
technique = dup.technique_neutralisation()
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
finally:
|
|
|
|
|
dup.bases_existantes = self._vrai
|
|
|
|
|
dup.lire_version = vraie_version
|
[FIX] database: de 12 à 15, neutraliser par le script plutôt que refuser
De 12 à 15 la neutralisation d'Odoo n'existe pas et la demande était refusée ;
le dépôt a pourtant sa technique de longue date, update_prod_to_dev.sh, et une
copie imparfaitement neutralisée vaut mieux qu'une copie brute. Le chemin suivi
est ANNONCÉ à l'exécution, car les deux ne posent pas les mêmes gestes : le
script ne pose pas is_neutralized, ne désactive pas les crons et laisse les
clés de paiement, là où il supprime les serveurs de courriel et pose un compte
de développement. Un seul cas reste refusé, le script introuvable, et son échec
fait échouer la copie : elle sortirait brute en s'annonçant neutralisée.
--- EN ---
From 12 to 15 Odoo's neutralisation does not exist and the request was refused;
yet the repository has long had its own technique, update_prod_to_dev.sh, and
an imperfectly neutralised copy beats a raw one. The route taken is ANNOUNCED
at run time, because the two do not make the same gestures: the script does not
set is_neutralized, does not disable crons and leaves the payment keys, where
it deletes the mail servers and sets up a development account. One case is
still refused, a missing script, and its failure fails the copy: it would come
out raw while announcing itself neutralised.
Assisted-by: Claude Opus 5
(cherry picked from commit bd7b648305a09c37adfe4b280e7479f1f178f20e)
2026-08-25 04:12:04 -04:00
|
|
|
self.assertEqual(
|
|
|
|
|
refus, [], "la demande a été refusée au lieu de basculer"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(technique, dup.NEUTRALISATION_SCRIPT)
|
|
|
|
|
|
|
|
|
|
def test_the_route_is_chosen_by_the_version(self):
|
|
|
|
|
for version, attendu in (
|
|
|
|
|
("12.0", dup.NEUTRALISATION_SCRIPT),
|
|
|
|
|
("15.0", dup.NEUTRALISATION_SCRIPT),
|
|
|
|
|
("16.0", dup.NEUTRALISATION_ODOO),
|
|
|
|
|
("18.0", dup.NEUTRALISATION_ODOO),
|
|
|
|
|
):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
dup.technique_neutralisation(version), attendu, version
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_a_missing_script_is_the_one_case_still_refused(self):
|
|
|
|
|
"""Sinon la copie sortirait brute en se croyant neutralisée."""
|
|
|
|
|
self._vrai = dup.bases_existantes
|
|
|
|
|
dup.bases_existantes = lambda: {"source"}
|
|
|
|
|
vraie_version = dup.lire_version
|
|
|
|
|
vrai_isfile = dup.os.path.isfile
|
|
|
|
|
dup.lire_version = lambda nom: "14.0"
|
|
|
|
|
dup.os.path.isfile = lambda chemin: False
|
|
|
|
|
try:
|
|
|
|
|
refus = dup.verifier("source", "neuve", True)
|
|
|
|
|
finally:
|
|
|
|
|
dup.bases_existantes = self._vrai
|
|
|
|
|
dup.lire_version = vraie_version
|
|
|
|
|
dup.os.path.isfile = vrai_isfile
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
self.assertTrue(refus)
|
[FIX] database: de 12 à 15, neutraliser par le script plutôt que refuser
De 12 à 15 la neutralisation d'Odoo n'existe pas et la demande était refusée ;
le dépôt a pourtant sa technique de longue date, update_prod_to_dev.sh, et une
copie imparfaitement neutralisée vaut mieux qu'une copie brute. Le chemin suivi
est ANNONCÉ à l'exécution, car les deux ne posent pas les mêmes gestes : le
script ne pose pas is_neutralized, ne désactive pas les crons et laisse les
clés de paiement, là où il supprime les serveurs de courriel et pose un compte
de développement. Un seul cas reste refusé, le script introuvable, et son échec
fait échouer la copie : elle sortirait brute en s'annonçant neutralisée.
--- EN ---
From 12 to 15 Odoo's neutralisation does not exist and the request was refused;
yet the repository has long had its own technique, update_prod_to_dev.sh, and
an imperfectly neutralised copy beats a raw one. The route taken is ANNOUNCED
at run time, because the two do not make the same gestures: the script does not
set is_neutralized, does not disable crons and leaves the payment keys, where
it deletes the mail servers and sets up a development account. One case is
still refused, a missing script, and its failure fails the copy: it would come
out raw while announcing itself neutralised.
Assisted-by: Claude Opus 5
(cherry picked from commit bd7b648305a09c37adfe4b280e7479f1f178f20e)
2026-08-25 04:12:04 -04:00
|
|
|
self.assertTrue(any("update_prod_to_dev" in r for r in refus))
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheProgramHandedToOdoo(unittest.TestCase):
|
|
|
|
|
"""Ce qu'on exécute, et surtout ce qu'on n'exécute pas par mégarde."""
|
|
|
|
|
|
|
|
|
|
def test_it_parses_the_config_before_duplicating(self):
|
|
|
|
|
"""Sans parse_config, Registry.new échoue APRÈS avoir créé la base."""
|
|
|
|
|
code = dup.script_python("a", "b", False, "config.conf")
|
|
|
|
|
self.assertIn("parse_config", code)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
code.index("parse_config"), code.index("exp_duplicate")
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] database: de 12 à 15, neutraliser par le script plutôt que refuser
De 12 à 15 la neutralisation d'Odoo n'existe pas et la demande était refusée ;
le dépôt a pourtant sa technique de longue date, update_prod_to_dev.sh, et une
copie imparfaitement neutralisée vaut mieux qu'une copie brute. Le chemin suivi
est ANNONCÉ à l'exécution, car les deux ne posent pas les mêmes gestes : le
script ne pose pas is_neutralized, ne désactive pas les crons et laisse les
clés de paiement, là où il supprime les serveurs de courriel et pose un compte
de développement. Un seul cas reste refusé, le script introuvable, et son échec
fait échouer la copie : elle sortirait brute en s'annonçant neutralisée.
--- EN ---
From 12 to 15 Odoo's neutralisation does not exist and the request was refused;
yet the repository has long had its own technique, update_prod_to_dev.sh, and
an imperfectly neutralised copy beats a raw one. The route taken is ANNOUNCED
at run time, because the two do not make the same gestures: the script does not
set is_neutralized, does not disable crons and leaves the payment keys, where
it deletes the mail servers and sets up a development account. One case is
still refused, a missing script, and its failure fails the copy: it would come
out raw while announcing itself neutralised.
Assisted-by: Claude Opus 5
(cherry picked from commit bd7b648305a09c37adfe4b280e7479f1f178f20e)
2026-08-25 04:12:04 -04:00
|
|
|
def test_odoo_is_not_asked_to_neutralise_when_it_cannot(self):
|
|
|
|
|
"""Sous 12→15, exp_duplicate_database ne prend que deux arguments :
|
|
|
|
|
lui en passer un troisième ferait échouer la duplication."""
|
|
|
|
|
# `lire_version` sert AUSSI à composer le venv : lui faire dire
|
|
|
|
|
# « 14.0 » partout rendait le chemin introuvable et `dupliquer`
|
|
|
|
|
# sortait avant d'appeler quoi que ce soit — un test vert pour
|
|
|
|
|
# rien. On ne détourne donc que la version d'Odoo.
|
|
|
|
|
vraie = dup.lire_version
|
|
|
|
|
appels = []
|
|
|
|
|
dup.lire_version = lambda nom: (
|
|
|
|
|
"14.0" if nom == ".odoo-version" else vraie(nom)
|
|
|
|
|
)
|
|
|
|
|
vrais_chemins = dup.chemins_odoo
|
|
|
|
|
dup.chemins_odoo = lambda: ("/usr/bin/python3", "/tmp")
|
|
|
|
|
vrai_run = dup.subprocess.run
|
|
|
|
|
|
|
|
|
|
class Faux:
|
|
|
|
|
returncode = 0
|
|
|
|
|
stdout = ""
|
|
|
|
|
stderr = ""
|
|
|
|
|
|
|
|
|
|
def espion(cmd, **kw):
|
|
|
|
|
appels.append(cmd)
|
|
|
|
|
return Faux()
|
|
|
|
|
|
|
|
|
|
dup.subprocess.run = espion
|
|
|
|
|
try:
|
|
|
|
|
dup.dupliquer("a", "b", neutraliser=True)
|
|
|
|
|
finally:
|
|
|
|
|
dup.lire_version = vraie
|
|
|
|
|
dup.chemins_odoo = vrais_chemins
|
|
|
|
|
dup.subprocess.run = vrai_run
|
|
|
|
|
programme = [c for c in appels if "-c" in c]
|
|
|
|
|
self.assertTrue(programme)
|
|
|
|
|
self.assertNotIn("True", programme[0][-1])
|
|
|
|
|
# …et le script du dépôt a bien été lancé ensuite.
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
any(
|
|
|
|
|
dup.SCRIPT_PROD_TO_DEV in " ".join(map(str, c)) for c in appels
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_a_failing_script_makes_the_whole_thing_fail(self):
|
|
|
|
|
"""Le cas vraiment dangereux : la copie existe, le script a
|
|
|
|
|
échoué, et l'on annoncerait « neutralisée ». Avaler ce code de
|
|
|
|
|
retour rendrait une base brute indiscernable d'une base traitée.
|
|
|
|
|
"""
|
|
|
|
|
vraie = dup.lire_version
|
|
|
|
|
dup.lire_version = lambda nom: (
|
|
|
|
|
"14.0" if nom == ".odoo-version" else vraie(nom)
|
|
|
|
|
)
|
|
|
|
|
vrais_chemins = dup.chemins_odoo
|
|
|
|
|
dup.chemins_odoo = lambda: ("/usr/bin/python3", "/tmp")
|
|
|
|
|
vrai_run = dup.subprocess.run
|
|
|
|
|
|
|
|
|
|
class Reponse:
|
|
|
|
|
def __init__(self, code, sortie=""):
|
|
|
|
|
self.returncode = code
|
|
|
|
|
self.stdout = sortie
|
|
|
|
|
self.stderr = sortie
|
|
|
|
|
|
|
|
|
|
def espion(cmd, **kw):
|
|
|
|
|
# La duplication réussit, le script échoue.
|
|
|
|
|
if dup.SCRIPT_PROD_TO_DEV in " ".join(map(str, cmd)):
|
|
|
|
|
return Reponse(1, "install_addons_dev.sh a échoué")
|
|
|
|
|
return Reponse(0)
|
|
|
|
|
|
|
|
|
|
dup.subprocess.run = espion
|
|
|
|
|
try:
|
|
|
|
|
code, sortie = dup.dupliquer("a", "b", neutraliser=True)
|
|
|
|
|
finally:
|
|
|
|
|
dup.lire_version = vraie
|
|
|
|
|
dup.chemins_odoo = vrais_chemins
|
|
|
|
|
dup.subprocess.run = vrai_run
|
|
|
|
|
self.assertNotEqual(code, 0, "l'échec du script a été avalé")
|
|
|
|
|
self.assertIn("install_addons_dev", sortie)
|
|
|
|
|
|
[ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.
Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.
--- EN ---
Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.
Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.
Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-25 03:55:46 -04:00
|
|
|
def test_neutralisation_is_passed_only_when_asked(self):
|
|
|
|
|
self.assertIn("'a', 'b', True", dup.script_python("a", "b", True, "c"))
|
|
|
|
|
self.assertIn("'a', 'b')", dup.script_python("a", "b", False, "c"))
|
|
|
|
|
|
|
|
|
|
def test_the_names_travel_as_python_literals(self):
|
|
|
|
|
"""Ils sont interpolés dans du code : `repr` et rien d'autre."""
|
|
|
|
|
code = dup.script_python("a'b", "c", False, "config.conf")
|
|
|
|
|
self.assertIn(repr("a'b"), code)
|
|
|
|
|
|
|
|
|
|
def test_the_odoo_path_is_read_from_the_version_files(self):
|
|
|
|
|
"""Composer le nom de tête, c'est se tromper un jour sur l'une
|
|
|
|
|
des deux versions qu'il porte."""
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
source = inspect.getsource(dup.chemins_odoo)
|
|
|
|
|
self.assertIn(".erplibre-version", source)
|
|
|
|
|
self.assertIn(".odoo-version", source)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheContractOfTheCommandLine(unittest.TestCase):
|
|
|
|
|
def test_a_refusal_exits_two_and_creates_nothing(self):
|
|
|
|
|
appels = []
|
|
|
|
|
vrai = dup.dupliquer
|
|
|
|
|
dup.dupliquer = lambda *a, **k: appels.append(a) or (0, "")
|
|
|
|
|
vraies_bases = dup.bases_existantes
|
|
|
|
|
dup.bases_existantes = lambda: {"source", "deja_la"}
|
|
|
|
|
try:
|
|
|
|
|
code = dup.main(["-s", "source", "-d", "deja_la"])
|
|
|
|
|
finally:
|
|
|
|
|
dup.dupliquer = vrai
|
|
|
|
|
dup.bases_existantes = vraies_bases
|
|
|
|
|
self.assertEqual(code, 2)
|
|
|
|
|
self.assertEqual(appels, [], "la duplication a été tentée quand même")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|