from __future__ import annotations import importlib import base64 import json import os import shlex import shutil import subprocess import sys import tempfile import textwrap import tomllib import unittest from dataclasses import replace from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from sqlalchemy import Column, Integer, MetaData, Table, inspect # Keep the default app import side effect from bootstrapping a development DB. _TEST_ROOT = Path(tempfile.mkdtemp(prefix="govoplan-module-tests-")) os.environ.setdefault("APP_ENV", "test") os.environ.setdefault("DATABASE_URL", f"sqlite:///{_TEST_ROOT / 'module-test.db'}") os.environ.setdefault("DEV_BOOTSTRAP_ENABLED", "false") os.environ.setdefault("CELERY_ENABLED", "false") from fastapi.testclient import TestClient from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from govoplan_core.core.migrations import migration_metadata_plan from govoplan_core.core.module_management import ( ModuleInstallPlan, ModuleInstallPlanItem, ModuleManagementError, desired_modules_after_package_plan, module_install_plan_commands, normalize_module_install_plan_item, saved_desired_enabled_modules, saved_module_install_plan, save_desired_enabled_modules, save_module_install_plan, plan_desired_enabled_modules, ) from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, MaintenanceMode, saved_maintenance_mode, save_maintenance_mode from govoplan_core.core.module_installer import ( cancel_module_installer_request, claim_next_module_installer_request, list_module_installer_runs, list_module_installer_requests, module_installer_daemon_status, module_install_preflight, module_installer_lock_status, queue_module_installer_request, read_module_installer_request, read_module_installer_run, retry_module_installer_request, rollback_module_install_run, run_module_install_plan, supervise_module_install_plan, update_module_installer_daemon_status, update_module_installer_request, ) from govoplan_core.core.module_license import module_license_decision, validate_module_license from govoplan_core.core.module_package_catalog import ( module_package_catalog, record_module_package_catalog_acceptance, sign_module_package_catalog, validate_module_package_catalog, ) from govoplan_core.core.modules import MigrationRetirementPlan, ModuleCompatibility, ModuleUninstallGuardResult from govoplan_core.core.module_guards import drop_table_retirement_provider from govoplan_core.core.modules import MigrationSpec, ModuleManifest from govoplan_core.admin.models import SystemSettings from govoplan_core.db.base import Base from govoplan_core.db.session import configure_database, get_database from govoplan_core.security.module_permissions import scopes_grant_compatible from govoplan_core.security.permissions import scope_grants from govoplan_core.server.app import create_app from govoplan_core.server.config import GovoplanServerConfig from govoplan_core.server.registry import available_module_manifests, build_platform_registry from govoplan_files.backend.storage.backends import LocalFilesystemStorageBackend, StorageBackendError _MANIFEST_PROJECT_MODULES = { "access": "govoplan_access", "admin": "govoplan_admin", "tenancy": "govoplan_tenancy", "policy": "govoplan_policy", "audit": "govoplan_audit", "files": "govoplan_files", "mail": "govoplan_mail", "campaigns": "govoplan_campaign", } def _settings(root: Path) -> SimpleNamespace: return SimpleNamespace( app_env="test", database_url=f"sqlite:///{root / 'test.db'}", dev_mailbox_api_enabled=False, file_storage_backend="local", file_storage_local_root=str(root / "files"), file_storage_local_fallback_roots="", file_storage_s3_endpoint_url=None, file_storage_s3_region=None, file_storage_s3_bucket=None, file_storage_s3_access_key_id=None, file_storage_s3_secret_access_key=None, s3_endpoint_url=None, s3_region=None, s3_bucket=None, s3_access_key_id=None, s3_secret_access_key=None, file_upload_max_bytes=1024 * 1024, file_upload_zip_max_bytes=10 * 1024 * 1024, celery_enabled=False, redis_url="redis://127.0.0.1:6379/15", mock_mailbox_dir=str(root / "mock-mailbox"), ) class ModuleSystemTests(unittest.TestCase): @classmethod def tearDownClass(cls) -> None: shutil.rmtree(_TEST_ROOT, ignore_errors=True) def _app_for_modules(self, modules: tuple[str, ...]): root = Path(tempfile.mkdtemp(prefix="govoplan-module-app-", dir=_TEST_ROOT)) settings = _settings(root) config = GovoplanServerConfig( title="GovOPlaN Module Test", version="test", settings=settings, enabled_modules=modules, base_routers=(), post_module_routers=(), extra_routers=(), lifespan=None, app_configurators=(), ) return create_app(config), settings def _source_project_version(self, package_module: str) -> str: module = importlib.import_module(package_module) module_file = getattr(module, "__file__", None) if not module_file: self.fail(f"Could not locate source package for {package_module}") for path in Path(module_file).resolve().parents: pyproject_path = path / "pyproject.toml" if pyproject_path.exists(): with pyproject_path.open("rb") as handle: return str(tomllib.load(handle)["project"]["version"]) self.fail(f"Could not locate pyproject.toml for {package_module}") def test_discovers_installed_core_and_product_module_manifests(self) -> None: manifests = available_module_manifests() self.assertTrue({"access", "admin", "tenancy", "policy", "audit", "files", "mail", "campaigns"}.issubset(manifests)) self.assertEqual(manifests["campaigns"].dependencies, ("access",)) self.assertEqual(manifests["campaigns"].optional_dependencies, ("files", "mail")) def test_access_manifest_contributes_admin_frontend_metadata(self) -> None: manifests = available_module_manifests() access = manifests["access"] self.assertEqual(("/admin",), tuple(item.path for item in access.nav_items)) self.assertIsNotNone(access.frontend) assert access.frontend is not None self.assertEqual("@govoplan/access-webui", access.frontend.package_name) self.assertEqual(("/admin",), tuple(route.path for route in access.frontend.routes)) self.assertEqual(("/admin",), tuple(item.path for item in access.frontend.nav_items)) self.assertTrue(access.frontend.routes[0].required_any) self.assertEqual("admin", access.frontend.nav_items[0].icon) def test_tenant_wildcard_grants_module_tenant_scopes(self) -> None: self.assertTrue(scope_grants("tenant:*", "calendar:event:read")) self.assertTrue(scopes_grant_compatible(["tenant:*"], "calendar:event:read")) self.assertFalse(scope_grants("tenant:*", "system:settings:read")) def test_platform_modules_own_live_legacy_model_tables(self) -> None: from govoplan_admin.backend.db.models import GovernanceTemplate from govoplan_audit.backend.db.models import AuditLog from govoplan_access.backend.db.base import AccessBase from govoplan_access.backend.db.models import Account, ApiKey, AuthSession, User from govoplan_core.admin.models import SystemSettings from govoplan_core.db.base import Base from govoplan_tenancy.backend.db.models import Tenant self.assertIs(AccessBase, Base) self.assertEqual("accounts", Account.__tablename__) self.assertEqual("tenants", Tenant.__tablename__) self.assertEqual("users", User.__tablename__) self.assertEqual("api_keys", ApiKey.__tablename__) self.assertEqual("auth_sessions", AuthSession.__tablename__) self.assertEqual("audit_log", AuditLog.__tablename__) self.assertEqual("governance_templates", GovernanceTemplate.__tablename__) self.assertEqual("system_settings", SystemSettings.__tablename__) self.assertIn("accounts", Base.metadata.tables) self.assertIn("tenants", Base.metadata.tables) self.assertNotIn("access_accounts", Base.metadata.tables) self.assertNotIn("access_auth_sessions", Base.metadata.tables) def test_module_manifest_versions_match_source_project_versions(self) -> None: manifests = available_module_manifests() for manifest_id, package_module in _MANIFEST_PROJECT_MODULES.items(): with self.subTest(manifest_id=manifest_id): self.assertIn(manifest_id, manifests) self.assertEqual(self._source_project_version(package_module), manifests[manifest_id].version) def test_enabled_module_permutations_register_expected_routes(self) -> None: cases = ( ("core_only", (), {"tenancy", "access"}, set()), ("files_only", ("files",), {"tenancy", "access", "files"}, {"/api/v1/files"}), ("mail_only", ("mail",), {"tenancy", "access", "mail"}, {"/api/v1/mail"}), ("campaign_without_files_or_mail", ("campaigns",), {"tenancy", "access", "campaigns"}, {"/api/v1/campaigns"}), ("campaign_without_files", ("campaigns", "mail"), {"tenancy", "access", "campaigns", "mail"}, {"/api/v1/campaigns", "/api/v1/mail"}), ("campaign_with_files_but_no_mail", ("campaigns", "files"), {"tenancy", "access", "campaigns", "files"}, {"/api/v1/campaigns", "/api/v1/files"}), ("full_product", ("campaigns", "files", "mail"), {"tenancy", "access", "campaigns", "files", "mail"}, {"/api/v1/campaigns", "/api/v1/files", "/api/v1/mail"}), ) for name, enabled_modules, expected_modules, expected_prefixes in cases: with self.subTest(name=name): app, _settings_obj = self._app_for_modules(enabled_modules) route_paths = {getattr(route, "path", "") for route in app.routes} self.assertIn("/api/v1/platform/modules", route_paths) for prefix in ("/api/v1/campaigns", "/api/v1/files", "/api/v1/mail"): has_prefix = any(path == prefix or path.startswith(f"{prefix}/") for path in route_paths) self.assertEqual(prefix in expected_prefixes, has_prefix, f"{name}: {prefix}") with TestClient(app) as client: health = client.get("/health") self.assertEqual(health.status_code, 200, health.text) self.assertEqual({"status": "ok"}, health.json()) response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) payload_modules = {item["id"] for item in response.json()["modules"]} self.assertEqual(expected_modules, payload_modules) def test_governance_template_routes_are_contributed_by_admin_module(self) -> None: access_only_app, _settings_obj = self._app_for_modules(()) access_only_paths = {getattr(route, "path", "") for route in access_only_app.routes} self.assertIn("/api/v1/admin/users", access_only_paths) self.assertNotIn("/api/v1/admin/system/governance-templates", access_only_paths) admin_app, _settings_obj = self._app_for_modules(("admin",)) admin_paths = {getattr(route, "path", "") for route in admin_app.routes} self.assertIn("/api/v1/admin/system/governance-templates", admin_paths) def _run_physical_absence_probe(self, *, enabled_modules: tuple[str, ...], blocked_modules: tuple[str, ...]) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-absence-probe-", dir=_TEST_ROOT)) enabled = ",".join(("tenancy", "access", *enabled_modules)) if enabled_modules else "tenancy,access" script = f""" import importlib.abc import json import os import sys from pathlib import Path os.environ["APP_ENV"] = "test" os.environ["DATABASE_URL"] = {f"sqlite:///{root / 'probe.db'}"!r} os.environ["DEV_BOOTSTRAP_ENABLED"] = "false" os.environ["CELERY_ENABLED"] = "false" os.environ["ENABLED_MODULES"] = {enabled!r} BLOCKED = {blocked_modules!r} class Blocker(importlib.abc.MetaPathFinder): def find_spec(self, fullname, path=None, target=None): for prefix in BLOCKED: if fullname == prefix or fullname.startswith(prefix + "."): raise ModuleNotFoundError(f"No module named {{prefix!r}}", name=prefix) return None sys.meta_path.insert(0, Blocker()) from govoplan_core.server.app import create_app from govoplan_core.server.config import GovoplanServerConfig from govoplan_core.server.registry import build_platform_registry registry = build_platform_registry({enabled_modules!r}) config = GovoplanServerConfig( title="absence probe", version="test", enabled_modules={enabled_modules!r}, base_routers=(), post_module_routers=(), extra_routers=(), lifespan=None, app_configurators=(), ) app = create_app(config) route_paths = [getattr(route, "path", "") for route in app.routes] print(json.dumps({{"modules": [item.id for item in registry.manifests()], "routes": route_paths}}, sort_keys=True)) """ result = subprocess.run( [sys.executable, "-c", script], cwd=str(Path(__file__).resolve().parents[1]), env=os.environ.copy(), text=True, capture_output=True, check=False, ) if result.returncode != 0: self.fail(result.stderr or result.stdout) def test_campaign_optional_integrations_follow_enabled_modules(self) -> None: cases = ( (("campaigns",), False, False), (("campaigns", "mail"), False, True), (("campaigns", "files"), True, False), (("campaigns", "files", "mail"), True, True), ) for enabled_modules, files_enabled, mail_enabled in cases: with self.subTest(enabled_modules=enabled_modules): registry = build_platform_registry(enabled_modules) self.assertTrue(registry.has_module("campaigns")) self.assertEqual(files_enabled, registry.integration_enabled("campaigns", "files")) self.assertEqual(mail_enabled, registry.integration_enabled("campaigns", "mail")) def test_module_management_plan_adds_protected_modules_and_required_dependencies(self) -> None: manifests = available_module_manifests() plan = plan_desired_enabled_modules(("campaigns",), manifests) self.assertEqual(("tenancy", "access", "admin", "campaigns"), plan.enabled_modules) self.assertEqual((), plan.added_dependencies) def test_module_install_plan_is_saved_alongside_desired_state(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-install-plan-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: plan = save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_package": "govoplan-files", "python_ref": "govoplan-files @ git+ssh://git.example.invalid/add-ideas/govoplan-files.git@v0.1.4", "webui_package": "@govoplan/files-webui", "webui_ref": "git+ssh://git.example.invalid/add-ideas/govoplan-files.git#v0.1.4", }]) save_desired_enabled_modules(session, ("tenancy", "access", "files")) session.commit() self.assertEqual(("files",), tuple(item.module_id for item in plan.items)) with database.session() as session: saved = saved_module_install_plan(session) self.assertEqual(("files",), tuple(item.module_id for item in saved.items)) self.assertEqual(( "python -m pip install 'govoplan-files @ git+ssh://git.example.invalid/add-ideas/govoplan-files.git@v0.1.4'", "npm pkg set 'dependencies.@govoplan/files-webui=git+ssh://git.example.invalid/add-ideas/govoplan-files.git#v0.1.4' && npm install", ), module_install_plan_commands(saved)) def test_module_install_plan_rejects_local_dependency_refs(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-install-plan-local-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: with self.assertRaises(ModuleManagementError): save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_package": "govoplan-files", "python_ref": "file:../govoplan-files", }]) def test_module_install_plan_allows_uninstall_webui_package_without_ref(self) -> None: item = normalize_module_install_plan_item({ "module_id": "files", "action": "uninstall", "python_package": "govoplan-files", "webui_package": "@govoplan/files-webui", }) plan = ModuleInstallPlan(items=(item,)) self.assertIsNone(item.webui_ref) self.assertEqual(( "python -m pip uninstall -y govoplan-files", "npm pkg delete dependencies.@govoplan/files-webui && npm install", ), module_install_plan_commands(plan)) def test_desired_modules_after_package_plan_applies_default_install_uninstall_state(self) -> None: plan = ModuleInstallPlan(items=( ModuleInstallPlanItem( module_id="mail", action="install", python_package="govoplan-mail", python_ref="govoplan-mail==0.1.4", ), ModuleInstallPlanItem( module_id="files", action="uninstall", python_package="govoplan-files", ), )) self.assertEqual( ("tenancy", "access", "mail"), desired_modules_after_package_plan(("tenancy", "access", "files"), plan), ) def test_maintenance_mode_round_trips_through_system_settings(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-maintenance-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: saved = save_maintenance_mode(session, MaintenanceMode(enabled=True, message="Tagged module install")) session.commit() self.assertTrue(saved.enabled) with database.session() as session: mode = saved_maintenance_mode(session) self.assertTrue(mode.enabled) self.assertEqual("Tagged module install", mode.message) self.assertEqual("system:maintenance:access", MAINTENANCE_ACCESS_SCOPE) def test_module_installer_preflight_blocks_active_module_uninstall(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-guard-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "uninstall", "python_package": "govoplan-files", }]) session.commit() preflight = module_install_preflight( plan=plan, available=available_module_manifests(), current_enabled=("tenancy", "access", "files"), desired_enabled=("tenancy", "access", "files"), maintenance_mode=True, ) self.assertFalse(preflight.allowed) self.assertIn("module_active", {issue.code for issue in preflight.issues}) self.assertIn("module_desired", {issue.code for issue in preflight.issues}) def test_module_installer_preflight_blocks_incompatible_manifest(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-compat-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_package": "govoplan-files", "python_ref": "govoplan-files==0.1.4", }]) session.commit() available = dict(available_module_manifests()) available["files"] = replace( available["files"], compatibility=ModuleCompatibility(core_version_min="999.0.0"), ) preflight = module_install_preflight( plan=plan, available=available, current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), maintenance_mode=True, ) self.assertFalse(preflight.allowed) self.assertIn("core_version_too_low", {issue.code for issue in preflight.issues}) self.assertTrue(preflight.checklist) def test_module_installer_preflight_requires_python_package_for_install_rollback(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-install-rollback-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_ref": "govoplan-files==0.1.4", }]) session.commit() preflight = module_install_preflight( plan=plan, available=available_module_manifests(), current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), maintenance_mode=True, ) self.assertFalse(preflight.allowed) self.assertIn("python_package_required", {issue.code for issue in preflight.issues}) def test_module_installer_preflight_uses_module_uninstall_guards(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-module-guard-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "uninstall", "python_package": "govoplan-files", }]) session.commit() available = dict(available_module_manifests()) migration = available["files"].migration_spec self.assertIsNotNone(migration) assert migration is not None available["files"] = replace( available["files"], migration_spec=replace(migration, retirement_supported=False, retirement_provider=None), uninstall_guard_providers=(lambda _session, _module_id: ( ModuleUninstallGuardResult("blocker", "stored_data_present", "Stored files remain."), ),), ) preflight = module_install_preflight( plan=plan, available=available, current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), maintenance_mode=True, session=session, ) self.assertFalse(preflight.allowed) self.assertIn("stored_data_present", {issue.code for issue in preflight.issues}) self.assertIn("migration_retirement_not_requested", {issue.code for issue in preflight.issues}) def test_module_installer_allows_non_destructive_uninstall_for_migration_owner(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-nondestructive-uninstall-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "uninstall", "python_package": "govoplan-files", }]) session.commit() available = dict(available_module_manifests()) migration = available["files"].migration_spec self.assertIsNotNone(migration) assert migration is not None available["files"] = replace( available["files"], migration_spec=replace(migration, retirement_supported=False, retirement_provider=None), uninstall_guard_providers=(), ) preflight = module_install_preflight( plan=plan, available=available, current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), maintenance_mode=True, session=session, ) self.assertTrue(preflight.allowed) issues = {issue.code: issue for issue in preflight.issues} self.assertEqual("warning", issues["migration_retirement_not_requested"].severity) def test_module_installer_preflight_uses_migration_retirement_provider(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-retirement-provider-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "uninstall", "python_package": "govoplan-files", }]) session.commit() available = dict(available_module_manifests()) migration = available["files"].migration_spec self.assertIsNotNone(migration) assert migration is not None available["files"] = replace( available["files"], migration_spec=replace( migration, retirement_supported=True, retirement_provider=lambda _session, _module_id: MigrationRetirementPlan( supported=True, summary="Retirement provider can preserve historical migration state.", warnings=("Operator must verify archived file storage.",), ), ), ) preflight = module_install_preflight( plan=plan, available=available, current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), maintenance_mode=True, session=session, ) codes = {issue.code for issue in preflight.issues} self.assertTrue(preflight.allowed) self.assertNotIn("migration_retirement_not_requested", codes) self.assertIn("migration_retirement_supported", codes) self.assertIn("migration_retirement_warning", codes) def test_module_installer_destroy_data_retirement_drops_module_tables(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-destroy-data-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) metadata = MetaData() table = Table("retirement_example", metadata, Column("id", Integer, primary_key=True)) metadata.create_all(bind=database.engine) example_model = type("ExampleModel", (), {"__table__": table}) manifest = ModuleManifest( id="example", name="Example", version="0.1.0", migration_spec=MigrationSpec( module_id="example", metadata=metadata, retirement_supported=True, retirement_provider=drop_table_retirement_provider(example_model, label="Example"), ), ) def fake_run(*_args, **_kwargs): return SimpleNamespace(returncode=0, stdout="", stderr="") with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "example", "action": "uninstall", "python_package": "govoplan-example", "destroy_data": True, }]) session.commit() preflight = module_install_preflight( plan=plan, available={"example": manifest}, current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), maintenance_mode=True, session=session, ) self.assertTrue(preflight.allowed) self.assertIn("migration_retirement_destroy_planned", {issue.code for issue in preflight.issues}) with patch("govoplan_core.core.module_installer.subprocess.run", side_effect=fake_run): result = run_module_install_plan( session=session, plan=plan, available={"example": manifest}, current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), database_url=settings.database_url, runtime_dir=root / "installer", ) self.assertEqual("applied", result.status) self.assertFalse(inspect(database.engine).has_table("retirement_example")) record = json.loads(result.record_path.read_text(encoding="utf-8")) self.assertEqual("example", record["retirements"][0]["module_id"]) self.assertIn("database_backup", record["snapshot"]) def test_module_installer_dry_run_writes_run_record_without_applying(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-dry-run-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_package": "govoplan-files", "python_ref": "govoplan-files==0.1.4", }]) session.commit() result = run_module_install_plan( session=session, plan=plan, available=available_module_manifests(), current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), database_url=settings.database_url, runtime_dir=root / "installer", migrate_database=True, dry_run=True, ) self.assertEqual("dry-run", result.status) self.assertTrue(result.record_path.exists()) record_text = result.record_path.read_text(encoding="utf-8") self.assertIn("pip", record_text) self.assertIn("database_backup", record_text) self.assertIn("govoplan_core.commands.module_verify", record_text) self.assertIn("govoplan_core.commands.init_db", record_text) runs = list_module_installer_runs(runtime_dir=root / "installer") self.assertEqual((result.run_id,), tuple(item["run_id"] for item in runs)) self.assertFalse(module_installer_lock_status(runtime_dir=root / "installer")["locked"]) record = read_module_installer_run(runtime_dir=root / "installer", run_id=result.run_id) self.assertEqual(result.run_id, record["run_id"]) def test_supervised_module_install_rollback_restores_desired_modules(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-desired-rollback-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) def fake_run(*_args, **kwargs): if kwargs.get("shell"): return SimpleNamespace(returncode=1, stdout="", stderr="restart failed") return SimpleNamespace(returncode=0, stdout="", stderr="") with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "example", "action": "install", "python_package": "govoplan-example", "python_ref": "govoplan-example==0.1.4", }]) save_desired_enabled_modules(session, ("tenancy", "access")) session.commit() with patch("govoplan_core.core.module_installer.subprocess.run", side_effect=fake_run): result = supervise_module_install_plan( session=session, plan=plan, available=available_module_manifests(), current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), database_url=settings.database_url, runtime_dir=root / "installer", restart_command="restart-fails", ) restored_desired = saved_desired_enabled_modules(session, ("tenancy", "access")) restored_plan = saved_module_install_plan(session) self.assertEqual("rolled-back", result.status) self.assertEqual(("tenancy", "access"), restored_desired) self.assertEqual(("planned",), tuple(item.status for item in restored_plan.items)) def test_module_installer_external_database_backup_command_is_recorded(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-external-backup-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) backup_script = ( "import json, os, pathlib; " "pathlib.Path(os.environ['GOVOPLAN_DATABASE_BACKUP_PATH']).write_text('backup', encoding='utf-8'); " "pathlib.Path(os.environ['GOVOPLAN_DATABASE_BACKUP_METADATA']).write_text(json.dumps({'snapshot': 'external'}), encoding='utf-8')" ) backup_command = f"{sys.executable} -c {shlex.quote(backup_script)}" with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_package": "govoplan-files", "python_ref": "govoplan-files==0.1.4", }]) session.commit() result = run_module_install_plan( session=session, plan=plan, available=available_module_manifests(), current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), database_url="postgresql://db.example.invalid/govoplan", runtime_dir=root / "installer", migrate_database=True, database_backup_command=backup_command, database_restore_command="restore-database", dry_run=True, ) record = json.loads(result.record_path.read_text(encoding="utf-8")) database_backup = record["snapshot"]["database_backup"] self.assertEqual("external", database_backup["type"]) self.assertEqual({"snapshot": "external"}, database_backup["metadata"]) self.assertEqual("backup", (result.record_path.parent / "database.external.backup").read_text(encoding="utf-8")) def test_module_installer_external_database_restore_check_runs_before_migrations(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-restore-check-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) backup_script = ( "import os, pathlib; " "pathlib.Path(os.environ['GOVOPLAN_DATABASE_BACKUP_PATH']).write_text('backup', encoding='utf-8')" ) check_script = ( "import os, pathlib; " "pathlib.Path(os.environ['GOVOPLAN_INSTALLER_RUN_DIR'], 'restore-check.marker').write_text(" "pathlib.Path(os.environ['GOVOPLAN_DATABASE_BACKUP_PATH']).read_text(encoding='utf-8'), encoding='utf-8')" ) with database.session() as session: save_maintenance_mode(session, MaintenanceMode(enabled=True)) plan = save_module_install_plan(session, [{ "module_id": "files", "action": "install", "python_package": "govoplan-files", "python_ref": "govoplan-files==0.1.4", }]) session.commit() result = run_module_install_plan( session=session, plan=plan, available=available_module_manifests(), current_enabled=("tenancy", "access"), desired_enabled=("tenancy", "access"), database_url="postgresql://db.example.invalid/govoplan", runtime_dir=root / "installer", migrate_database=True, database_backup_command=f"{sys.executable} -c {shlex.quote(backup_script)}", database_restore_command="restore-database", database_restore_check_command=f"{sys.executable} -c {shlex.quote(check_script)}", dry_run=True, ) record = json.loads(result.record_path.read_text(encoding="utf-8")) database_backup = record["snapshot"]["database_backup"] self.assertEqual(0, database_backup["restore_check"]["return_code"]) self.assertEqual("backup", (result.record_path.parent / "restore-check.marker").read_text(encoding="utf-8")) def test_module_installer_external_database_restore_command_runs_on_rollback(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-external-restore-", dir=_TEST_ROOT)) runtime_dir = root / "installer" run_id = "external-restore-test" run_dir = runtime_dir / "runs" / run_id run_dir.mkdir(parents=True) restore_script = ( "import os, pathlib; " "pathlib.Path(os.environ['GOVOPLAN_INSTALLER_RUN_DIR'], 'restore.marker').write_text(" "os.environ.get('GOVOPLAN_DATABASE_URL', ''), encoding='utf-8')" ) restore_command = f"{sys.executable} -c {shlex.quote(restore_script)}" (run_dir / "database.external.backup").write_text("backup", encoding="utf-8") (run_dir / "record.json").write_text(json.dumps({ "run_id": run_id, "plan": [], "snapshot": { "database_backup": { "type": "external", "database_url": "postgresql://db.example.invalid/govoplan", "backup_path": "database.external.backup", "metadata_path": "database-backup-metadata.json", "restore_command": restore_command, }, }, }), encoding="utf-8") result = rollback_module_install_run(run_id=run_id, runtime_dir=runtime_dir, npm_bin="npm") self.assertEqual("rolled-back", result.status) self.assertEqual("postgresql://db.example.invalid/govoplan", (run_dir / "restore.marker").read_text(encoding="utf-8")) record = json.loads((run_dir / "record.json").read_text(encoding="utf-8")) self.assertTrue(record["rollback_database_restore"]["restored"]) def test_module_installer_request_queue_round_trips(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-request-queue-", dir=_TEST_ROOT)) runtime_dir = root / "installer" queued = queue_module_installer_request( runtime_dir=runtime_dir, requested_by="user-1", options={"migrate_database": True, "health_urls": ["http://127.0.0.1:8000/health"]}, ) request_id = str(queued["request_id"]) self.assertEqual("queued", queued["status"]) self.assertEqual((request_id,), tuple(item["request_id"] for item in list_module_installer_requests(runtime_dir=runtime_dir))) claimed = claim_next_module_installer_request(runtime_dir=runtime_dir) self.assertIsNotNone(claimed) assert claimed is not None self.assertEqual(request_id, claimed["request_id"]) self.assertEqual("running", claimed["status"]) updated = update_module_installer_request( runtime_dir=runtime_dir, request_id=request_id, patch={"status": "completed", "result": {"return_code": 0}}, ) self.assertEqual("completed", updated["status"]) self.assertEqual("completed", read_module_installer_request(runtime_dir=runtime_dir, request_id=request_id)["status"]) cancellable = queue_module_installer_request(runtime_dir=runtime_dir, requested_by="user-1") cancelled = cancel_module_installer_request( runtime_dir=runtime_dir, request_id=str(cancellable["request_id"]), cancelled_by="user-2", ) self.assertEqual("cancelled", cancelled["status"]) self.assertEqual("user-2", cancelled["cancelled_by"]) retry = retry_module_installer_request( runtime_dir=runtime_dir, request_id=str(cancellable["request_id"]), requested_by="user-3", ) self.assertEqual("queued", retry["status"]) self.assertEqual(cancellable["request_id"], retry["retry_of"]) def test_module_installer_daemon_status_reports_heartbeat(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-daemon-status-", dir=_TEST_ROOT)) runtime_dir = root / "installer" initial = module_installer_daemon_status(runtime_dir=runtime_dir) self.assertFalse(initial["running"]) status = update_module_installer_daemon_status( runtime_dir=runtime_dir, patch={"status": "polling", "current_request_id": None}, ) self.assertTrue(status["running"]) self.assertEqual("polling", status["status"]) def test_module_package_catalog_loads_json_entries(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" catalog_path.write_text(json.dumps({ "modules": [{ "module_id": "files", "name": "Files", "version": "0.1.4", "python_package": "govoplan-files", "python_ref": "govoplan-files==0.1.4", "tags": ["official"], }], }), encoding="utf-8") catalog = module_package_catalog(catalog_path) self.assertEqual("files", catalog[0]["module_id"]) self.assertEqual("install", catalog[0]["action"]) self.assertEqual(["official"], catalog[0]["tags"]) validation = validate_module_package_catalog(catalog_path) self.assertTrue(validation["valid"]) self.assertEqual("files", validation["modules"][0]["module_id"]) def test_module_package_catalog_validation_reports_bad_entries(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-invalid-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" catalog_path.write_text(json.dumps({"modules": [{"name": "Missing id"}]}), encoding="utf-8") validation = validate_module_package_catalog(catalog_path) self.assertFalse(validation["valid"]) self.assertIn("module_id", str(validation["error"])) def test_module_package_catalog_validates_signed_approved_channel(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-signed-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" private_key_path = root / "catalog-private.pem" signed_path = root / "catalog.signed.json" private_key = Ed25519PrivateKey.generate() private_key_path.write_bytes(private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), )) public_key = private_key.public_key().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw, ) catalog_path.write_text(json.dumps({ "channel": "stable", "modules": [{ "module_id": "files", "python_package": "govoplan-files", "python_ref": "govoplan-files==0.1.4", }], }), encoding="utf-8") sign_module_package_catalog( path=catalog_path, key_id="release-1", private_key_path=private_key_path, output_path=signed_path, ) validation = validate_module_package_catalog( signed_path, require_trusted=True, approved_channels=("stable",), trusted_keys={"release-1": base64.b64encode(public_key).decode("ascii")}, ) self.assertTrue(validation["valid"], validation["error"]) self.assertTrue(validation["signed"]) self.assertTrue(validation["trusted"]) self.assertEqual("stable", validation["channel"]) def test_module_package_catalog_rejects_unapproved_channel_when_required(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-channel-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" catalog_path.write_text(json.dumps({"channel": "nightly", "modules": [{"module_id": "files"}]}), encoding="utf-8") validation = validate_module_package_catalog(catalog_path, approved_channels=("stable",)) self.assertFalse(validation["valid"]) self.assertIn("not approved", str(validation["error"])) def test_module_package_catalog_rejects_expired_catalog(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-expired-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" catalog_path.write_text(json.dumps({ "channel": "stable", "sequence": 1, "generated_at": "2000-01-01T00:00:00Z", "expires_at": "2000-01-02T00:00:00Z", "modules": [{"module_id": "files"}], }), encoding="utf-8") validation = validate_module_package_catalog(catalog_path) self.assertFalse(validation["valid"]) self.assertIn("expired", str(validation["error"])) def test_module_package_catalog_records_sequence_acceptance(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-sequence-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" state_path = root / "sequence-state.json" catalog_path.write_text(json.dumps({ "channel": "stable", "sequence": 7, "generated_at": "2026-07-07T00:00:00Z", "expires_at": "2030-12-31T23:59:59Z", "modules": [{"module_id": "files"}], }), encoding="utf-8") with patch.dict(os.environ, { "GOVOPLAN_MODULE_PACKAGE_CATALOG_SEQUENCE_STATE": str(state_path), "GOVOPLAN_MODULE_PACKAGE_CATALOG_ENFORCE_SEQUENCE": "true", "GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNELS": "", "GOVOPLAN_MODULE_PACKAGE_CATALOG_REQUIRE_SIGNATURE": "", }): validation = validate_module_package_catalog(catalog_path) self.assertTrue(validation["valid"], validation["error"]) record_module_package_catalog_acceptance(validation) replay = validate_module_package_catalog(catalog_path) self.assertFalse(replay["valid"]) self.assertIn("already accepted", str(replay["error"])) def test_module_license_decision_is_observe_only_unless_enforced(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-license-observe-", dir=_TEST_ROOT)) missing_license = root / "missing-license.json" with patch.dict(os.environ, { "GOVOPLAN_LICENSE_FILE": str(missing_license), "GOVOPLAN_LICENSE_ENFORCEMENT": "", }): observe = module_license_decision(("module.mail",)) with patch.dict(os.environ, { "GOVOPLAN_LICENSE_FILE": str(missing_license), "GOVOPLAN_LICENSE_ENFORCEMENT": "true", }): enforced = module_license_decision(("module.mail",)) self.assertTrue(observe["allowed"]) self.assertFalse(observe["enforced"]) self.assertFalse(enforced["allowed"]) self.assertTrue(enforced["enforced"]) def test_module_license_validates_signed_feature_license(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-license-signed-", dir=_TEST_ROOT)) license_path = root / "license.json" private_key = Ed25519PrivateKey.generate() public_key = private_key.public_key().public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw, ) payload = { "license_id": "test-license", "subject": "Test installation", "features": ["module.mail"], "valid_from": "2026-01-01T00:00:00Z", "valid_until": "2030-12-31T23:59:59Z", } signature = private_key.sign(json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")) payload["signature"] = { "algorithm": "ed25519", "key_id": "license-1", "value": base64.b64encode(signature).decode("ascii"), } license_path.write_text(json.dumps(payload), encoding="utf-8") trusted_keys = {"license-1": base64.b64encode(public_key).decode("ascii")} validation = validate_module_license(license_path, trusted_keys=trusted_keys, require_trusted=True) self.assertTrue(validation["valid"], validation["error"]) self.assertTrue(validation["trusted"]) self.assertEqual(["module.mail"], validation["features"]) def test_module_installer_validates_package_catalog_from_cli(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-cli-", dir=_TEST_ROOT)) catalog_path = root / "catalog.json" catalog_path.write_text(json.dumps({"modules": [{"module_id": "files"}]}), encoding="utf-8") result = subprocess.run( [ sys.executable, "-m", "govoplan_core.commands.module_installer", "--validate-package-catalog", str(catalog_path), "--format", "json", ], cwd=str(Path(__file__).resolve().parents[1]), env=os.environ.copy(), text=True, capture_output=True, check=False, ) self.assertEqual(0, result.returncode, result.stderr) self.assertTrue(json.loads(result.stdout)["valid"]) def test_module_installer_daemon_once_exits_with_empty_queue(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-daemon-once-", dir=_TEST_ROOT)) result = subprocess.run( [ sys.executable, "-m", "govoplan_core.commands.module_installer", "--daemon-once", "--runtime-dir", str(root / "installer"), "--database-url", f"sqlite:///{root / 'daemon.db'}", ], cwd=str(Path(__file__).resolve().parents[1]), env=os.environ.copy(), text=True, capture_output=True, check=False, ) self.assertEqual(0, result.returncode, result.stderr) def test_module_installer_rollback_rebuilds_webui_when_original_run_built(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-installer-rollback-webui-", dir=_TEST_ROOT)) runtime_dir = root / "installer" run_id = "rollback-webui-test" run_dir = runtime_dir / "runs" / run_id webui_root = root / "webui" run_dir.mkdir(parents=True) webui_root.mkdir() (webui_root / "package.json").write_text('{"scripts":{"build":"vite build"}}', encoding="utf-8") (run_dir / "package.json.before").write_text('{"scripts":{"build":"vite build"},"dependencies":{}}', encoding="utf-8") (run_dir / "package-lock.json.before").write_text('{"lockfileVersion":3}', encoding="utf-8") (run_dir / "record.json").write_text(json.dumps({ "run_id": run_id, "build_webui": True, "plan": [{ "module_id": "example", "action": "install", "python_package": "govoplan-example", }], "snapshot": { "webui_root": str(webui_root), "package_json": "package.json.before", "package_lock": "package-lock.json.before", }, }), encoding="utf-8") with patch("govoplan_core.core.module_installer.subprocess.run") as run_mock: run_mock.return_value = SimpleNamespace(returncode=0, stdout="", stderr="") result = rollback_module_install_run(run_id=run_id, runtime_dir=runtime_dir, npm_bin="npm") commands = [call.args[0] for call in run_mock.call_args_list] self.assertEqual("rolled-back", result.status) self.assertIn([sys.executable, "-m", "pip", "uninstall", "-y", "govoplan-example"], commands) self.assertIn(["npm", "install"], commands) self.assertIn(["npm", "run", "build"], commands) def test_create_app_uses_saved_desired_module_state_on_startup(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-state-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: session.add(SystemSettings( id="global", settings={"module_management": {"desired_enabled": ["files"]}}, )) session.commit() config = GovoplanServerConfig( title="GovOPlaN Module State Test", version="test", settings=settings, enabled_modules=(), base_routers=(), post_module_routers=(), extra_routers=(), lifespan=None, app_configurators=(), ) app = create_app(config) with TestClient(app) as client: response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) payload_modules = {item["id"] for item in response.json()["modules"]} self.assertEqual({"tenancy", "access", "files"}, payload_modules) def test_create_app_ignores_saved_desired_modules_that_are_not_installed(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-state-stale-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: session.add(SystemSettings( id="global", settings={"module_management": {"desired_enabled": ["files", "missing-module"]}}, )) session.commit() config = GovoplanServerConfig( title="GovOPlaN Module State Test", version="test", settings=settings, enabled_modules=(), base_routers=(), post_module_routers=(), extra_routers=(), lifespan=None, app_configurators=(), ) app = create_app(config) with TestClient(app) as client: response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) payload_modules = {item["id"] for item in response.json()["modules"]} self.assertEqual({"tenancy", "access", "files"}, payload_modules) def test_create_app_preserves_admin_when_configured_and_saved_state_is_older(self) -> None: root = Path(tempfile.mkdtemp(prefix="govoplan-module-state-admin-", dir=_TEST_ROOT)) settings = _settings(root) configure_database(settings.database_url) database = get_database() Base.metadata.create_all(bind=database.engine, tables=[SystemSettings.__table__]) with database.session() as session: session.add(SystemSettings( id="global", settings={"module_management": {"desired_enabled": ["files"]}}, )) session.commit() config = GovoplanServerConfig( title="GovOPlaN Module State Test", version="test", settings=settings, enabled_modules=("admin",), base_routers=(), post_module_routers=(), extra_routers=(), lifespan=None, app_configurators=(), ) app = create_app(config) with TestClient(app) as client: response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) payload_modules = {item["id"] for item in response.json()["modules"]} self.assertEqual({"tenancy", "access", "admin", "files"}, payload_modules) def test_module_lifecycle_enables_and_disables_routes_without_restart(self) -> None: app, _settings_obj = self._app_for_modules(()) lifecycle = getattr(app.state, "govoplan_lifecycle", None) self.assertIsNotNone(lifecycle) with TestClient(app) as client: response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) self.assertEqual({"tenancy", "access"}, {item["id"] for item in response.json()["modules"]}) self.assertFalse(any(getattr(route, "path", "") == "/api/v1/files" for route in app.routes)) result = lifecycle.apply_enabled_modules(("files",), migrate=False) self.assertEqual(("files",), result.activated_modules) response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) self.assertEqual({"tenancy", "access", "files"}, {item["id"] for item in response.json()["modules"]}) self.assertTrue(any(getattr(route, "path", "") == "/api/v1/files" for route in app.routes)) self.assertEqual(401, client.get("/api/v1/files").status_code) result = lifecycle.apply_enabled_modules((), migrate=False) self.assertEqual(("files",), result.deactivated_modules) response = client.get("/api/v1/platform/modules") self.assertEqual(response.status_code, 200, response.text) self.assertEqual({"tenancy", "access"}, {item["id"] for item in response.json()["modules"]}) disabled_response = client.get("/api/v1/files") self.assertEqual(404, disabled_response.status_code) self.assertEqual("Module is disabled: files", disabled_response.json()["detail"]) def test_module_permutations_start_when_absent_modules_are_physically_unavailable(self) -> None: cases = ( ((), ("govoplan_files", "govoplan_mail", "govoplan_campaign")), (("files",), ("govoplan_mail", "govoplan_campaign")), (("mail",), ("govoplan_files", "govoplan_campaign")), (("campaigns",), ("govoplan_files", "govoplan_mail")), (("campaigns", "files"), ("govoplan_mail",)), (("campaigns", "mail"), ("govoplan_files",)), (("campaigns", "files", "mail"), ()), ) for enabled_modules, blocked_modules in cases: with self.subTest(enabled_modules=enabled_modules, blocked_modules=blocked_modules): self._run_physical_absence_probe(enabled_modules=enabled_modules, blocked_modules=blocked_modules) def test_module_route_factories_receive_runtime_settings(self) -> None: app, settings = self._app_for_modules(("files", "mail")) self.assertIsNotNone(app) from govoplan_files.backend.runtime import get_settings as get_files_settings from govoplan_mail.backend.runtime import get_settings as get_mail_settings self.assertIs(settings, get_files_settings()) self.assertIs(settings, get_mail_settings()) def test_module_migrations_are_registered_only_for_enabled_modules(self) -> None: core_plan = migration_metadata_plan(build_platform_registry(())) self.assertEqual((), core_plan.script_locations) self.assertEqual(1, len(core_plan.metadata)) files_plan = migration_metadata_plan(build_platform_registry(("files",))) self.assertEqual(1, len(files_plan.script_locations)) self.assertTrue(files_plan.script_locations[0].endswith("govoplan_files/backend/migrations/versions")) full_plan = migration_metadata_plan(build_platform_registry(("campaigns", "files", "mail"))) locations = "\n".join(full_plan.script_locations) self.assertIn("govoplan_campaign/backend/migrations/versions", locations) self.assertIn("govoplan_files/backend/migrations/versions", locations) self.assertIn("govoplan_mail/backend/migrations/versions", locations) def test_local_storage_backend_reads_from_fallback_roots_without_copying(self) -> None: with tempfile.TemporaryDirectory(prefix="govoplan-storage-fallback-") as directory: root = Path(directory) / "primary" fallback = Path(directory) / "fallback" key = "tenant-a/files/demo.txt" (fallback / key).parent.mkdir(parents=True) (fallback / key).write_bytes(b"fallback-data") backend = LocalFilesystemStorageBackend(root=root, fallback_roots=(fallback,)) self.assertTrue(backend.exists(key)) self.assertEqual(b"fallback-data", backend.get_bytes(key)) self.assertEqual([b"fallback", b"-data"], list(backend.iter_bytes(key, chunk_size=8))) self.assertFalse((root / key).exists()) backend.delete(key) self.assertTrue((fallback / key).exists()) self.assertEqual(b"fallback-data", backend.get_bytes(key)) with self.assertRaises(StorageBackendError): backend.get_bytes("../escape.txt") if __name__ == "__main__": unittest.main()