#!/usr/bin/env python3 # © 2021-2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) """Ouvrir le back-office comme quelqu'un qui s'y connecte. Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit RIEN du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un modèle dont le code n'accompagne plus la version cible. Rien de tout cela n'arrête le chargement des modules ; cela arrête le jour où quelqu'un ouvre l'appli. Ce que ces tests verrouillent est ce que l'exécution réelle a corrigé — et chaque point ci-dessous a d'abord été un vrai défaut, mesuré sur une base 18.0 de 25 applications : - `web_search_read` a changé de signature en 17 (`fields` est devenu `specification`). Seize applications sur vingt-deux échouaient sur MON appel. `search_read` n'a pas bougé depuis la 12 ; - un modèle absent du registre rend un « 404 Not Found » nu, illisible, alors que c'est la trouvaille la plus nette d'une migration ; - lire les champs d'un arch à l'expression régulière ramasse ceux des SOUS-VUES : neuf applications rapportées « nommant un champ absent », toutes fausses, parce que les lignes d'une facture ne sont pas des champs de la facture ; - `run_psql` rend une liste vide aussi bien pour « aucune ligne » que pour « requête refusée » : un SQL fautif faisait dire « base non neutralisée ». """ import os import sys import unittest REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(REPO, "script", "odoo", "migration")) import smoke_internal_ui as ui # noqa: E402 class TestFindingTheFirstPageOfEachApp(unittest.TestCase): """Ce que le client web ouvre au clic : le premier menu avec une action.""" MENUS = [ { "id": 1, "name": "Ventes", "parent_id": False, "sequence": 10, "action": False, }, { "id": 2, "name": "Commandes", "parent_id": [1, "Ventes"], "sequence": 5, "action": False, }, { "id": 3, "name": "Devis", "parent_id": [2, "Commandes"], "sequence": 1, "action": "ir.actions.act_window,42", }, { "id": 4, "name": "Clients", "parent_id": [1, "Ventes"], "sequence": 9, "action": "ir.actions.act_window,43", }, { "id": 5, "name": "Réglages", "parent_id": False, "sequence": 99, "action": "ir.actions.act_window,7", }, { "id": 6, "name": "Vide", "parent_id": False, "sequence": 50, "action": False, }, ] def test_it_descends_to_the_first_actionable_menu(self): trouve = dict( (app["name"], first["name"] if first else None) for app, first in ui.apps(self.MENUS) ) self.assertEqual(trouve["Ventes"], "Devis") def test_sequence_decides_not_the_id(self): # « Commandes » (5) passe avant « Clients » (9) : l'ordre affiché # est celui que l'utilisateur voit, pas l'ordre d'insertion. trouve = dict( (app["name"], first["name"] if first else None) for app, first in ui.apps(self.MENUS) ) self.assertNotEqual(trouve["Ventes"], "Clients") def test_an_app_carrying_its_own_action_is_its_own_first_page(self): trouve = dict( (app["name"], first["name"] if first else None) for app, first in ui.apps(self.MENUS) ) self.assertEqual(trouve["Réglages"], "Réglages") def test_an_app_without_any_action_is_not_an_error(self): # Elle n'a rien à ouvrir. La compter comme un échec ferait chercher # un dégât là où il n'y a qu'un menu de regroupement. trouve = dict( (app["name"], first["name"] if first else None) for app, first in ui.apps(self.MENUS) ) self.assertIsNone(trouve["Vide"]) def test_a_menu_loop_does_not_hang(self): # Une base migrée porte parfois des données incohérentes ; un # parcours naïf tournerait indéfiniment sans rien dire. boucle = [ { "id": 1, "name": "A", "parent_id": False, "sequence": 1, "action": False, }, { "id": 2, "name": "B", "parent_id": [1, "A"], "sequence": 1, "action": False, }, { "id": 3, "name": "C", "parent_id": [2, "B"], "sequence": 1, "action": False, }, ] boucle[0]["parent_id"] = [3, "C"] self.assertIsInstance(ui.apps(boucle), list) class TestReadingTheFieldsOfAPage(unittest.TestCase): """Les champs de la page, et RIEN de ce qui appartient aux sous-vues.""" FORM = { "views": { "form": { "arch": ( "
" ) } } } def test_the_top_level_fields_are_taken(self): self.assertEqual( ui.arch_fields(self.FORM), ["partner_id", "line_ids", "amount_total"], ) def test_the_fields_of_an_embedded_view_are_NOT(self): # Le défaut mesuré : `product_id` et `price_unit` sont des champs de # la LIGNE de facture. Les attribuer à la facture faisait rapporter # neuf applications cassées qui allaient parfaitement bien. lst = ui.arch_fields(self.FORM) self.assertNotIn("product_id", lst) self.assertNotIn("price_unit", lst) def test_a_dotted_name_is_left_alone(self): views = {"a": {"arch": '