"
""
)
neuf = cow.repair_anchor(
copie, ARCH_MODULE, "//t[@t-set='contactus_form_values']"
)
hote = etree.fromstring(neuf.encode("utf-8")).xpath(
"//t[@t-call='website.layout']"
)[0]
noms = [e.get("t-set") or e.get("id") for e in hote]
self.assertEqual(
["deja_la", "contactus_form_values", "wrap"], noms, neuf
)
def test_it_keeps_the_page_written_by_the_customer(self):
# C'est tout l'enjeu : réinitialiser la vue rendrait la page
# fonctionnelle et effacerait son contenu.
neuf = cow.repair_anchor(
ARCH_COPIE, ARCH_MODULE, "//t[@t-set='contactus_form_values']"
)
self.assertIn("Parlons de votre projet", neuf)
def test_it_refuses_when_the_host_element_has_no_twin(self):
# Mieux vaut une réparation refusée qu'un bloc posé au hasard au
# milieu de la page de quelqu'un.
etrangere = ""
self.assertIsNone(
cow.repair_anchor(
etrangere, ARCH_MODULE, "//t[@t-set='contactus_form_values']"
)
)
def test_it_refuses_when_the_module_has_no_anchor_either(self):
self.assertIsNone(
cow.repair_anchor(ARCH_COPIE, ARCH_COPIE, "//t[@t-set='absent']")
)
def test_it_refuses_an_unparsable_arch(self):
self.assertIsNone(cow.repair_anchor("", "fr_CA": ""})),
["en_US", "fr_CA"],
)
def test_the_query_never_pins_one_language(self):
# C'est ICI que le défaut est né : `arch_db->>'en_US'` ramenait
# une seule langue, l'audit déclarait la vue saine et /contact
# restait en 500 parce que le site rend en fr_CA. La requête doit
# ramener l'OBJET entier.
requete = cow.arch_expr(jsonb=True)
self.assertNotIn("en_US", requete)
self.assertNotIn("->>", requete)
self.assertIn("arch_db", requete)
def test_a_varchar_column_is_wrapped_as_one_nameless_language(self):
# Une seule forme en aval : le reste du code n'a pas à savoir si
# la version traduit l'arch ou non.
requete = cow.arch_expr(jsonb=False)
self.assertIn("json_build_object", requete)
def test_a_plain_string_is_the_language_less_case(self):
self.assertEqual(cow.langs_of(""), {"": ""})
def test_a_non_string_value_is_ignored(self):
self.assertEqual(
cow.langs_of({"en_US": "", "x": 3}), {"en_US": ""}
)
def test_the_xpath_of_a_child_is_read_from_any_language(self):
# Les xpath ne sont pas traduits : n'importe quelle langue suffit.
self.assertEqual(
cow.anchors_wanted(cow.any_lang({"fr_CA": ARCH_ENFANT})),
["//t[@t-set='contactus_form_values']"],
)
class TestTheSqlItWrites(unittest.TestCase):
def test_it_writes_all_the_languages_in_one_update(self):
# Deux UPDATE sur la même vue et le second effacerait le premier ;
# un seul portant une langue laisserait l'autre cassée.
sql = cow.write_arch_sql(
7, {"en_US": "", "fr_CA": ""}, jsonb=True
)
self.assertEqual(1, sql.count("UPDATE"), sql)
self.assertIn("en_US", sql)
self.assertIn("fr_CA", sql)
self.assertIn("", sql)
self.assertIn("", sql)
self.assertIn("WHERE id = 7", sql)
def test_the_varchar_case_writes_the_text_itself(self):
sql = cow.write_arch_sql(7, {"": ""}, jsonb=False)
self.assertNotIn("jsonb_build_object", sql)
self.assertIn("", sql)
def test_both_branches_survive_an_apostrophe(self):
# La 16 stocke en varchar, la 17 et la 18 en jsonb : les DEUX
# chemins écrivent, et n'éprouver que l'un laisse l'autre casser
# au premier attribut cité.
arch = ""
for jsonb, langues in ((True, {"en_US": arch}), (False, {"": arch})):
with self.subTest(jsonb=jsonb):
sql = cow.write_arch_sql(7, langues, jsonb=jsonb)
self.assertIn("$elcow$" + arch + "$elcow$", sql)
class TestTheReport(unittest.TestCase):
def rapport(self):
return {
"database": "essai",
"jsonb": True,
"views": [
{
"id": 1228,
"key": "website.contactus",
"anchors": [{"enfant": 2782, "expr": "//t[@t-set='x']"}],
"calls": ["website.company_description"],
}
],
}
def test_a_clean_database_says_so(self):
texte = "\n".join(
cow.render({"database": "x", "jsonb": True, "views": []}, [])
)
self.assertIn("✅", texte)
def test_it_names_the_view_the_child_and_the_template(self):
texte = "\n".join(cow.render(self.rapport(), []))
self.assertIn("1228", texte)
self.assertIn("2782", texte)
self.assertIn("website.company_description", texte)
def test_it_warns_that_removing_a_call_removes_a_block(self):
# Le message passe par `t()` : le comparer à un mot anglais en
# dur le fait tomber dès qu'on le traduit. On demande la MÊME
# traduction que le code, ce qui éprouve l'avertissement sans
# épouser une langue.
gestes = [(1228, "k", "t-call", "website.company_description", "SQL")]
texte = "\n".join(cow.render(self.rapport(), gestes))
self.assertIn(
cow.t("Removing a call removes its block from the page."), texte
)
def test_it_stays_silent_about_blocks_when_nothing_is_removed(self):
gestes = [(1228, "k", "anchor", "//t", "SQL")]
texte = "\n".join(cow.render(self.rapport(), gestes))
self.assertNotIn(
cow.t("Removing a call removes its block from the page."), texte
)
def test_it_says_when_a_repair_is_out_of_reach(self):
gestes = [(1228, "k", "anchor-impossible", "//t", None)]
texte = "\n".join(cow.render(self.rapport(), gestes))
self.assertIn("1", texte)
self.assertIn("⚠", texte)
class TestItRunsInsideTheMigration(unittest.TestCase):
"""La réparation doit tourner APRÈS OpenUpgrade, pas avant.
Elle prend l'ancrage manquant dans la vue MODULE, qui ne porte l'arch
de la version cible qu'une fois la migration passée. Lancée avant,
elle recopierait l'ancien ancrage — ou rien.
"""
def pilote(self):
chemin = os.path.join(
os.path.dirname(__file__),
"..",
"script",
"todo",
"todo_upgrade.py",
)
with open(chemin, encoding="utf-8") as handle:
return handle.read()
def corps_de_la_reparation(self):
"""Le CODE de la méthode, docstring retirée.
La docstring nomme `wait_at_error=False` pour l'expliquer : un
test qui lit tout le texte le trouve même quand l'argument a
disparu, et croit garder ce qu'il a laissé partir. Mesuré.
"""
import ast
for noeud in ast.walk(ast.parse(self.pilote())):
if (
isinstance(noeud, ast.FunctionDef)
and noeud.name == "repair_cow_render"
):
sans_texte = [
n
for n in noeud.body
if not (
isinstance(n, ast.Expr)
and isinstance(n.value, ast.Constant)
and isinstance(n.value.value, str)
)
]
return ast.dump(ast.Module(body=sans_texte, type_ignores=[]))
return ""
def test_the_scan_finds_the_method_at_all(self):
# Sans cette borne, les tests suivants passeraient sur une chaîne
# vide le jour où la méthode est renommée.
self.assertTrue(self.corps_de_la_reparation())
def test_the_driver_calls_the_repair(self):
self.assertIn("repair_cow_render", self.pilote())
def test_it_applies_rather_than_only_reporting(self):
corps = self.corps_de_la_reparation()
self.assertIn("fix_cow_render.py", corps)
self.assertIn("--apply", corps)
def test_a_finding_does_not_open_the_error_menu(self):
# Le code 1 de cet outil veut dire « j'ai trouvé », pas « je suis
# tombé ». Sans ce drapeau le pilote s'arrête sur une réparation
# réussie.
corps = self.corps_de_la_reparation()
self.assertIn("wait_at_error", corps)
self.assertIn("value=False", corps)
def test_it_is_called_after_the_data_migration(self):
source = self.pilote()
migration = source.index("cmd_upgrade,\n new_env=")
appel = source.index("self.repair_cow_render(")
self.assertLess(
migration,
appel,
"la réparation doit suivre OpenUpgrade : avant, la vue module"
" porte encore l'arch de l'ancienne version",
)
def test_a_missing_tool_is_not_a_crash(self):
# Un vieux checkout n'a pas le fichier ; la migration ne doit pas
# s'arrêter pour autant.
corps = self.corps_de_la_reparation()
self.assertIn("isfile", corps)
if __name__ == "__main__":
unittest.main()