From 99ab6067a9d4e4d4230e4ce8f0698bb9f450fa1b Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 26 May 2026 19:37:05 +0800 Subject: [PATCH 1/3] test: add tests/ folders to 5 modules missing CI coverage (#1040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five OpenSPP2 modules shipped without a tests/ folder, so CI's detect-changes coverage matrix had no flags to upload against them. This patch wires up at minimum one passing test per module so the matrix can start reporting coverage. - spp_drims_sl (data-only, SL locale): install + key hazard category + LKR currency activation. - spp_farmer_registry_dashboard (data-only, dashboard records): install + farmer dashboard category / group / overview records loaded. - spp_hide_menus_base (models): install + group_hide_menus_user seed + hide_menu/show_menu round-trip + idempotency + MENU_APP catalog well-formed. - spp_indicator_studio (UI bridge): install + indicator views + indicator category views + act_window points at spp.indicator. - spp_starter_farmer_registry (bundle): install + every declared dependency installed + config_smallholder_threshold seed loaded. Locally verified: spp_drims_sl reports 3/3 tests, spp_hide_menus_base reports 6/6. The other three fail to install in the local Docker setup only because they depend on modules outside OpenSPP2 (spp_dashboard_base lives in the private openspp-modules-v2 repo) — CI mounts both repos so those will install + run there. Out of scope per the ticket: - spp_registry — handled on a separate branch. - theme_openspp_muk — theme module, no Python logic to cover. --- spp_drims_sl/tests/__init__.py | 2 + spp_drims_sl/tests/test_spp_drims_sl.py | 42 +++++++++ .../tests/__init__.py | 2 + .../test_spp_farmer_registry_dashboard.py | 57 +++++++++++++ spp_hide_menus_base/tests/__init__.py | 2 + spp_hide_menus_base/tests/test_hide_menu.py | 85 +++++++++++++++++++ spp_indicator_studio/tests/__init__.py | 2 + .../tests/test_spp_indicator_studio.py | 55 ++++++++++++ spp_starter_farmer_registry/tests/__init__.py | 2 + .../tests/test_spp_starter_farmer_registry.py | 60 +++++++++++++ 10 files changed, 309 insertions(+) create mode 100644 spp_drims_sl/tests/__init__.py create mode 100644 spp_drims_sl/tests/test_spp_drims_sl.py create mode 100644 spp_farmer_registry_dashboard/tests/__init__.py create mode 100644 spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py create mode 100644 spp_hide_menus_base/tests/__init__.py create mode 100644 spp_hide_menus_base/tests/test_hide_menu.py create mode 100644 spp_indicator_studio/tests/__init__.py create mode 100644 spp_indicator_studio/tests/test_spp_indicator_studio.py create mode 100644 spp_starter_farmer_registry/tests/__init__.py create mode 100644 spp_starter_farmer_registry/tests/test_spp_starter_farmer_registry.py diff --git a/spp_drims_sl/tests/__init__.py b/spp_drims_sl/tests/__init__.py new file mode 100644 index 00000000..defe031f --- /dev/null +++ b/spp_drims_sl/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_spp_drims_sl diff --git a/spp_drims_sl/tests/test_spp_drims_sl.py b/spp_drims_sl/tests/test_spp_drims_sl.py new file mode 100644 index 00000000..57e00d7e --- /dev/null +++ b/spp_drims_sl/tests/test_spp_drims_sl.py @@ -0,0 +1,42 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Install / data-load sanity tests for spp_drims_sl. + +This is a data-only module (Sri Lanka locale configuration for DRIMS) — +it ships seed records but no Python models or methods. The tests below +exercise the install path so CI's per-module coverage matrix has +something to report against, and assert that the headline data records +the rest of the module relies on are actually present after install. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSppDrimsSl(TransactionCase): + """Spot-check that the seed data declared in __manifest__.py loaded.""" + + def test_module_is_installed(self): + module = self.env["ir.module.module"].search([("name", "=", "spp_drims_sl")], limit=1) + self.assertTrue(module, "spp_drims_sl module not registered") + self.assertEqual( + module.state, + "installed", + f"spp_drims_sl expected 'installed', got {module.state}", + ) + + def test_hazard_category_seed_loaded(self): + """data/hazard_categories.xml declares at least one category.""" + category = self.env.ref("spp_drims_sl.category_natural", raise_if_not_found=False) + self.assertTrue( + category, + "spp_drims_sl.category_natural missing — hazard_categories.xml didn't load", + ) + + def test_sl_currency_company_config(self): + """data/company_config.xml activates LKR for the locale.""" + currency = self.env.ref("base.LKR", raise_if_not_found=False) + self.assertTrue(currency, "base.LKR currency missing") + self.assertTrue( + currency.active, + "LKR currency expected to be active after spp_drims_sl install", + ) diff --git a/spp_farmer_registry_dashboard/tests/__init__.py b/spp_farmer_registry_dashboard/tests/__init__.py new file mode 100644 index 00000000..865a7166 --- /dev/null +++ b/spp_farmer_registry_dashboard/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_spp_farmer_registry_dashboard diff --git a/spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py b/spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py new file mode 100644 index 00000000..8293fdc7 --- /dev/null +++ b/spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py @@ -0,0 +1,57 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Install / data-load sanity tests for spp_farmer_registry_dashboard. + +This is a data-only module — it ships dashboard metric definitions and +spreadsheet dashboards but no Python models or methods. The tests exercise +the install path so CI's per-module coverage matrix records something +against it, and assert that the headline dashboard data records loaded. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSppFarmerRegistryDashboard(TransactionCase): + """Spot-check that the seed data declared in __manifest__.py loaded.""" + + def test_module_is_installed(self): + module = self.env["ir.module.module"].search([("name", "=", "spp_farmer_registry_dashboard")], limit=1) + self.assertTrue(module, "spp_farmer_registry_dashboard not registered") + self.assertEqual( + module.state, + "installed", + f"spp_farmer_registry_dashboard expected 'installed', got {module.state}", + ) + + def test_dashboard_metric_category_seed_loaded(self): + """data/dashboard_metrics.xml declares the farmer dashboard category.""" + category = self.env.ref( + "spp_farmer_registry_dashboard.category_dashboard_farmer", + raise_if_not_found=False, + ) + self.assertTrue( + category, + "category_dashboard_farmer missing — dashboard_metrics.xml didn't load", + ) + + def test_dashboard_group_seed_loaded(self): + """data/dashboards.xml declares the spreadsheet dashboard group.""" + group = self.env.ref( + "spp_farmer_registry_dashboard.spreadsheet_dashboard_group_farmer", + raise_if_not_found=False, + ) + self.assertTrue( + group, + "spreadsheet_dashboard_group_farmer missing — dashboards.xml didn't load", + ) + + def test_dashboard_record_seed_loaded(self): + """data/dashboards.xml declares the farmer overview dashboard.""" + dashboard = self.env.ref( + "spp_farmer_registry_dashboard.dashboard_farmer_overview", + raise_if_not_found=False, + ) + self.assertTrue( + dashboard, + "dashboard_farmer_overview missing — dashboards.xml didn't load", + ) diff --git a/spp_hide_menus_base/tests/__init__.py b/spp_hide_menus_base/tests/__init__.py new file mode 100644 index 00000000..5ac956af --- /dev/null +++ b/spp_hide_menus_base/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_hide_menu diff --git a/spp_hide_menus_base/tests/test_hide_menu.py b/spp_hide_menus_base/tests/test_hide_menu.py new file mode 100644 index 00000000..0a655186 --- /dev/null +++ b/spp_hide_menus_base/tests/test_hide_menu.py @@ -0,0 +1,85 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Tests for spp_hide_menus_base — hide/show menu visibility logic. + +The module patches ``ir.module.module`` to hide a curated list of stock +Odoo menus (Project, Calendar, Stock, ...) from the OpenSPP user group +when an install/upgrade completes. The tests exercise the ``hide_menu`` +and ``show_menu`` round-trip on ``spp.hide.menu`` directly so we cover +the model's state transition without depending on a real "Apps install" +flow. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSppHideMenu(TransactionCase): + """Exercise the hide / show round-trip on a sample menu.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Pick any existing menu we can safely toggle in a test transaction. + cls.menu = cls.env["ir.ui.menu"].search([], limit=1) + if not cls.menu: + raise AssertionError("No ir.ui.menu records found to test against") + + def test_module_is_installed(self): + module = self.env["ir.module.module"].search([("name", "=", "spp_hide_menus_base")], limit=1) + self.assertEqual(module.state, "installed") + + def test_group_hide_menus_user_seed(self): + """security/groups.xml must declare the hide-menus-user group.""" + group = self.env.ref("spp_hide_menus_base.group_hide_menus_user", raise_if_not_found=False) + self.assertTrue( + group, + "group_hide_menus_user must exist — hide_menu() falls back on it", + ) + + def test_hide_menu_transition(self): + """hide_menu() flips state show → hide and snapshots original groups.""" + original_groups = self.env["ir.ui.menu"].browse(self.menu.id).group_ids + record = self.env["spp.hide.menu"].create({"menu_id": self.menu.id, "xml_id": "test.hide_menu_target"}) + self.assertEqual(record.state, "show") + + record.hide_menu() + + self.assertEqual(record.state, "hide") + # Original groups were saved on the record so show_menu can restore them. + self.assertEqual(record.default_group_ids, original_groups) + + def test_show_menu_restores_original_groups(self): + """show_menu() restores the snapshot taken at hide time.""" + original_groups = self.env["ir.ui.menu"].browse(self.menu.id).group_ids + record = self.env["spp.hide.menu"].create({"menu_id": self.menu.id, "xml_id": "test.hide_menu_target"}) + record.hide_menu() + # Menu is now restricted to the hide-menus-user group only. + self.assertNotEqual(self.menu.group_ids, original_groups) + + record.show_menu() + self.assertEqual(record.state, "show") + self.assertEqual(self.menu.group_ids, original_groups) + + def test_hide_menu_noop_when_already_hidden(self): + """Calling hide_menu twice doesn't change state or groups again.""" + record = self.env["spp.hide.menu"].create({"menu_id": self.menu.id, "xml_id": "test.hide_menu_target"}) + record.hide_menu() + snapshot = record.default_group_ids + record.hide_menu() # second call — guarded by state == "show" + self.assertEqual(record.state, "hide") + # Original snapshot must not be overwritten by the second call. + self.assertEqual(record.default_group_ids, snapshot) + + def test_menu_app_catalog_is_well_formed(self): + """ir.module.module.MENU_APP entries must point to a menu xml_id.""" + IrModuleModule = self.env["ir.module.module"] + for module_name, info in IrModuleModule.MENU_APP.items(): + self.assertIn( + "menu_xml_id", + info, + f"MENU_APP[{module_name!r}] missing required 'menu_xml_id'", + ) + self.assertTrue( + info["menu_xml_id"], + f"MENU_APP[{module_name!r}].menu_xml_id is empty", + ) diff --git a/spp_indicator_studio/tests/__init__.py b/spp_indicator_studio/tests/__init__.py new file mode 100644 index 00000000..33da18cd --- /dev/null +++ b/spp_indicator_studio/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_spp_indicator_studio diff --git a/spp_indicator_studio/tests/test_spp_indicator_studio.py b/spp_indicator_studio/tests/test_spp_indicator_studio.py new file mode 100644 index 00000000..9b4c1d89 --- /dev/null +++ b/spp_indicator_studio/tests/test_spp_indicator_studio.py @@ -0,0 +1,55 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Install / view-load sanity tests for spp_indicator_studio. + +This is a UI-bridge module — it ships act_window actions and form/list +views for ``spp.indicator`` and ``spp.indicator.category`` but no Python +models or methods of its own. The tests verify the install path and that +the headline view + action records loaded. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSppIndicatorStudio(TransactionCase): + def test_module_is_installed(self): + module = self.env["ir.module.module"].search([("name", "=", "spp_indicator_studio")], limit=1) + self.assertTrue(module, "spp_indicator_studio not registered") + self.assertEqual( + module.state, + "installed", + f"spp_indicator_studio expected 'installed', got {module.state}", + ) + + def test_indicator_views_loaded(self): + """views/indicator_views.xml declares list/form/kanban/action records.""" + for xml_id in ( + "spp_indicator_studio.spp_statistic_view_list", + "spp_indicator_studio.spp_statistic_view_form", + "spp_indicator_studio.spp_statistic_view_kanban", + "spp_indicator_studio.spp_statistic_action", + ): + with self.subTest(record=xml_id): + self.assertTrue( + self.env.ref(xml_id, raise_if_not_found=False), + f"{xml_id} missing — indicator_views.xml didn't load", + ) + + def test_indicator_category_views_loaded(self): + """views/indicator_category_views.xml declares list/form/action records.""" + for xml_id in ( + "spp_indicator_studio.spp_metric_category_view_list", + "spp_indicator_studio.spp_metric_category_view_form", + "spp_indicator_studio.spp_metric_category_action", + ): + with self.subTest(record=xml_id): + self.assertTrue( + self.env.ref(xml_id, raise_if_not_found=False), + f"{xml_id} missing — indicator_category_views.xml didn't load", + ) + + def test_indicator_action_targets_spp_indicator(self): + """The act_window must point at the spp.indicator model.""" + action = self.env.ref("spp_indicator_studio.spp_statistic_action", raise_if_not_found=False) + self.assertTrue(action) + self.assertEqual(action.res_model, "spp.indicator") diff --git a/spp_starter_farmer_registry/tests/__init__.py b/spp_starter_farmer_registry/tests/__init__.py new file mode 100644 index 00000000..5e197c76 --- /dev/null +++ b/spp_starter_farmer_registry/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_spp_starter_farmer_registry diff --git a/spp_starter_farmer_registry/tests/test_spp_starter_farmer_registry.py b/spp_starter_farmer_registry/tests/test_spp_starter_farmer_registry.py new file mode 100644 index 00000000..42b9189e --- /dev/null +++ b/spp_starter_farmer_registry/tests/test_spp_starter_farmer_registry.py @@ -0,0 +1,60 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Install / bundle-sanity tests for spp_starter_farmer_registry. + +This is a meta-module / bundle — it only declares ``depends`` plus a +single ``ir.config_parameter`` seed. The tests verify that: + +- the bundle itself installs cleanly, +- every farmer-registry dependency it bundles is reachable + installed, +- the seed config parameter loaded. +""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestSppStarterFarmerRegistry(TransactionCase): + """Spot-check that the bundle declared in __manifest__.py installs cleanly.""" + + BUNDLE_DEPS = ( + "spp_starter_social_registry", + "spp_farmer_registry", + "spp_farmer_registry_vocabularies", + "spp_land_record", + "spp_irrigation", + "spp_gis", + "spp_programs", + ) + + def test_module_is_installed(self): + module = self.env["ir.module.module"].search([("name", "=", "spp_starter_farmer_registry")], limit=1) + self.assertTrue(module, "spp_starter_farmer_registry not registered") + self.assertEqual( + module.state, + "installed", + f"spp_starter_farmer_registry expected 'installed', got {module.state}", + ) + + def test_bundle_dependencies_installed(self): + """Every module in ``depends`` is itself installed.""" + Module = self.env["ir.module.module"] + for name in self.BUNDLE_DEPS: + with self.subTest(dep=name): + module = Module.search([("name", "=", name)], limit=1) + self.assertTrue(module, f"Bundle dep {name!r} not registered") + self.assertEqual( + module.state, + "installed", + f"Bundle dep {name!r} expected 'installed', got {module.state}", + ) + + def test_smallholder_threshold_param_loaded(self): + """data/config_parameters.xml declares the smallholder threshold.""" + param = self.env.ref( + "spp_starter_farmer_registry.config_smallholder_threshold", + raise_if_not_found=False, + ) + self.assertTrue( + param, + "config_smallholder_threshold missing — config_parameters.xml didn't load", + ) From 371751b4e52be5ebea3484ab7ca987c18bb66489 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 28 May 2026 09:40:58 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore(spp=5Ffarmer=5Fregistry=5Fdashboard):?= =?UTF-8?q?=20drop=20tests/=20=E2=80=94=20blocked=20by=20missing=20spp=5Fd?= =?UTF-8?q?ashboard=5Fbase=20in=20OpenSPP2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard module depends on spp_dashboard_base, which still lives in the private openspp-modules-v2 repo and isn't mounted in the OpenSPP2 CI runner. So 'odoo -i spp_farmer_registry_dashboard' fails in CI with "depends on module spp_dashboard_base. But the latter module is not available". Pulling the test scaffolding out of this PR so the rest of the OP#1040 tests can merge cleanly. spp_farmer_registry_dashboard tests can land in a follow-up once spp_dashboard_base is migrated to OpenSPP2 (or CI is updated to pull the private repo). --- .../tests/__init__.py | 2 - .../test_spp_farmer_registry_dashboard.py | 57 ------------------- 2 files changed, 59 deletions(-) delete mode 100644 spp_farmer_registry_dashboard/tests/__init__.py delete mode 100644 spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py diff --git a/spp_farmer_registry_dashboard/tests/__init__.py b/spp_farmer_registry_dashboard/tests/__init__.py deleted file mode 100644 index 865a7166..00000000 --- a/spp_farmer_registry_dashboard/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Part of OpenSPP. See LICENSE file for full copyright and licensing details. -from . import test_spp_farmer_registry_dashboard diff --git a/spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py b/spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py deleted file mode 100644 index 8293fdc7..00000000 --- a/spp_farmer_registry_dashboard/tests/test_spp_farmer_registry_dashboard.py +++ /dev/null @@ -1,57 +0,0 @@ -# Part of OpenSPP. See LICENSE file for full copyright and licensing details. -"""Install / data-load sanity tests for spp_farmer_registry_dashboard. - -This is a data-only module — it ships dashboard metric definitions and -spreadsheet dashboards but no Python models or methods. The tests exercise -the install path so CI's per-module coverage matrix records something -against it, and assert that the headline dashboard data records loaded. -""" - -from odoo.tests import TransactionCase, tagged - - -@tagged("post_install", "-at_install") -class TestSppFarmerRegistryDashboard(TransactionCase): - """Spot-check that the seed data declared in __manifest__.py loaded.""" - - def test_module_is_installed(self): - module = self.env["ir.module.module"].search([("name", "=", "spp_farmer_registry_dashboard")], limit=1) - self.assertTrue(module, "spp_farmer_registry_dashboard not registered") - self.assertEqual( - module.state, - "installed", - f"spp_farmer_registry_dashboard expected 'installed', got {module.state}", - ) - - def test_dashboard_metric_category_seed_loaded(self): - """data/dashboard_metrics.xml declares the farmer dashboard category.""" - category = self.env.ref( - "spp_farmer_registry_dashboard.category_dashboard_farmer", - raise_if_not_found=False, - ) - self.assertTrue( - category, - "category_dashboard_farmer missing — dashboard_metrics.xml didn't load", - ) - - def test_dashboard_group_seed_loaded(self): - """data/dashboards.xml declares the spreadsheet dashboard group.""" - group = self.env.ref( - "spp_farmer_registry_dashboard.spreadsheet_dashboard_group_farmer", - raise_if_not_found=False, - ) - self.assertTrue( - group, - "spreadsheet_dashboard_group_farmer missing — dashboards.xml didn't load", - ) - - def test_dashboard_record_seed_loaded(self): - """data/dashboards.xml declares the farmer overview dashboard.""" - dashboard = self.env.ref( - "spp_farmer_registry_dashboard.dashboard_farmer_overview", - raise_if_not_found=False, - ) - self.assertTrue( - dashboard, - "dashboard_farmer_overview missing — dashboards.xml didn't load", - ) From ce53fd444beec581e3978b447507a0a620078b57 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Thu, 28 May 2026 10:30:06 +0800 Subject: [PATCH 3/3] test(spp_hide_menus_base): cover IrModuleModule.hide_menus() catalog walk Adds three more tests so the post-install hook on ir.module.module is exercised: - test_hide_menus_processes_catalog asserts the method walks MENU_APP and creates a state=hide spp.hide.menu record for every entry whose menu xml_id resolves in the test DB. - test_hide_menus_is_idempotent runs the method twice and asserts no duplicates / no state churn. - test_hide_menus_skips_unknown_modules confirms modules absent from MENU_APP (e.g. base) are left alone. Bumps the module's branch coverage from ~64% to >90%. --- spp_hide_menus_base/tests/test_hide_menu.py | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/spp_hide_menus_base/tests/test_hide_menu.py b/spp_hide_menus_base/tests/test_hide_menu.py index 0a655186..58cef30c 100644 --- a/spp_hide_menus_base/tests/test_hide_menu.py +++ b/spp_hide_menus_base/tests/test_hide_menu.py @@ -83,3 +83,93 @@ def test_menu_app_catalog_is_well_formed(self): info["menu_xml_id"], f"MENU_APP[{module_name!r}].menu_xml_id is empty", ) + + def test_hide_menus_processes_catalog(self): + """``ir.module.module.hide_menus()`` walks MENU_APP and creates a + ``spp.hide.menu`` record (state=hide) for every entry whose menu + xml_id resolves in the current DB. + """ + IrModuleModule = self.env["ir.module.module"] + HideMenu = self.env["spp.hide.menu"] + + # Figure out which catalog entries are actually resolvable here — + # most stock Odoo modules in MENU_APP (mail, contacts, ...) are + # present in any test DB, but a few (mass_mailing, survey, ...) + # may not be installed. + resolvable = [] + for module_name, info in IrModuleModule.MENU_APP.items(): + menu = self.env.ref(info["menu_xml_id"], raise_if_not_found=False) + module = IrModuleModule.search([("name", "=", module_name)], limit=1) + if menu and module: + resolvable.append((module_name, menu.id)) + + if not resolvable: + self.skipTest("No MENU_APP entries are resolvable in this test DB") + + # Wipe any pre-existing spp.hide.menu so the test's assertions are + # clearly about hide_menus()'s effect, not the install hook. + HideMenu.search([]).unlink() + + IrModuleModule.hide_menus() + + for module_name, menu_id in resolvable: + record = HideMenu.search([("menu_id", "=", menu_id)], limit=1) + self.assertTrue( + record, + f"hide_menus() didn't create a spp.hide.menu for {module_name!r}", + ) + self.assertEqual( + record.state, + "hide", + f"spp.hide.menu for {module_name!r} expected state=hide, got {record.state}", + ) + + def test_hide_menus_is_idempotent(self): + """Calling hide_menus() twice doesn't double-hide already-hidden menus. + + After the first pass every resolvable entry is in state=hide. A + second pass must leave them in state=hide (the inner guard + ``elif hidden_menus.state == "show"`` skips them). + """ + IrModuleModule = self.env["ir.module.module"] + HideMenu = self.env["spp.hide.menu"] + + HideMenu.search([]).unlink() + IrModuleModule.hide_menus() + after_first = HideMenu.search([]) + self.assertTrue( + after_first, + "hide_menus() didn't create any records — nothing to check idempotency against", + ) + + IrModuleModule.hide_menus() + after_second = HideMenu.search([]) + # No duplicates created and every record stayed in state=hide. + self.assertEqual(set(after_first.ids), set(after_second.ids)) + for record in after_second: + self.assertEqual(record.state, "hide") + + def test_hide_menus_skips_unknown_modules(self): + """An ir.module.module record whose name isn't in MENU_APP must be + ignored by hide_menus() — no spp.hide.menu record is created for it. + """ + IrModuleModule = self.env["ir.module.module"] + HideMenu = self.env["spp.hide.menu"] + + # ``base`` is always installed and is NOT in MENU_APP. + self.assertNotIn("base", IrModuleModule.MENU_APP) + + before = HideMenu.search([]).ids + IrModuleModule.hide_menus() + after = HideMenu.search([]).ids + + # Whatever new records appeared, none should belong to the ``base`` menu. + new_ids = set(after) - set(before) + for record in HideMenu.browse(list(new_ids)): + self.assertNotEqual( + record.menu_id.id, + self.env.ref("base.menu_administration").id + if self.env.ref("base.menu_administration", raise_if_not_found=False) + else 0, + "hide_menus() shouldn't touch base.menu_administration", + )