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_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..58cef30c --- /dev/null +++ b/spp_hide_menus_base/tests/test_hide_menu.py @@ -0,0 +1,175 @@ +# 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", + ) + + 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", + ) 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", + )