from __future__ import annotations import unittest from unittest.mock import patch from fastapi import HTTPException from govoplan_admin.backend.api.v1.routes import _catalog_plan_item class CatalogPlanItemTests(unittest.TestCase): def test_builds_catalog_item_and_preserves_catalog_metadata(self) -> None: validation: dict[str, object] = { "valid": True, "source": "https://catalog.example.test/modules.json", "source_type": "remote", "channel": "stable", "signed": True, "trusted": True, "modules": [ "ignored", {"module_id": "other", "action": "install"}, { "module_id": "calendar", "action": "install", "python_package": "govoplan-calendar", "python_ref": 42, "webui_package": "@govoplan/calendar-webui", "notes": "Catalog note", "license_features": ["calendar.sync", "", 7], }, ], } decision = { "allowed": True, "reason": "Feature expires soon.", "missing_features": ["calendar.future"], } with patch( "govoplan_admin.backend.api.v1.routes.module_license_decision", return_value=decision, ) as license_decision: item, returned_validation = _catalog_plan_item( "calendar", {"calendar"}, validation=validation, ) self.assertIs(returned_validation, validation) self.assertEqual(item.module_id, "calendar") self.assertEqual(item.action, "update") self.assertEqual(item.source, "catalog") self.assertEqual(item.python_package, "govoplan-calendar") self.assertIsNone(item.python_ref) self.assertEqual(item.webui_package, "@govoplan/calendar-webui") self.assertIsNone(item.webui_ref) self.assertEqual(item.notes, "Catalog note\nLicense warning: Feature expires soon.") self.assertEqual(item.catalog["source"], "https://catalog.example.test/modules.json") self.assertEqual(item.catalog["channel"], "stable") self.assertTrue(item.catalog["signed"]) license_decision.assert_called_once_with(["calendar.sync", "7"]) def test_rejects_catalog_action_when_license_is_missing(self) -> None: validation: dict[str, object] = { "valid": True, "modules": [{"module_id": "calendar", "action": "install", "license_features": ["calendar.sync"]}], } with patch( "govoplan_admin.backend.api.v1.routes.module_license_decision", return_value={"allowed": False, "missing_features": ["calendar.sync", "calendar.write"]}, ), self.assertRaises(HTTPException) as raised: _catalog_plan_item("calendar", set(), validation=validation) self.assertEqual(raised.exception.status_code, 403) self.assertEqual( raised.exception.detail, "License does not allow install for calendar: calendar.sync, calendar.write.", ) def test_rejects_invalid_or_missing_catalog_entries(self) -> None: with self.subTest("invalid catalog"), self.assertRaises(HTTPException) as invalid: _catalog_plan_item( "calendar", set(), validation={"valid": False, "error": "Signature is invalid."}, ) self.assertEqual(invalid.exception.status_code, 422) self.assertEqual(invalid.exception.detail, "Signature is invalid.") with self.subTest("entry not found"), self.assertRaises(HTTPException) as missing: _catalog_plan_item( "calendar", set(), validation={ "valid": True, "modules": [ {"module_id": "calendar", "action": "remove"}, {"module_id": "other", "action": "install"}, ], }, ) self.assertEqual(missing.exception.status_code, 404) self.assertEqual(missing.exception.detail, "Catalog install/update entry not found: calendar") if __name__ == "__main__": unittest.main()