#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Une migration peut réussir et servir quand même des 500.
Mesuré sur une vraie migration 12 → 13 : tous les modules chargés, aucune
erreur au journal, et pourtant deux pages publiques sur trente-trois
répondaient 500 — un billet de blogue et /contactus. Rien ne les distinguait
avant de les demander.
La liste vient du sitemap, celle qu'Odoo publie pour les moteurs de
recherche. Elle ne se lit PAS depuis « odoo-bin shell » : enumerate_pages()
réclame http.root.get_db_router(request.db) et lève « object unbound » sans
requête réelle. D'où le serveur démarré, interrogé, arrêté.
"""
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_public_url as smoke # noqa: E402
class TestTheUrlAreBroughtHome(unittest.TestCase):
"""Le sitemap porte le domaine du site ; on teste une base locale."""
def test_the_host_is_replaced(self):
# Garder le domaine ferait interroger la PRODUCTION depuis une
# migration. C'est le genre d'erreur qui ne se voit qu'après.
url = smoke.local_url(
"http://127.0.0.1:8169", "https://technolibre.ca/blog/x-3/post/y-5"
)
self.assertEqual(url, "http://127.0.0.1:8169/blog/x-3/post/y-5")
def test_a_query_string_survives(self):
url = smoke.local_url("http://127.0.0.1:8169", "https://a.ca/p?x=1")
self.assertEqual(url, "http://127.0.0.1:8169/p?x=1")
def test_a_bare_domain_becomes_the_root(self):
url = smoke.local_url("http://127.0.0.1:8169", "https://a.ca")
self.assertEqual(url, "http://127.0.0.1:8169/")
def test_a_trailing_slash_on_the_base_does_not_double(self):
url = smoke.local_url("http://127.0.0.1:8169/", "https://a.ca/p")
self.assertEqual(url, "http://127.0.0.1:8169/p")
class TestReadingTheSitemap(unittest.TestCase):
def setUp(self):
self.answers = {}
self.original = smoke.fetch
smoke.fetch = lambda url, timeout=30: self.answers.get(url, (404, ""))
self.addCleanup(setattr, smoke, "fetch", self.original)
def test_a_plain_sitemap(self):
self.answers["http://h/sitemap.xml"] = (
200,
"https://a.ca/xhttps://a.ca/y",
)
lst_url, status = smoke.sitemap_urls("http://h")
self.assertEqual(status, 200)
self.assertEqual(lst_url, ["http://h/x", "http://h/y"])
def test_an_index_is_followed(self):
# Odoo découpe le sitemap au-delà d'un certain nombre de pages : ne
# pas descendre d'un cran ferait tester deux fichiers XML au lieu du
# site, et conclure « tout va bien ».
self.answers["http://h/sitemap.xml"] = (
200,
"https://a.ca/sitemap-1-1.xml"
"",
)
self.answers["http://h/sitemap-1-1.xml"] = (
200,
"https://a.ca/deep",
)
lst_url, _status = smoke.sitemap_urls("http://h")
self.assertEqual(lst_url, ["http://h/deep"])
def test_duplicates_are_dropped(self):
self.answers["http://h/sitemap.xml"] = (
200,
"https://a.ca/xhttps://a.ca/x",
)
lst_url, _status = smoke.sitemap_urls("http://h")
self.assertEqual(lst_url, ["http://h/x"])
def test_an_unreachable_sitemap_is_reported_not_taken_as_empty(self):
# Sans cela, un sitemap injoignable se lirait comme « aucune page à
# tester », c'est-à-dire comme un succès.
lst_url, status = smoke.sitemap_urls("http://h")
self.assertEqual(lst_url, [])
self.assertEqual(status, 404)
class TestWhatCountsAsAFailure(unittest.TestCase):
def setUp(self):
self.answers = {}
self.original = smoke.fetch
smoke.fetch = lambda url, timeout=30: self.answers.get(url, (200, ""))
self.addCleanup(setattr, smoke, "fetch", self.original)
def test_a_500_fails(self):
self.answers["http://h/bad"] = (500, "")
self.assertEqual(
smoke.check_urls(["http://h/bad"]), [("http://h/bad", 500)]
)
def test_a_404_fails_too(self):
# Le sitemap est la liste PUBLIQUE : une page qui y figure et répond
# 404 est aussi fausse qu'un 500, juste plus discrète.
self.answers["http://h/gone"] = (404, "")
self.assertEqual(len(smoke.check_urls(["http://h/gone"])), 1)
def test_no_answer_at_all_fails(self):
self.answers["http://h/dead"] = (0, "")
self.assertEqual(
smoke.check_urls(["http://h/dead"]), [("http://h/dead", 0)]
)
def test_a_200_passes(self):
self.assertEqual(smoke.check_urls(["http://h/ok"]), [])
class TestTheReport(unittest.TestCase):
def setUp(self):
from script.todo import todo_i18n
self.addCleanup(
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
)
todo_i18n._current_lang = "en"
def test_all_green_says_how_many(self):
text = smoke.render(["a", "b"], [])
self.assertIn("2", text)
self.assertIn("✅", text)
def test_a_failure_shows_the_status_and_the_url(self):
text = smoke.render(["a"], [("http://h/blog/x", 500)])
self.assertIn("500", text)
self.assertIn("http://h/blog/x", text)
def test_an_empty_sitemap_is_not_a_success(self):
# « rien testé » ne doit pas se lire « rien de cassé ».
text = smoke.render([], [])
self.assertIn("⚠️", text)
class TestTheServerIsAlwaysStopped(unittest.TestCase):
"""Un serveur oublié tient le port et fait échouer le palier suivant."""
def test_it_is_terminated_even_when_the_sitemap_fails(self):
stopped = []
class FakeServer:
def terminate(self):
stopped.append("terminate")
def wait(self, timeout=None):
return 0
def kill(self):
stopped.append("kill")
original_start = smoke.start_server
original_wait = smoke.wait_ready
smoke.start_server = lambda db, port, cfg="./config.conf": FakeServer()
smoke.wait_ready = lambda base, timeout=180, sleep=2: False
self.addCleanup(setattr, smoke, "start_server", original_start)
self.addCleanup(setattr, smoke, "wait_ready", original_wait)
with self.assertRaises(RuntimeError):
smoke.run("db", 8169, "./config.conf")
self.assertEqual(stopped, ["terminate"])
def test_a_distinct_port_by_default(self):
# 8069 est souvent pris par l'instance de travail : le lui voler
# ferait échouer le test pour une raison sans rapport.
self.assertNotEqual(smoke.DEFAULT_PORT, 8069)
class TestTheMigrationOffersIt(unittest.TestCase):
def setUp(self):
from script.todo import todo_i18n
self.addCleanup(
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
)
todo_i18n._current_lang = "en"
def run_prompt(self, answer):
from script.todo.todo_upgrade import TodoUpgrade
upgrade = TodoUpgrade.__new__(TodoUpgrade)
upgrade.dct_progression = {}
upgrade.lst_command_executed = []
upgrade.write_config = lambda: None
lst_cmd = []
upgrade.run_on_terminal = lambda cmd: lst_cmd.append(cmd) or 0
upgrade.ask_gate = lambda prompt: answer
upgrade.prompt_smoke_public_url("db_upgrade_13")
return lst_cmd
def test_the_default_runs_nothing(self):
# Cela démarre un serveur et peut durer : pas à chaque palier sans
# qu'on l'ait demandé.
self.assertEqual(self.run_prompt(""), [])
def test_yes_runs_it_on_the_upgraded_database(self):
lst_cmd = self.run_prompt("y")
self.assertEqual(len(lst_cmd), 1)
self.assertIn("smoke_public_url.py", lst_cmd[0])
self.assertIn("-d db_upgrade_13", lst_cmd[0])
def test_it_is_asked_before_the_selenium_prompt(self):
# Après, la question arriverait une fois le navigateur ouvert : on
# aurait déjà cherché à la main ce que le test nomme.
import inspect
from script.todo.todo_upgrade import TodoUpgrade
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
self.assertLess(
source.index("prompt_smoke_public_url"),
source.index("Open the server with Selenium"),
)
if __name__ == "__main__":
unittest.main()